-module(sqlode@query_analyzer@token_utils). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/sqlode/query_analyzer/token_utils.gleam"). -export([skip_parens/2, read_table_name/1, extract_placeholders/1, collect_paren_contents/1, read_subquery_alias/1, extract_table_names/1, split_on_commas/1, find_insert_parts/1, find_equality_patterns/1, find_in_patterns/1, find_type_casts/1, parse_placeholder_index/1, find_set_patterns/1]). -export_type([insert_parts/0, equality_match/0, type_cast/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. -type insert_parts() :: {insert_parts, binary(), list(binary()), list(list(sqlode@lexer:token()))}. -type equality_match() :: {equality_match, binary(), gleam@option:option(binary()), binary()}. -type type_cast() :: {type_cast, binary(), binary()}. -file("src/sqlode/query_analyzer/token_utils.gleam", 121). ?DOC(" Skip tokens until all parentheses at the given depth are closed.\n"). -spec skip_parens(list(sqlode@lexer:token()), integer()) -> list(sqlode@lexer:token()). skip_parens(Tokens, Depth) -> case Depth =< 0 of true -> Tokens; false -> case Tokens of [] -> []; [l_paren | Rest] -> skip_parens(Rest, Depth + 1); [r_paren | Rest@1] -> skip_parens(Rest@1, Depth - 1); [_ | Rest@2] -> skip_parens(Rest@2, Depth) end end. -file("src/sqlode/query_analyzer/token_utils.gleam", 102). ?DOC( " Read a table name from the current token position, handling schema-qualified\n" " names (schema.table) and subqueries in parentheses.\n" ). -spec read_table_name(list(sqlode@lexer:token())) -> {gleam@option:option(binary()), list(sqlode@lexer:token())}. read_table_name(Tokens) -> case Tokens of [{ident, _}, dot, {ident, Name} | Rest] -> {{some, string:lowercase(Name)}, Rest}; [{ident, Name@1} | Rest@1] -> {{some, string:lowercase(Name@1)}, Rest@1}; [{quoted_ident, Name@2} | Rest@2] -> {{some, string:lowercase(Name@2)}, Rest@2}; [l_paren | Rest@3] -> Remaining = skip_parens(Rest@3, 1), {none, Remaining}; _ -> {none, Tokens} end. -file("src/sqlode/query_analyzer/token_utils.gleam", 139). ?DOC(" Extract all placeholder token strings from a token list.\n"). -spec extract_placeholders(list(sqlode@lexer:token())) -> list(binary()). extract_placeholders(Tokens) -> gleam@list:filter_map(Tokens, fun(Token) -> case Token of {placeholder, P} -> {ok, P}; _ -> {error, nil} end end). -file("src/sqlode/query_analyzer/token_utils.gleam", 157). -spec collect_paren_loop( list(sqlode@lexer:token()), integer(), list(sqlode@lexer:token()) ) -> {list(sqlode@lexer:token()), list(sqlode@lexer:token())}. collect_paren_loop(Tokens, Depth, Acc) -> case Depth =< 0 of true -> {lists:reverse(Acc), Tokens}; false -> case Tokens of [] -> {lists:reverse(Acc), []}; [l_paren | Rest] -> collect_paren_loop(Rest, Depth + 1, [l_paren | Acc]); [r_paren | Rest@1] -> case Depth =:= 1 of true -> {lists:reverse(Acc), Rest@1}; false -> collect_paren_loop( Rest@1, Depth - 1, [r_paren | Acc] ) end; [Token | Rest@2] -> collect_paren_loop(Rest@2, Depth, [Token | Acc]) end end. -file("src/sqlode/query_analyzer/token_utils.gleam", 151). ?DOC( " Collect tokens inside the next parenthesized group.\n" " Expects tokens starting right after the opening LParen.\n" " Returns #(inner_tokens, remaining_tokens_after_RParen).\n" ). -spec collect_paren_contents(list(sqlode@lexer:token())) -> {list(sqlode@lexer:token()), list(sqlode@lexer:token())}. collect_paren_contents(Tokens) -> collect_paren_loop(Tokens, 1, []). -file("src/sqlode/query_analyzer/token_utils.gleam", 78). ?DOC( " Read an optional AS followed by an identifier alias, optionally\n" " followed by a parenthesised column list. Returns the alias and the\n" " token stream positioned after the alias (and column list, if any).\n" ). -spec read_subquery_alias(list(sqlode@lexer:token())) -> {gleam@option:option(binary()), list(sqlode@lexer:token())}. read_subquery_alias(Tokens) -> After_as = case Tokens of [{keyword, <<"as"/utf8>>} | Rest] -> Rest; _ -> Tokens end, case After_as of [{ident, Name}, l_paren | Rest_after_lp] -> {_, After_cols} = collect_paren_contents(Rest_after_lp), {{some, string:lowercase(Name)}, After_cols}; [{quoted_ident, Name@1}, l_paren | Rest_after_lp@1] -> {_, After_cols@1} = collect_paren_contents(Rest_after_lp@1), {{some, string:lowercase(Name@1)}, After_cols@1}; [{ident, Name@2} | Rest@1] -> {{some, string:lowercase(Name@2)}, Rest@1}; [{quoted_ident, Name@3} | Rest@2] -> {{some, string:lowercase(Name@3)}, Rest@2}; _ -> {none, After_as} end. -file("src/sqlode/query_analyzer/token_utils.gleam", 14). -spec table_names_loop(list(sqlode@lexer:token()), list(binary())) -> list(binary()). table_names_loop(Tokens, Acc) -> case Tokens of [] -> lists:reverse(Acc); [{keyword, <<"from"/utf8>>}, l_paren | Rest] -> Remaining = skip_parens(Rest, 1), case read_subquery_alias(Remaining) of {{some, N}, After_alias} -> table_names_loop(After_alias, [N | Acc]); {none, _} -> table_names_loop(Remaining, Acc) end; [{keyword, Kw} | Rest@1] when ((Kw =:= <<"from"/utf8>>) orelse (Kw =:= <<"into"/utf8>>)) orelse (Kw =:= <<"update"/utf8>>) -> {Name, Remaining@1} = read_table_name(Rest@1), case Name of {some, N@1} -> table_names_loop(Remaining@1, [N@1 | Acc]); none -> table_names_loop(Rest@1, Acc) end; [{keyword, <<"join"/utf8>>}, {keyword, <<"lateral"/utf8>>}, l_paren | Rest@2] -> Remaining@2 = skip_parens(Rest@2, 1), case read_subquery_alias(Remaining@2) of {{some, N@2}, After_alias@1} -> table_names_loop(After_alias@1, [N@2 | Acc]); {none, _} -> table_names_loop(Remaining@2, Acc) end; [{keyword, <<"join"/utf8>>}, l_paren | Rest@3] -> Remaining@3 = skip_parens(Rest@3, 1), case read_subquery_alias(Remaining@3) of {{some, N@3}, After_alias@2} -> table_names_loop(After_alias@2, [N@3 | Acc]); {none, _} -> table_names_loop(Remaining@3, Acc) end; [{keyword, <<"lateral"/utf8>>}, l_paren | Rest@4] -> Remaining@4 = skip_parens(Rest@4, 1), case read_subquery_alias(Remaining@4) of {{some, N@4}, After_alias@3} -> table_names_loop(After_alias@3, [N@4 | Acc]); {none, _} -> table_names_loop(Remaining@4, Acc) end; [{keyword, <<"join"/utf8>>} | Rest@5] -> {Name@1, Remaining@5} = read_table_name(Rest@5), case Name@1 of {some, N@5} -> table_names_loop(Remaining@5, [N@5 | Acc]); none -> table_names_loop(Rest@5, Acc) end; [_ | Rest@6] -> table_names_loop(Rest@6, Acc) end. -file("src/sqlode/query_analyzer/token_utils.gleam", 9). ?DOC(" Extract all table names referenced in a token list (FROM, INTO, UPDATE, JOIN).\n"). -spec extract_table_names(list(sqlode@lexer:token())) -> list(binary()). extract_table_names(Tokens) -> _pipe = table_names_loop(Tokens, []), gleam@list:unique(_pipe). -file("src/sqlode/query_analyzer/token_utils.gleam", 184). -spec split_commas_loop( list(sqlode@lexer:token()), integer(), list(sqlode@lexer:token()), list(list(sqlode@lexer:token())) ) -> list(list(sqlode@lexer:token())). split_commas_loop(Tokens, Depth, Current, Acc) -> case Tokens of [] -> case Current of [] -> lists:reverse(Acc); _ -> lists:reverse([lists:reverse(Current) | Acc]) end; [comma | Rest] when Depth =:= 0 -> case Current of [] -> split_commas_loop(Rest, 0, [], Acc); _ -> split_commas_loop( Rest, 0, [], [lists:reverse(Current) | Acc] ) end; [l_paren | Rest@1] -> split_commas_loop(Rest@1, Depth + 1, [l_paren | Current], Acc); [r_paren | Rest@2] -> split_commas_loop(Rest@2, Depth - 1, [r_paren | Current], Acc); [Token | Rest@3] -> split_commas_loop(Rest@3, Depth, [Token | Current], Acc) end. -file("src/sqlode/query_analyzer/token_utils.gleam", 180). ?DOC(" Split tokens on top-level commas (depth 0).\n"). -spec split_on_commas(list(sqlode@lexer:token())) -> list(list(sqlode@lexer:token())). split_on_commas(Tokens) -> split_commas_loop(Tokens, 0, [], []). -file("src/sqlode/query_analyzer/token_utils.gleam", 275). -spec skip_to_values(list(sqlode@lexer:token())) -> gleam@option:option(list(sqlode@lexer:token())). skip_to_values(Tokens) -> case Tokens of [] -> none; [{keyword, <<"values"/utf8>>} | Rest] -> {some, Rest}; [_ | Rest@1] -> skip_to_values(Rest@1) end. -file("src/sqlode/query_analyzer/token_utils.gleam", 235). -spec parse_insert_after_into(list(sqlode@lexer:token())) -> gleam@option:option(insert_parts()). parse_insert_after_into(Tokens) -> {Table_name_opt, Rest} = read_table_name(Tokens), case Table_name_opt of none -> none; {some, Table_name} -> case Rest of [l_paren | After_lparen] -> {Col_tokens, After_cols} = collect_paren_contents( After_lparen ), Columns = begin _pipe = split_on_commas(Col_tokens), gleam@list:filter_map(_pipe, fun(Group) -> case Group of [{ident, Name}] -> {ok, sqlode@naming:normalize_identifier( Name )}; [{quoted_ident, Name@1}] -> {ok, sqlode@naming:normalize_identifier( Name@1 )}; _ -> {error, nil} end end) end, case skip_to_values(After_cols) of none -> none; {some, After_values_kw} -> case After_values_kw of [l_paren | After_vlparen] -> {Val_tokens, _} = collect_paren_contents( After_vlparen ), Values = split_on_commas(Val_tokens), {some, {insert_parts, Table_name, Columns, Values}}; _ -> none end end; _ -> none end end. -file("src/sqlode/query_analyzer/token_utils.gleam", 226). -spec find_insert_loop(list(sqlode@lexer:token())) -> gleam@option:option(insert_parts()). find_insert_loop(Tokens) -> case Tokens of [] -> none; [{keyword, <<"insert"/utf8>>}, {keyword, <<"into"/utf8>>} | Rest] -> parse_insert_after_into(Rest); [_ | Rest@1] -> find_insert_loop(Rest@1) end. -file("src/sqlode/query_analyzer/token_utils.gleam", 222). ?DOC(" Find INSERT INTO table (columns) VALUES (values) structure in tokens.\n"). -spec find_insert_parts(list(sqlode@lexer:token())) -> gleam@option:option(insert_parts()). find_insert_parts(Tokens) -> find_insert_loop(Tokens). -file("src/sqlode/query_analyzer/token_utils.gleam", 301). -spec find_equality_loop(list(sqlode@lexer:token()), list(equality_match())) -> list(equality_match()). find_equality_loop(Tokens, Acc) -> case Tokens of [] -> Acc; [{ident, T}, dot, {ident, C}, {operator, Op}, {placeholder, P} | Rest] when ((((((Op =:= <<"="/utf8>>) orelse (Op =:= <<"!="/utf8>>)) orelse (Op =:= <<"<>"/utf8>>)) orelse (Op =:= <<"<"/utf8>>)) orelse (Op =:= <<">"/utf8>>)) orelse (Op =:= <<"<="/utf8>>)) orelse (Op =:= <<">="/utf8>>) -> find_equality_loop( Rest, [{equality_match, sqlode@naming:normalize_identifier(C), {some, string:lowercase(T)}, P} | Acc] ); [{ident, T@1}, dot, {quoted_ident, C@1}, {operator, Op@1}, {placeholder, P@1} | Rest@1] when ((((((Op@1 =:= <<"="/utf8>>) orelse (Op@1 =:= <<"!="/utf8>>)) orelse (Op@1 =:= <<"<>"/utf8>>)) orelse (Op@1 =:= <<"<"/utf8>>)) orelse (Op@1 =:= <<">"/utf8>>)) orelse (Op@1 =:= <<"<="/utf8>>)) orelse (Op@1 =:= <<">="/utf8>>) -> find_equality_loop( Rest@1, [{equality_match, sqlode@naming:normalize_identifier(C@1), {some, string:lowercase(T@1)}, P@1} | Acc] ); [{ident, C@2}, {operator, Op@2}, {placeholder, P@2} | Rest@2] when ((((((Op@2 =:= <<"="/utf8>>) orelse (Op@2 =:= <<"!="/utf8>>)) orelse (Op@2 =:= <<"<>"/utf8>>)) orelse (Op@2 =:= <<"<"/utf8>>)) orelse (Op@2 =:= <<">"/utf8>>)) orelse (Op@2 =:= <<"<="/utf8>>)) orelse (Op@2 =:= <<">="/utf8>>) -> find_equality_loop( Rest@2, [{equality_match, sqlode@naming:normalize_identifier(C@2), none, P@2} | Acc] ); [{quoted_ident, C@3}, {operator, Op@3}, {placeholder, P@3} | Rest@3] when ((((((Op@3 =:= <<"="/utf8>>) orelse (Op@3 =:= <<"!="/utf8>>)) orelse (Op@3 =:= <<"<>"/utf8>>)) orelse (Op@3 =:= <<"<"/utf8>>)) orelse (Op@3 =:= <<">"/utf8>>)) orelse (Op@3 =:= <<"<="/utf8>>)) orelse (Op@3 =:= <<">="/utf8>>) -> find_equality_loop( Rest@3, [{equality_match, sqlode@naming:normalize_identifier(C@3), none, P@3} | Acc] ); [{ident, T@2}, dot, {ident, C@4}, {keyword, Kw}, {placeholder, P@4} | Rest@4] when (Kw =:= <<"like"/utf8>>) orelse (Kw =:= <<"ilike"/utf8>>) -> find_equality_loop( Rest@4, [{equality_match, sqlode@naming:normalize_identifier(C@4), {some, string:lowercase(T@2)}, P@4} | Acc] ); [{ident, C@5}, {keyword, Kw@1}, {placeholder, P@5} | Rest@5] when (Kw@1 =:= <<"like"/utf8>>) orelse (Kw@1 =:= <<"ilike"/utf8>>) -> find_equality_loop( Rest@5, [{equality_match, sqlode@naming:normalize_identifier(C@5), none, P@5} | Acc] ); [_ | Rest@6] -> find_equality_loop(Rest@6, Acc) end. -file("src/sqlode/query_analyzer/token_utils.gleam", 296). ?DOC(" Find all column [op] placeholder patterns in tokens.\n"). -spec find_equality_patterns(list(sqlode@lexer:token())) -> list(equality_match()). find_equality_patterns(Tokens) -> _pipe = find_equality_loop(Tokens, []), lists:reverse(_pipe). -file("src/sqlode/query_analyzer/token_utils.gleam", 443). -spec find_in_loop(list(sqlode@lexer:token()), list(equality_match())) -> list(equality_match()). find_in_loop(Tokens, Acc) -> case Tokens of [] -> Acc; [{ident, T}, dot, {ident, C}, {keyword, <<"in"/utf8>>}, l_paren, {placeholder, P}, r_paren | Rest] -> find_in_loop( Rest, [{equality_match, sqlode@naming:normalize_identifier(C), {some, string:lowercase(T)}, P} | Acc] ); [{ident, C@1}, {keyword, <<"in"/utf8>>}, l_paren, {placeholder, P@1}, r_paren | Rest@1] -> find_in_loop( Rest@1, [{equality_match, sqlode@naming:normalize_identifier(C@1), none, P@1} | Acc] ); [{quoted_ident, C@2}, {keyword, <<"in"/utf8>>}, l_paren, {placeholder, P@2}, r_paren | Rest@2] -> find_in_loop( Rest@2, [{equality_match, sqlode@naming:normalize_identifier(C@2), none, P@2} | Acc] ); [_ | Rest@3] -> find_in_loop(Rest@3, Acc) end. -file("src/sqlode/query_analyzer/token_utils.gleam", 438). ?DOC(" Find all column IN (placeholder) patterns in tokens.\n"). -spec find_in_patterns(list(sqlode@lexer:token())) -> list(equality_match()). find_in_patterns(Tokens) -> _pipe = find_in_loop(Tokens, []), lists:reverse(_pipe). -file("src/sqlode/query_analyzer/token_utils.gleam", 523). -spec find_type_cast_loop(list(sqlode@lexer:token()), list(type_cast())) -> list(type_cast()). find_type_cast_loop(Tokens, Acc) -> case Tokens of [] -> Acc; [{placeholder, P}, {operator, <<"::"/utf8>>}, {ident, T} | Rest] -> find_type_cast_loop( Rest, [{type_cast, P, string:lowercase(T)} | Acc] ); [{placeholder, P@1}, {operator, <<"::"/utf8>>}, {keyword, T@1} | Rest@1] -> find_type_cast_loop(Rest@1, [{type_cast, P@1, T@1} | Acc]); [_ | Rest@2] -> find_type_cast_loop(Rest@2, Acc) end. -file("src/sqlode/query_analyzer/token_utils.gleam", 518). ?DOC(" Find all $N::type patterns in tokens (PostgreSQL only).\n"). -spec find_type_casts(list(sqlode@lexer:token())) -> list(type_cast()). find_type_casts(Tokens) -> _pipe = find_type_cast_loop(Tokens, []), lists:reverse(_pipe). -file("src/sqlode/query_analyzer/token_utils.gleam", 546). ?DOC(" Parse a placeholder string like \"$3\" into its integer index.\n"). -spec parse_placeholder_index(binary()) -> {ok, integer()} | {error, nil}. parse_placeholder_index(Placeholder) -> _pipe = Placeholder, _pipe@1 = gleam@string:replace(_pipe, <<"$"/utf8>>, <<""/utf8>>), _pipe@2 = gleam_stdlib:parse_int(_pipe@1), _pipe@3 = gleam@option:from_result(_pipe@2), gleam@option:to_result(_pipe@3, nil). -file("src/sqlode/query_analyzer/token_utils.gleam", 575). -spec scan_set_assignments(list(sqlode@lexer:token()), list(equality_match())) -> list(equality_match()). scan_set_assignments(Tokens, Acc) -> case Tokens of [] -> Acc; [{keyword, Kw} | _] when ((Kw =:= <<"where"/utf8>>) orelse (Kw =:= <<"returning"/utf8>>)) orelse (Kw =:= <<"from"/utf8>>) -> Acc; [{ident, C}, {operator, <<"="/utf8>>}, {placeholder, P} | Rest] -> scan_set_assignments( Rest, [{equality_match, sqlode@naming:normalize_identifier(C), none, P} | Acc] ); [{quoted_ident, C@1}, {operator, <<"="/utf8>>}, {placeholder, P@1} | Rest@1] -> scan_set_assignments( Rest@1, [{equality_match, sqlode@naming:normalize_identifier(C@1), none, P@1} | Acc] ); [_ | Rest@2] -> scan_set_assignments(Rest@2, Acc) end. -file("src/sqlode/query_analyzer/token_utils.gleam", 564). -spec find_set_clause(list(sqlode@lexer:token()), list(equality_match())) -> list(equality_match()). find_set_clause(Tokens, Acc) -> case Tokens of [] -> Acc; [{keyword, <<"set"/utf8>>} | Rest] -> scan_set_assignments(Rest, Acc); [_ | Rest@1] -> find_set_clause(Rest@1, Acc) end. -file("src/sqlode/query_analyzer/token_utils.gleam", 559). ?DOC(" Find all column = placeholder patterns in SET clauses.\n"). -spec find_set_patterns(list(sqlode@lexer:token())) -> list(equality_match()). find_set_patterns(Tokens) -> _pipe = find_set_clause(Tokens, []), lists:reverse(_pipe).