-module(sqlode@query_analyzer@param_inferencer). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/sqlode/query_analyzer/param_inferencer.gleam"). -export([infer_insert_params/4, infer_insert_params_from_ir/3, extract_type_casts/3, infer_in_params/5, infer_equality_params/5]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 66). -spec map_insert_columns( sqlode@model:engine(), sqlode@model:catalog(), binary(), list(binary()), list(list(sqlode@lexer:token())), integer(), gleam@dict:dict(binary(), integer()), list({integer(), sqlode@model:column()}) ) -> list({integer(), sqlode@model:column()}). map_insert_columns( Engine, Catalog, Table_name, Columns, Values, Occurrence, Seen, Acc ) -> case {Columns, Values} of {[], _} -> Acc; {_, []} -> Acc; {[Column_name | Rest_columns], [Value_tokens | Rest_values]} -> Value_placeholder = case Value_tokens of [{placeholder, P}] -> {some, P}; [{keyword, <<"cast"/utf8>>}, l_paren, {placeholder, P@1}, {keyword, <<"as"/utf8>>}, _, r_paren] -> {some, P@1}; _ -> none end, {Maybe_index, Next_occurrence, Updated_seen} = case Value_placeholder of {some, P@2} -> sqlode@query_analyzer@placeholder:resolve_index( Engine, P@2, Occurrence, Seen ); none -> {none, Occurrence, Seen} end, Acc@1 = case Maybe_index of {some, Index} -> case sqlode@query_analyzer@context:find_column( Catalog, Table_name, Column_name ) of {some, Column} -> [{Index, Column} | Acc]; none -> Acc end; none -> Acc end, map_insert_columns( Engine, Catalog, Table_name, Rest_columns, Rest_values, Next_occurrence, Updated_seen, Acc@1 ) end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 18). -spec infer_insert_params( sqlode@query_analyzer@context:analyzer_context(), sqlode@model:engine(), list(sqlode@lexer:token()), sqlode@model:catalog() ) -> list({integer(), sqlode@model:column()}). infer_insert_params(_, Engine, Tokens, Catalog) -> case sqlode@query_analyzer@token_utils:find_insert_parts(Tokens) of {some, Parts} -> _pipe = map_insert_columns( Engine, Catalog, erlang:element(2, Parts), erlang:element(3, Parts), erlang:element(4, Parts), 1, maps:new(), [] ), lists:reverse(_pipe); none -> [] end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 44). ?DOC( " Structured IR variant of `infer_insert_params`. Consumes the\n" " pre-parsed `InsertStatement` directly instead of re-scanning\n" " the token list.\n" ). -spec infer_insert_params_from_ir( sqlode@model:engine(), sqlode@query_ir:sql_statement(), sqlode@model:catalog() ) -> list({integer(), sqlode@model:column()}). infer_insert_params_from_ir(Engine, Statement, Catalog) -> case Statement of {insert_statement, Table_name, Columns, Value_groups, _} -> _pipe = map_insert_columns( Engine, Catalog, Table_name, Columns, Value_groups, 1, maps:new(), [] ), lists:reverse(_pipe); _ -> [] end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 236). -spec assignment_match(sqlode@query_ir:assignment()) -> {ok, sqlode@query_analyzer@token_utils:equality_match()} | {error, nil}. assignment_match(Assignment) -> case erlang:element(3, Assignment) of {param, _, Raw} -> {ok, {equality_match, sqlode@naming:normalize_identifier( erlang:element(2, Assignment) ), none, Raw}}; _ -> {error, nil} end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 417). -spec is_comparison_op(binary()) -> boolean(). is_comparison_op(Op) -> ((((((Op =:= <<"="/utf8>>) orelse (Op =:= <<"!="/utf8>>)) orelse (Op =:= <<"<>"/utf8>>)) orelse (Op =:= <<"<"/utf8>>)) orelse (Op =:= <<">"/utf8>>)) orelse (Op =:= <<"<="/utf8>>)) orelse (Op =:= <<">="/utf8>>). -file("src/sqlode/query_analyzer/param_inferencer.gleam", 457). -spec column_of(sqlode@query_ir:expr()) -> gleam@option:option({gleam@option:option(binary()), binary()}). column_of(Expr) -> case Expr of {column_ref, Table, Name} -> {some, {Table, Name}}; _ -> none end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 464). -spec param_of(sqlode@query_ir:expr()) -> gleam@option:option(binary()). param_of(Expr) -> case Expr of {param, _, Raw} -> {some, Raw}; {cast, Inner, _} -> param_of(Inner); _ -> none end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 484). -spec normalize_table_qualifier(gleam@option:option(binary())) -> gleam@option:option(binary()). normalize_table_qualifier(Table) -> case Table of {some, T} -> {some, string:lowercase(T)}; none -> none end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 472). -spec build_match(gleam@option:option(binary()), binary(), binary()) -> sqlode@query_analyzer@token_utils:equality_match(). build_match(Table, Name, Placeholder) -> {equality_match, sqlode@naming:normalize_identifier(Name), normalize_table_qualifier(Table), Placeholder}. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 433). ?DOC( " Emit a match for `ColumnRef Param` or the reversed operand\n" " form `Param ColumnRef`. The column operand is what we infer\n" " against either way — `col = ?` and `? = col` both constrain `col`.\n" " A PostgreSQL-style `$1::type` cast on the placeholder side is\n" " unwrapped so e.g. `score > $3::int` still binds $3 to `score`'s\n" " type (the cast is separately consumed by `extract_type_casts`).\n" ). -spec comparison_match(sqlode@query_ir:expr(), sqlode@query_ir:expr()) -> gleam@option:option(sqlode@query_analyzer@token_utils:equality_match()). comparison_match(Left, Right) -> case {column_of(Left), param_of(Right)} of {{some, {Table, Name}}, {some, Raw}} -> {some, build_match(Table, Name, Raw)}; {_, _} -> case {column_of(Right), param_of(Left)} of {{some, {Table@1, Name@1}}, {some, Raw@1}} -> {some, build_match(Table@1, Name@1, Raw@1)}; {_, _} -> none end end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 447). -spec like_match(sqlode@query_ir:expr(), sqlode@query_ir:expr()) -> gleam@option:option(sqlode@query_analyzer@token_utils:equality_match()). like_match(Subject, Pattern) -> case {column_of(Subject), param_of(Pattern)} of {{some, {Table, Name}}, {some, Raw}} -> {some, build_match(Table, Name, Raw)}; {_, _} -> none end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 705). ?DOC( " `col IN ($1)` with exactly one placeholder on the RHS. Anything\n" " else (empty list, multiple elements, non-placeholder elements)\n" " returns `None` so the caller descends into the sub-expressions\n" " instead of emitting a bogus single-param binding. `Cast(Param, _)`\n" " on the placeholder is transparently unwrapped.\n" ). -spec in_list_match(sqlode@query_ir:expr(), list(sqlode@query_ir:expr())) -> gleam@option:option(sqlode@query_analyzer@token_utils:equality_match()). in_list_match(Subject, Values) -> case Values of [Only] -> case {column_of(Subject), param_of(Only)} of {{some, {Table, Name}}, {some, Raw}} -> {some, build_match(Table, Name, Raw)}; {_, _} -> none end; _ -> none end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 737). -spec quantified_param_of(sqlode@query_ir:expr()) -> gleam@option:option(binary()). quantified_param_of(Expr) -> case Expr of {tuple, [Only]} -> param_of(Only); _ -> param_of(Expr) end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 726). ?DOC( " `col ANY|ALL|SOME()`. The token scanner matches\n" " on the literal `ANY ( placeholder )` paren shape, which the IR\n" " parses as either `Quantified(right: Param)` or — when the\n" " placeholder is itself parenthesised inside the ANY expression —\n" " `Quantified(right: Tuple([Param]))`. Both are accepted so the\n" " walker is shape-compatible with `find_quantified_patterns`.\n" " `Cast(Param)` on the placeholder side is transparently unwrapped.\n" ). -spec quantified_match( sqlode@query_ir:expr(), sqlode@query_ir:quantifier(), sqlode@query_ir:expr() ) -> gleam@option:option(sqlode@query_analyzer@token_utils:equality_match()). quantified_match(Left, _, Right) -> case {column_of(Left), quantified_param_of(Right)} of {{some, {Table, Name}}, {some, Raw}} -> {some, build_match(Table, Name, Raw)}; {_, _} -> none end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 755). ?DOC( " Walk each `column placeholder` / `column IN (placeholder)` /\n" " quantified pattern the token scanners found and bind a parameter\n" " type when the referenced column exists. Ambiguity (the column name\n" " exists in more than one in-scope table and is not qualified) is\n" " surfaced as `AmbiguousColumnName` so `sqlode generate` fails before\n" " emitting a wrong `Params` type — the result-column inferencer has\n" " raised the same diagnostic for select-list ambiguity; parameter\n" " inference now behaves symmetrically. A qualified column that can't\n" " be found, and an unqualified column not present in any visible\n" " table, simply skip inference for that placeholder — the outer\n" " analyzer still has type-cast / macro hooks to satisfy the param.\n" ). -spec scan_token_matches( sqlode@model:engine(), sqlode@model:catalog(), binary(), list(binary()), list(sqlode@query_analyzer@token_utils:equality_match()), integer(), gleam@dict:dict(binary(), integer()), list({integer(), sqlode@model:column()}) ) -> {ok, list({integer(), sqlode@model:column()})} | {error, sqlode@query_analyzer@context:analysis_error()}. scan_token_matches( Engine, Catalog, Query_name, All_tables, Matches, Occurrence, Seen, Acc ) -> case Matches of [] -> {ok, Acc}; [Match | Rest] -> {Maybe_index, Next_occurrence, Updated_seen} = sqlode@query_analyzer@placeholder:resolve_index( Engine, erlang:element(4, Match), Occurrence, Seen ), case Maybe_index of none -> scan_token_matches( Engine, Catalog, Query_name, All_tables, Rest, Next_occurrence, Updated_seen, Acc ); {some, Index} -> Lookup = case erlang:element(3, Match) of {some, Table} -> {ok, begin _pipe = sqlode@query_analyzer@context:find_column( Catalog, Table, erlang:element(2, Match) ), gleam@option:map( _pipe, fun(Col) -> {Table, Col} end ) end}; none -> sqlode@query_analyzer@context:find_column_in_tables( Catalog, All_tables, erlang:element(2, Match) ) end, case Lookup of {error, Matching_tables} -> {error, {ambiguous_column_name, Query_name, erlang:element(2, Match), Matching_tables}}; {ok, {some, {_, Column}}} -> scan_token_matches( Engine, Catalog, Query_name, All_tables, Rest, Next_occurrence, Updated_seen, [{Index, Column} | Acc] ); {ok, none} -> scan_token_matches( Engine, Catalog, Query_name, All_tables, Rest, Next_occurrence, Updated_seen, Acc ) end end end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 909). -spec cast_type_to_scalar(binary()) -> {ok, sqlode@model:scalar_type()} | {error, nil}. cast_type_to_scalar(Type_name) -> sqlode@model:parse_sql_type(gleam@string:trim(Type_name)). -file("src/sqlode/query_analyzer/param_inferencer.gleam", 882). -spec extract_type_casts( sqlode@query_analyzer@context:analyzer_context(), sqlode@model:engine(), list(sqlode@lexer:token()) ) -> {ok, gleam@dict:dict(integer(), sqlode@model:scalar_type())} | {error, {integer(), binary()}}. extract_type_casts(_, Engine, Tokens) -> case Engine of postgre_s_q_l -> Casts = sqlode@query_analyzer@token_utils:find_type_casts(Tokens), gleam@list:try_fold( Casts, maps:new(), fun(D, Cast) -> case begin _pipe = erlang:element(2, Cast), _pipe@1 = gleam@string:replace( _pipe, <<"$"/utf8>>, <<""/utf8>> ), gleam_stdlib:parse_int(_pipe@1) end of {ok, Index} -> case cast_type_to_scalar(erlang:element(3, Cast)) of {ok, Scalar_type} -> {ok, gleam@dict:insert(D, Index, Scalar_type)}; {error, nil} -> {error, {Index, gleam@string:trim( erlang:element(3, Cast) )}} end; {error, _} -> {ok, D} end end ); _ -> {ok, maps:new()} end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 560). -spec walk_set_op_iq(gleam@option:option(sqlode@query_ir:set_op())) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_set_op_iq(Set_op) -> case Set_op of {some, {set_op, _, _, Right}} -> walk_select_core_iq(Right); none -> [] end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 534). -spec walk_select_core_iq(sqlode@query_ir:select_core()) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_select_core_iq(Core) -> lists:append( [gleam@list:flat_map(erlang:element(3, Core), fun walk_select_item_iq/1), gleam@list:flat_map( erlang:element(4, Core), fun walk_from_item_joins_iq/1 ), walk_optional_expr_iq(erlang:element(5, Core)), gleam@list:flat_map(erlang:element(6, Core), fun walk_expr_iq/1), walk_optional_expr_iq(erlang:element(7, Core)), gleam@list:flat_map( erlang:element(8, Core), fun walk_order_key_iq/1 ), walk_optional_expr_iq(erlang:element(9, Core)), walk_optional_expr_iq(erlang:element(10, Core)), walk_set_op_iq(erlang:element(11, Core))] ). -file("src/sqlode/query_analyzer/param_inferencer.gleam", 556). -spec walk_order_key_iq(sqlode@query_ir:order_key()) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_order_key_iq(Key) -> walk_expr_iq(erlang:element(2, Key)). -file("src/sqlode/query_analyzer/param_inferencer.gleam", 623). ?DOC( " Walk an expression in left-to-right source order, emitting an\n" " `EqualityMatch` for every `col IN (placeholder)` / `col ANY|ALL|SOME\n" " (placeholder)` predicate. Nested subqueries (`EXISTS`, scalar,\n" " `IN (SELECT …)`) are traversed too so placeholders buried inside\n" " them still surface, matching the token path which scans the full\n" " main body after `strip_leading_with`.\n" ). -spec walk_expr_iq(sqlode@query_ir:expr()) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_expr_iq(Expr) -> case Expr of {in_expr, Subject, {in_list, Values}, _} -> case in_list_match(Subject, Values) of {some, M} -> [M]; none -> lists:append( walk_expr_iq(Subject), gleam@list:flat_map(Values, fun walk_expr_iq/1) ) end; {in_expr, Subject@1, Source, _} -> lists:append(walk_expr_iq(Subject@1), walk_in_source_iq(Source)); {quantified, _, Left, Quantifier, Right} -> case quantified_match(Left, Quantifier, Right) of {some, M@1} -> [M@1]; none -> lists:append(walk_expr_iq(Left), walk_expr_iq(Right)) end; {binary, _, Left@1, Right@1} -> lists:append(walk_expr_iq(Left@1), walk_expr_iq(Right@1)); {like_expr, Subject@2, _, Pattern, _, _} -> lists:append(walk_expr_iq(Subject@2), walk_expr_iq(Pattern)); {unary, _, Arg} -> walk_expr_iq(Arg); {between, Subject@3, Low, High, _} -> lists:append( [walk_expr_iq(Subject@3), walk_expr_iq(Low), walk_expr_iq(High)] ); {is_check, Subject@4, _, _} -> walk_expr_iq(Subject@4); {'case', Scrutinee, Branches, Else_} -> lists:append( [walk_optional_expr_iq(Scrutinee), gleam@list:flat_map( Branches, fun(B) -> lists:append( walk_expr_iq(erlang:element(2, B)), walk_expr_iq(erlang:element(3, B)) ) end ), walk_optional_expr_iq(Else_)] ); {cast, Subject@5, _} -> walk_expr_iq(Subject@5); {func, _, Args, _, Filter, Over} -> lists:append( [gleam@list:flat_map( Args, fun(A) -> walk_expr_iq(erlang:element(2, A)) end ), walk_optional_expr_iq(Filter), case Over of {some, Spec} -> walk_window_spec_iq(Spec); none -> [] end] ); {tuple, Elements} -> gleam@list:flat_map(Elements, fun walk_expr_iq/1); {array_lit, Elements@1} -> gleam@list:flat_map(Elements@1, fun walk_expr_iq/1); {exists, Core, _} -> walk_select_core_iq(Core); {scalar_subquery, Core@1} -> walk_select_core_iq(Core@1); _ -> [] end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 569). -spec walk_window_spec_iq(sqlode@query_ir:window_spec()) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_window_spec_iq(Spec) -> lists:append( [gleam@list:flat_map(erlang:element(2, Spec), fun walk_expr_iq/1), gleam@list:flat_map( erlang:element(3, Spec), fun walk_order_key_iq/1 )] ). -file("src/sqlode/query_analyzer/param_inferencer.gleam", 578). -spec walk_select_item_iq(sqlode@query_ir:select_item_ex()) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_select_item_iq(Item) -> case Item of {expr_item, Expr, _} -> walk_expr_iq(Expr); {star_ex, _} -> [] end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 601). -spec walk_join_on_iq(sqlode@query_ir:join_on()) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_join_on_iq(On) -> case On of {join_on_expr, Expr} -> walk_expr_iq(Expr); _ -> [] end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 587). -spec walk_from_item_joins_iq(sqlode@query_ir:from_item_ex()) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_from_item_joins_iq(From) -> case From of {from_join, Left, Right, _, On, _} -> lists:append( [walk_from_item_joins_iq(Left), walk_from_item_joins_iq(Right), walk_join_on_iq(On)] ); _ -> [] end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 608). -spec walk_optional_expr_iq(gleam@option:option(sqlode@query_ir:expr())) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_optional_expr_iq(Expr) -> case Expr of {some, E} -> walk_expr_iq(E); none -> [] end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 513). ?DOC( " Walk a parsed `Stmt` in source order and emit an `EqualityMatch`\n" " for every `col IN (placeholder)` or `col ANY|ALL|SOME(placeholder)`\n" " predicate reachable from the outer statement.\n" "\n" " Matches are returned in source order (interleaved). The legacy\n" " token path used `list.append(find_in_patterns, find_quantified_patterns)`\n" " which concatenated all IN matches before all quantified matches;\n" " for `$N` placeholders the index comes from the raw text so the\n" " ordering has no observable effect, and for sequential placeholders\n" " the combined walker preserves source order just like the equality\n" " walker that already shipped.\n" "\n" " Only the single-placeholder shapes the token scanners matched are\n" " emitted: `col IN ($1)` (one element, no subquery) and\n" " `col = ANY($1)` / `= ALL` / `= SOME` (with or without an extra\n" " paren wrapping the placeholder). Multi-element `IN (a, b, c)` and\n" " `IN (SELECT …)` / `ANY (SELECT …)` are intentionally skipped —\n" " those aren't simple single-parameter bindings.\n" ). -spec find_in_quantified_matches_in_stmt(sqlode@query_ir:stmt()) -> list(sqlode@query_analyzer@token_utils:equality_match()). find_in_quantified_matches_in_stmt(Stmt) -> case Stmt of {select_stmt, _, Core} -> walk_select_core_iq(Core); {update_stmt, _, _, _, Assignments, _, Where_, _} -> lists:append( [gleam@list:flat_map( Assignments, fun(A) -> walk_expr_iq(erlang:element(3, A)) end ), walk_optional_expr_iq(Where_)] ); {delete_stmt, _, _, _, _, Where_@1, _} -> walk_optional_expr_iq(Where_@1); {insert_stmt, _, _, _, _, _, _} -> []; {unstructured_stmt, _, _} -> [] end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 688). -spec walk_in_source_iq(sqlode@query_ir:in_source()) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_in_source_iq(Source) -> case Source of {in_subquery, Core} -> walk_select_core_iq(Core); {in_list, Values} -> gleam@list:flat_map(Values, fun walk_expr_iq/1); {in_slice_macro, _} -> [] end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 833). -spec infer_in_params( sqlode@query_analyzer@context:analyzer_context(), sqlode@model:engine(), binary(), list(sqlode@lexer:token()), sqlode@model:catalog() ) -> {ok, list({integer(), sqlode@model:column()})} | {error, sqlode@query_analyzer@context:analysis_error()}. infer_in_params(_, Engine, Query_name, Tokens, Catalog) -> Main_tokens = sqlode@query_analyzer@token_utils:strip_leading_with(Tokens), All_tables = sqlode@query_analyzer@token_utils:extract_table_names( Main_tokens ), case All_tables of [] -> {ok, []}; _ -> Matches = case sqlode@query_analyzer@expr_parser:parse_stmt( Tokens, Engine ) of {unstructured_stmt, _, _} -> lists:append( sqlode@query_analyzer@token_utils:find_in_patterns( Main_tokens ), sqlode@query_analyzer@token_utils:find_quantified_patterns( Main_tokens ) ); {insert_stmt, _, _, _, _, _, _} -> lists:append( sqlode@query_analyzer@token_utils:find_in_patterns( Main_tokens ), sqlode@query_analyzer@token_utils:find_quantified_patterns( Main_tokens ) ); Stmt -> find_in_quantified_matches_in_stmt(Stmt) end, _pipe = scan_token_matches( Engine, Catalog, Query_name, All_tables, Matches, 1, maps:new(), [] ), gleam@result:map(_pipe, fun lists:reverse/1) end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 279). -spec walk_set_op(gleam@option:option(sqlode@query_ir:set_op())) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_set_op(Set_op) -> case Set_op of {some, {set_op, _, _, Right}} -> walk_select_core(Right); none -> [] end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 250). -spec walk_select_core(sqlode@query_ir:select_core()) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_select_core(Core) -> lists:append( [gleam@list:flat_map(erlang:element(3, Core), fun walk_select_item/1), gleam@list:flat_map( erlang:element(4, Core), fun walk_from_item_joins/1 ), walk_optional_expr(erlang:element(5, Core)), gleam@list:flat_map(erlang:element(6, Core), fun walk_expr/1), walk_optional_expr(erlang:element(7, Core)), gleam@list:flat_map(erlang:element(8, Core), fun walk_order_key/1), walk_optional_expr(erlang:element(9, Core)), walk_optional_expr(erlang:element(10, Core)), walk_set_op(erlang:element(11, Core))] ). -file("src/sqlode/query_analyzer/param_inferencer.gleam", 275). -spec walk_order_key(sqlode@query_ir:order_key()) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_order_key(Key) -> walk_expr(erlang:element(2, Key)). -file("src/sqlode/query_analyzer/param_inferencer.gleam", 347). ?DOC( " Walk an expression in left-to-right source order, emitting an\n" " `EqualityMatch` for every comparison/LIKE that binds a column to a\n" " placeholder. Nested subqueries (`EXISTS`, scalar, `IN (SELECT …)`)\n" " are traversed as well so the scoping matches the token-based path\n" " (which scans every token in the main body after `strip_leading_with`\n" " and therefore picked up placeholders inside subqueries). `IN\n" " ()` and quantified forms stay the responsibility of\n" " `infer_in_params`, which is out of scope for this pass.\n" ). -spec walk_expr(sqlode@query_ir:expr()) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_expr(Expr) -> case Expr of {binary, Op, Left, Right} -> case is_comparison_op(Op) of true -> case comparison_match(Left, Right) of {some, M} -> [M]; none -> lists:append(walk_expr(Left), walk_expr(Right)) end; false -> lists:append(walk_expr(Left), walk_expr(Right)) end; {like_expr, Subject, _, Pattern, _, _} -> case like_match(Subject, Pattern) of {some, M@1} -> [M@1]; none -> lists:append(walk_expr(Subject), walk_expr(Pattern)) end; {unary, _, Arg} -> walk_expr(Arg); {between, Subject@1, Low, High, _} -> lists:append( [walk_expr(Subject@1), walk_expr(Low), walk_expr(High)] ); {is_check, Subject@2, _, _} -> walk_expr(Subject@2); {'case', Scrutinee, Branches, Else_} -> lists:append( [walk_optional_expr(Scrutinee), gleam@list:flat_map( Branches, fun(B) -> lists:append( walk_expr(erlang:element(2, B)), walk_expr(erlang:element(3, B)) ) end ), walk_optional_expr(Else_)] ); {cast, Subject@3, _} -> walk_expr(Subject@3); {func, _, Args, _, Filter, Over} -> lists:append( [gleam@list:flat_map( Args, fun(A) -> walk_expr(erlang:element(2, A)) end ), walk_optional_expr(Filter), case Over of {some, Spec} -> walk_window_spec(Spec); none -> [] end] ); {tuple, Elements} -> gleam@list:flat_map(Elements, fun walk_expr/1); {array_lit, Elements@1} -> gleam@list:flat_map(Elements@1, fun walk_expr/1); {exists, Core, _} -> walk_select_core(Core); {scalar_subquery, Core@1} -> walk_select_core(Core@1); {in_expr, Subject@4, Source, _} -> lists:append(walk_expr(Subject@4), walk_in_source(Source)); {quantified, _, Left@1, _, Right@1} -> lists:append(walk_expr(Left@1), walk_expr(Right@1)); _ -> [] end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 288). -spec walk_window_spec(sqlode@query_ir:window_spec()) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_window_spec(Spec) -> lists:append( [gleam@list:flat_map(erlang:element(2, Spec), fun walk_expr/1), gleam@list:flat_map(erlang:element(3, Spec), fun walk_order_key/1)] ). -file("src/sqlode/query_analyzer/param_inferencer.gleam", 297). -spec walk_select_item(sqlode@query_ir:select_item_ex()) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_select_item(Item) -> case Item of {expr_item, Expr, _} -> walk_expr(Expr); {star_ex, _} -> [] end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 323). -spec walk_join_on(sqlode@query_ir:join_on()) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_join_on(On) -> case On of {join_on_expr, Expr} -> walk_expr(Expr); _ -> [] end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 306). -spec walk_from_item_joins(sqlode@query_ir:from_item_ex()) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_from_item_joins(From) -> case From of {from_join, Left, Right, _, On, _} -> lists:append( [walk_from_item_joins(Left), walk_from_item_joins(Right), walk_join_on(On)] ); _ -> [] end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 330). -spec walk_optional_expr(gleam@option:option(sqlode@query_ir:expr())) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_optional_expr(Expr) -> case Expr of {some, E} -> walk_expr(E); none -> [] end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 206). ?DOC( " Walk a parsed `Stmt` in source order and emit an `EqualityMatch`\n" " for each `col placeholder` / `placeholder col` /\n" " `col [I]LIKE placeholder` predicate reachable from the outer\n" " statement.\n" "\n" " Scoping mirrors the token-based path: it descends through SELECT\n" " items, JOIN ON clauses of the outermost FROM, outermost\n" " WHERE / GROUP BY / HAVING, and (for UPDATE/DELETE) the single\n" " WHERE. Nested subqueries inside `EXISTS` / scalar subqueries /\n" " `IN (SELECT …)` are traversed too, because the pre-refactor\n" " `token_utils.find_equality_patterns` saw every placeholder in the\n" " main body regardless of paren depth. CTE bodies are excluded\n" " because `infer_equality_params` already strips them via\n" " `token_utils.strip_leading_with` before the ambiguity check.\n" ). -spec find_equality_matches_in_stmt(sqlode@query_ir:stmt()) -> list(sqlode@query_analyzer@token_utils:equality_match()). find_equality_matches_in_stmt(Stmt) -> case Stmt of {select_stmt, _, Core} -> walk_select_core(Core); {update_stmt, _, _, _, Assignments, _, Where_, _} -> lists:append( [gleam@list:filter_map(Assignments, fun assignment_match/1), gleam@list:flat_map( Assignments, fun(A) -> walk_expr(erlang:element(3, A)) end ), walk_optional_expr(Where_)] ); {delete_stmt, _, _, _, _, Where_@1, _} -> walk_optional_expr(Where_@1); {insert_stmt, _, _, _, _, _, _} -> []; {unstructured_stmt, _, _} -> [] end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 132). -spec infer_equality_params( sqlode@query_analyzer@context:analyzer_context(), sqlode@model:engine(), binary(), list(sqlode@lexer:token()), sqlode@model:catalog() ) -> {ok, list({integer(), sqlode@model:column()})} | {error, sqlode@query_analyzer@context:analysis_error()}. infer_equality_params(_, Engine, Query_name, Tokens, Catalog) -> Main_tokens = sqlode@query_analyzer@token_utils:strip_leading_with(Tokens), All_tables = sqlode@query_analyzer@token_utils:extract_table_names( Main_tokens ), case All_tables of [] -> {ok, []}; _ -> Matches = case sqlode@query_analyzer@expr_parser:parse_stmt( Tokens, Engine ) of {unstructured_stmt, _, _} -> sqlode@query_analyzer@token_utils:find_equality_patterns( Main_tokens ); {insert_stmt, _, _, _, _, _, _} -> sqlode@query_analyzer@token_utils:find_equality_patterns( Main_tokens ); Stmt -> find_equality_matches_in_stmt(Stmt) end, _pipe = scan_token_matches( Engine, Catalog, Query_name, All_tables, Matches, 1, maps:new(), [] ), gleam@result:map(_pipe, fun lists:reverse/1) end. -file("src/sqlode/query_analyzer/param_inferencer.gleam", 409). -spec walk_in_source(sqlode@query_ir:in_source()) -> list(sqlode@query_analyzer@token_utils:equality_match()). walk_in_source(Source) -> case Source of {in_subquery, Core} -> walk_select_core(Core); {in_list, Values} -> gleam@list:flat_map(Values, fun walk_expr/1); {in_slice_macro, _} -> [] end.