-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, structure_tokens/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", 122). ?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", 103). ?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", 140). ?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", 158). -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", 152). ?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", 79). ?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", 15). -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", 10). ?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", 185). -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", 181). ?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", 276). -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", 236). -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", 227). -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", 223). ?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", 302). -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", 297). ?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", 444). -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", 439). ?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", 524). -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", 519). ?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", 547). ?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", 576). -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", 565). -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", 560). ?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). -file("src/sqlode/query_analyzer/token_utils.gleam", 635). -spec skip_cte_body(list(sqlode@lexer:token())) -> list(sqlode@lexer:token()). skip_cte_body(Tokens) -> case Tokens of [] -> []; [{keyword, <<"select"/utf8>>} | _] = T -> T; [{keyword, <<"insert"/utf8>>} | _] = T@1 -> T@1; [{keyword, <<"update"/utf8>>} | _] = T@2 -> T@2; [{keyword, <<"delete"/utf8>>} | _] = T@3 -> T@3; [l_paren | Rest] -> After = skip_parens(Rest, 1), skip_cte_body(After); [_ | Rest@1] -> skip_cte_body(Rest@1) end. -file("src/sqlode/query_analyzer/token_utils.gleam", 628). -spec strip_leading_cte(list(sqlode@lexer:token())) -> list(sqlode@lexer:token()). strip_leading_cte(Tokens) -> case Tokens of [{keyword, <<"with"/utf8>>} | Rest] -> skip_cte_body(Rest); _ -> Tokens end. -file("src/sqlode/query_analyzer/token_utils.gleam", 716). -spec extract_alias_from_item(list(sqlode@lexer:token())) -> gleam@option:option(binary()). extract_alias_from_item(Tokens) -> case lists:reverse(Tokens) of [{ident, Name}, {keyword, <<"as"/utf8>>} | _] -> {some, sqlode@naming:normalize_identifier(Name)}; [{quoted_ident, Name@1}, {keyword, <<"as"/utf8>>} | _] -> {some, sqlode@naming:normalize_identifier(Name@1)}; _ -> none end. -file("src/sqlode/query_analyzer/token_utils.gleam", 701). -spec parse_select_items(list(sqlode@lexer:token())) -> list(sqlode@query_ir:select_item()). parse_select_items(Tokens) -> Groups = split_on_commas(Tokens), gleam@list:map(Groups, fun(Group) -> case Group of [{operator, <<"*"/utf8>>}] -> {star_item, none}; [{ident, T}, dot, {operator, <<"*"/utf8>>}] -> {star_item, {some, string:lowercase(T)}}; _ -> Alias = extract_alias_from_item(Group), {expression_item, Group, Alias} end end). -file("src/sqlode/query_analyzer/token_utils.gleam", 745). -spec parse_from_items(list(sqlode@lexer:token())) -> list(sqlode@query_ir:from_item()). parse_from_items(Tokens) -> Groups = split_on_commas(Tokens), gleam@list:filter_map(Groups, fun(Group) -> case Group of [{ident, Name}] -> {ok, {table_ref, string:lowercase(Name), none}}; [{quoted_ident, Name@1}] -> {ok, {table_ref, string:lowercase(Name@1), none}}; [{ident, _}, dot, {ident, Name@2}] -> {ok, {table_ref, string:lowercase(Name@2), none}}; [{ident, Name@3}, {keyword, <<"as"/utf8>>}, {ident, A}] -> {ok, {table_ref, string:lowercase(Name@3), {some, string:lowercase(A)}}}; [{ident, Name@4}, {ident, A@1}] -> {ok, {table_ref, string:lowercase(Name@4), {some, string:lowercase(A@1)}}}; _ -> {error, nil} end end). -file("src/sqlode/query_analyzer/token_utils.gleam", 814). -spec skip_join_keywords(list(sqlode@lexer:token())) -> list(sqlode@lexer:token()). skip_join_keywords(Tokens) -> case Tokens of [{keyword, Kw} | Rest] when ((((((Kw =:= <<"outer"/utf8>>) orelse (Kw =:= <<"inner"/utf8>>)) orelse (Kw =:= <<"cross"/utf8>>)) orelse (Kw =:= <<"natural"/utf8>>)) orelse (Kw =:= <<"left"/utf8>>)) orelse (Kw =:= <<"right"/utf8>>)) orelse (Kw =:= <<"full"/utf8>>) -> skip_join_keywords(Rest); _ -> Tokens end. -file("src/sqlode/query_analyzer/token_utils.gleam", 854). -spec read_optional_alias(list(sqlode@lexer:token())) -> {gleam@option:option(binary()), list(sqlode@lexer:token())}. read_optional_alias(Tokens) -> case Tokens of [{keyword, <<"as"/utf8>>}, {ident, A} | Rest] -> {{some, string:lowercase(A)}, Rest}; [{keyword, <<"as"/utf8>>}, {quoted_ident, A@1} | Rest@1] -> {{some, string:lowercase(A@1)}, Rest@1}; [{ident, A@2} | Rest@2] when ((((((((A@2 =/= <<"on"/utf8>>) andalso (A@2 =/= <<"where"/utf8>>)) andalso (A@2 =/= <<"group"/utf8>>)) andalso (A@2 =/= <<"order"/utf8>>)) andalso (A@2 =/= <<"limit"/utf8>>)) andalso (A@2 =/= <<"join"/utf8>>)) andalso (A@2 =/= <<"left"/utf8>>)) andalso (A@2 =/= <<"right"/utf8>>)) andalso (A@2 =/= <<"inner"/utf8>>) -> {{some, string:lowercase(A@2)}, Rest@2}; _ -> {none, Tokens} end. -file("src/sqlode/query_analyzer/token_utils.gleam", 964). -spec collect_until_kw_loop( list(sqlode@lexer:token()), list(binary()), integer(), list(sqlode@lexer:token()) ) -> {list(sqlode@lexer:token()), list(sqlode@lexer:token())}. collect_until_kw_loop(Tokens, Stop_keywords, Depth, Acc) -> case Tokens of [] -> {lists:reverse(Acc), []}; [{keyword, Kw} | _] when Depth =:= 0 -> case gleam@list:contains(Stop_keywords, Kw) of true -> {lists:reverse(Acc), Tokens}; false -> case Tokens of [T | Rest] -> collect_until_kw_loop( Rest, Stop_keywords, Depth, [T | Acc] ); _ -> {lists:reverse(Acc), []} end end; [l_paren | Rest@1] -> collect_until_kw_loop( Rest@1, Stop_keywords, Depth + 1, [l_paren | Acc] ); [r_paren | Rest@2] -> collect_until_kw_loop( Rest@2, Stop_keywords, Depth - 1, [r_paren | Acc] ); [T@1 | Rest@3] -> collect_until_kw_loop(Rest@3, Stop_keywords, Depth, [T@1 | Acc]) end. -file("src/sqlode/query_analyzer/token_utils.gleam", 957). ?DOC(" Collect tokens until one of the stop keywords is found at depth 0.\n"). -spec collect_until_keyword(list(sqlode@lexer:token()), list(binary())) -> {list(sqlode@lexer:token()), list(sqlode@lexer:token())}. collect_until_keyword(Tokens, Stop_keywords) -> collect_until_kw_loop(Tokens, Stop_keywords, 0, []). -file("src/sqlode/query_analyzer/token_utils.gleam", 829). -spec parse_single_join(list(sqlode@lexer:token())) -> {gleam@option:option(sqlode@query_ir:join_clause()), list(sqlode@lexer:token())}. parse_single_join(Tokens) -> {Table_name_opt, Rest} = read_table_name(Tokens), case Table_name_opt of none -> {none, Rest}; {some, Name} -> {Alias, Rest2} = read_optional_alias(Rest), {On_tokens, Rest3} = case Rest2 of [{keyword, <<"on"/utf8>>} | After_on] -> {On_toks, After} = collect_until_keyword( After_on, [<<"join"/utf8>>, <<"left"/utf8>>, <<"right"/utf8>>, <<"inner"/utf8>>, <<"outer"/utf8>>, <<"cross"/utf8>>, <<"full"/utf8>>, <<"natural"/utf8>>, <<"where"/utf8>>, <<"group"/utf8>>, <<"having"/utf8>>, <<"order"/utf8>>, <<"limit"/utf8>>, <<"union"/utf8>>, <<"intersect"/utf8>>, <<"except"/utf8>>] ), {{some, On_toks}, After}; _ -> {none, Rest2} end, {{some, {join_clause, Name, Alias, On_tokens}}, Rest3} end. -file("src/sqlode/query_analyzer/token_utils.gleam", 776). -spec parse_joins_loop( list(sqlode@lexer:token()), list(sqlode@query_ir:join_clause()) ) -> {list(sqlode@query_ir:join_clause()), list(sqlode@lexer:token())}. parse_joins_loop(Tokens, Acc) -> case Tokens of [{keyword, Kw} | Rest] when (((((((Kw =:= <<"join"/utf8>>) orelse (Kw =:= <<"left"/utf8>>)) orelse (Kw =:= <<"right"/utf8>>)) orelse (Kw =:= <<"inner"/utf8>>)) orelse (Kw =:= <<"outer"/utf8>>)) orelse (Kw =:= <<"cross"/utf8>>)) orelse (Kw =:= <<"full"/utf8>>)) orelse (Kw =:= <<"natural"/utf8>>) -> After_join_kw = skip_join_keywords(Rest), case After_join_kw of [{keyword, <<"join"/utf8>>} | After_join] -> {Clause, Remaining} = parse_single_join(After_join), case Clause of {some, J} -> parse_joins_loop(Remaining, [J | Acc]); none -> parse_joins_loop(Remaining, Acc) end; _ -> {Clause@1, Remaining@1} = parse_single_join(After_join_kw), case Clause@1 of {some, J@1} -> parse_joins_loop(Remaining@1, [J@1 | Acc]); none -> parse_joins_loop(Remaining@1, Acc) end end; _ -> {lists:reverse(Acc), Tokens} end. -file("src/sqlode/query_analyzer/token_utils.gleam", 770). -spec parse_join_clauses(list(sqlode@lexer:token())) -> {list(sqlode@query_ir:join_clause()), list(sqlode@lexer:token())}. parse_join_clauses(Tokens) -> parse_joins_loop(Tokens, []). -file("src/sqlode/query_analyzer/token_utils.gleam", 726). -spec parse_from_clause(list(sqlode@lexer:token())) -> {list(sqlode@query_ir:from_item()), list(sqlode@query_ir:join_clause()), list(sqlode@lexer:token())}. parse_from_clause(Tokens) -> case Tokens of [{keyword, <<"from"/utf8>>} | Rest] -> {From_tokens, After_from} = collect_until_keyword( Rest, [<<"where"/utf8>>, <<"group"/utf8>>, <<"having"/utf8>>, <<"order"/utf8>>, <<"limit"/utf8>>, <<"union"/utf8>>, <<"intersect"/utf8>>, <<"except"/utf8>>, <<"join"/utf8>>, <<"left"/utf8>>, <<"right"/utf8>>, <<"inner"/utf8>>, <<"outer"/utf8>>, <<"cross"/utf8>>, <<"full"/utf8>>, <<"natural"/utf8>>, <<"lateral"/utf8>>] ), From_items = parse_from_items(From_tokens), {Joins, After_joins} = parse_join_clauses(After_from), {From_items, Joins, After_joins}; _ -> {[], [], Tokens} end. -file("src/sqlode/query_analyzer/token_utils.gleam", 997). ?DOC(" Extract a clause that starts with `keyword` and ends before any of `stop_keywords`.\n"). -spec extract_clause(list(sqlode@lexer:token()), binary(), list(binary())) -> {gleam@option:option(list(sqlode@lexer:token())), list(sqlode@lexer:token())}. extract_clause(Tokens, Keyword, Stop_keywords) -> case Tokens of [{keyword, Kw} | Rest] when Kw =:= Keyword -> After_kw = case {Kw, Rest} of {<<"group"/utf8>>, [{keyword, <<"by"/utf8>>} | R]} -> R; {<<"order"/utf8>>, [{keyword, <<"by"/utf8>>} | R@1]} -> R@1; {_, _} -> Rest end, {Clause_tokens, Remaining} = collect_until_keyword( After_kw, Stop_keywords ), case Clause_tokens of [] -> {none, Remaining}; _ -> {{some, Clause_tokens}, Remaining} end; _ -> {none, Tokens} end. -file("src/sqlode/query_analyzer/token_utils.gleam", 653). -spec structure_select(list(sqlode@lexer:token())) -> sqlode@query_ir:sql_statement(). structure_select(Tokens) -> After_select = case Tokens of [{keyword, <<"select"/utf8>>}, {keyword, <<"distinct"/utf8>>} | Rest] -> Rest; [{keyword, <<"select"/utf8>>} | Rest@1] -> Rest@1; _ -> Tokens end, {Select_tokens, Rest_after_select} = collect_until_keyword( After_select, [<<"from"/utf8>>, <<"where"/utf8>>, <<"group"/utf8>>, <<"having"/utf8>>, <<"order"/utf8>>, <<"limit"/utf8>>, <<"union"/utf8>>, <<"intersect"/utf8>>, <<"except"/utf8>>] ), Select_items = parse_select_items(Select_tokens), {From_items, Joins, Rest_after_from} = parse_from_clause(Rest_after_select), {Where_tokens, Rest_after_where} = extract_clause( Rest_after_from, <<"where"/utf8>>, [<<"group"/utf8>>, <<"having"/utf8>>, <<"order"/utf8>>, <<"limit"/utf8>>, <<"union"/utf8>>, <<"intersect"/utf8>>, <<"except"/utf8>>] ), {Group_by_tokens, Rest_after_group} = extract_clause( Rest_after_where, <<"group"/utf8>>, [<<"having"/utf8>>, <<"order"/utf8>>, <<"limit"/utf8>>, <<"union"/utf8>>, <<"intersect"/utf8>>, <<"except"/utf8>>] ), {Having_tokens, Rest_after_having} = extract_clause( Rest_after_group, <<"having"/utf8>>, [<<"order"/utf8>>, <<"limit"/utf8>>, <<"union"/utf8>>, <<"intersect"/utf8>>, <<"except"/utf8>>] ), {Order_by_tokens, Rest_after_order} = extract_clause( Rest_after_having, <<"order"/utf8>>, [<<"limit"/utf8>>, <<"union"/utf8>>, <<"intersect"/utf8>>, <<"except"/utf8>>] ), {Limit_tokens, _} = extract_clause( Rest_after_order, <<"limit"/utf8>>, [<<"union"/utf8>>, <<"intersect"/utf8>>, <<"except"/utf8>>] ), {select_statement, Select_items, From_items, Joins, Where_tokens, Group_by_tokens, Having_tokens, Order_by_tokens, Limit_tokens}. -file("src/sqlode/query_analyzer/token_utils.gleam", 1021). -spec skip_to_keyword(list(sqlode@lexer:token()), binary()) -> gleam@option:option(list(sqlode@lexer:token())). skip_to_keyword(Tokens, Keyword) -> case Tokens of [] -> none; [{keyword, Kw} | Rest] when Kw =:= Keyword -> {some, Rest}; [_ | Rest@1] -> skip_to_keyword(Rest@1, Keyword) end. -file("src/sqlode/query_analyzer/token_utils.gleam", 1032). -spec extract_returning_tokens(list(sqlode@lexer:token())) -> gleam@option:option(list(sqlode@lexer:token())). extract_returning_tokens(Tokens) -> case skip_to_keyword(Tokens, <<"returning"/utf8>>) of {some, After_returning} -> case After_returning of [] -> none; Toks -> {some, Toks} end; none -> none end. -file("src/sqlode/query_analyzer/token_utils.gleam", 883). -spec structure_insert(list(sqlode@lexer:token())) -> sqlode@query_ir:sql_statement(). structure_insert(Tokens) -> case find_insert_parts(Tokens) of {some, Parts} -> Returning = extract_returning_tokens(Tokens), {insert_statement, erlang:element(2, Parts), erlang:element(3, Parts), erlang:element(4, Parts), Returning}; none -> {unstructured_statement, Tokens} end. -file("src/sqlode/query_analyzer/token_utils.gleam", 900). -spec structure_update(list(sqlode@lexer:token())) -> sqlode@query_ir:sql_statement(). structure_update(Tokens) -> case Tokens of [{keyword, <<"update"/utf8>>} | Rest] -> {Table_name_opt, After_table} = read_table_name(Rest), case Table_name_opt of none -> {unstructured_statement, Tokens}; {some, Name} -> {Set_tokens, After_set} = case skip_to_keyword( After_table, <<"set"/utf8>> ) of {some, After_set_kw} -> collect_until_keyword( After_set_kw, [<<"where"/utf8>>, <<"returning"/utf8>>, <<"from"/utf8>>] ); none -> {[], After_table} end, {Where_tokens, _} = extract_clause( After_set, <<"where"/utf8>>, [<<"returning"/utf8>>] ), Returning = extract_returning_tokens(Tokens), {update_statement, Name, Set_tokens, Where_tokens, Returning} end; _ -> {unstructured_statement, Tokens} end. -file("src/sqlode/query_analyzer/token_utils.gleam", 932). -spec structure_delete(list(sqlode@lexer:token())) -> sqlode@query_ir:sql_statement(). structure_delete(Tokens) -> case Tokens of [{keyword, <<"delete"/utf8>>}, {keyword, <<"from"/utf8>>} | Rest] -> {Table_name_opt, After_table} = read_table_name(Rest), case Table_name_opt of none -> {unstructured_statement, Tokens}; {some, Name} -> {Where_tokens, _} = extract_clause( After_table, <<"where"/utf8>>, [<<"returning"/utf8>>] ), Returning = extract_returning_tokens(Tokens), {delete_statement, Name, Where_tokens, Returning} end; _ -> {unstructured_statement, Tokens} end. -file("src/sqlode/query_analyzer/token_utils.gleam", 617). ?DOC( " Build a `SqlStatement` from a token list. This function identifies the\n" " statement kind and decomposes it into its major clauses. Sub-expressions\n" " (WHERE predicates, etc.) remain as raw token lists — this is\n" " intentionally a *thin* IR that avoids building a full expression AST.\n" ). -spec structure_tokens(list(sqlode@lexer:token())) -> sqlode@query_ir:sql_statement(). structure_tokens(Tokens) -> Stripped = strip_leading_cte(Tokens), case Stripped of [{keyword, <<"select"/utf8>>} | _] -> structure_select(Stripped); [{keyword, <<"insert"/utf8>>} | _] -> structure_insert(Stripped); [{keyword, <<"update"/utf8>>} | _] -> structure_update(Stripped); [{keyword, <<"delete"/utf8>>} | _] -> structure_delete(Stripped); _ -> {unstructured_statement, Stripped} end.