Current section
Files
Jump to
Current section
Files
src/sqlode@query_analyzer@param_inferencer.erl
-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, extract_int_context_params/2, 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 <op> Param` or the reversed operand\n"
" form `Param <op> 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 <op> ANY|ALL|SOME(<placeholder>)`. 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 <op> 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", 948).
?DOC(
" Walk the token stream once, counting placeholders to assign each a\n"
" 1-based occurrence index, and collect those that fall inside a\n"
" `LIMIT` / `OFFSET` clause. `in_int_context` flips on at `LIMIT` /\n"
" `OFFSET` and flips off at any keyword that ends the integer\n"
" expression (`union`, `except`, `intersect`, `returning`, `for`).\n"
).
-spec scan_int_context_indices(
list(sqlode@lexer:token()),
sqlode@model:engine(),
integer(),
boolean(),
list(integer())
) -> list(integer()).
scan_int_context_indices(Tokens, Engine, Occurrence, In_int_context, Acc) ->
case Tokens of
[] ->
lists:reverse(Acc);
[{keyword, K} | Rest] ->
Lower = string:lowercase(K),
Next_state = case Lower of
<<"limit"/utf8>> ->
true;
<<"offset"/utf8>> ->
true;
<<"union"/utf8>> ->
false;
<<"except"/utf8>> ->
false;
<<"intersect"/utf8>> ->
false;
<<"returning"/utf8>> ->
false;
<<"for"/utf8>> ->
false;
_ ->
In_int_context
end,
scan_int_context_indices(Rest, Engine, Occurrence, Next_state, Acc);
[{placeholder, Raw} | Rest@1] ->
Assigned = case Engine of
postgre_s_q_l ->
case begin
_pipe = Raw,
_pipe@1 = gleam@string:replace(
_pipe,
<<"$"/utf8>>,
<<""/utf8>>
),
gleam_stdlib:parse_int(_pipe@1)
end of
{ok, N} ->
N;
{error, _} ->
Occurrence
end;
_ ->
Occurrence
end,
Acc@1 = case In_int_context of
true ->
[Assigned | Acc];
false ->
Acc
end,
scan_int_context_indices(
Rest@1,
Engine,
Occurrence + 1,
In_int_context,
Acc@1
);
[_ | Rest@2] ->
scan_int_context_indices(
Rest@2,
Engine,
Occurrence,
In_int_context,
Acc
)
end.
-file("src/sqlode/query_analyzer/param_inferencer.gleam", 933).
?DOC(
" Extract placeholder type hints from SQL contexts that the spec\n"
" requires to be integer: the expressions following `LIMIT` and\n"
" `OFFSET`. Each placeholder reachable from those keywords (until a\n"
" terminating keyword or end of statement) is pinned to `IntType`.\n"
" Used as a complement to `extract_type_casts` so callers can write\n"
" `LIMIT sqlode.arg(lim)` / `OFFSET sqlode.arg(off)` without an\n"
" explicit cast on engines like SQLite that cannot infer the type\n"
" from a bare `?` (Issue #491).\n"
"\n"
" Implementation note: this works at the token level rather than the\n"
" IR, because the IR's `Param.index` is `0` for SQLite anonymous `?`\n"
" placeholders (`expr_parser.decode_placeholder` deliberately\n"
" returns `0` for them). The token walker assigns each placeholder\n"
" an occurrence index that matches what `placeholder.build`\n"
" computes, so the resulting dict keys line up with\n"
" `PlaceholderOccurrence.index` consumed by `build_params`.\n"
).
-spec extract_int_context_params(
list(sqlode@lexer:token()),
sqlode@model:engine()
) -> gleam@dict:dict(integer(), sqlode@model:scalar_type()).
extract_int_context_params(Tokens, Engine) ->
Int_indices = scan_int_context_indices(Tokens, Engine, 1, false, []),
gleam@list:fold(
Int_indices,
maps:new(),
fun(D, Idx) -> gleam@dict:insert(D, Idx, int_type) 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 <op> 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 <op> 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"
" (<list>)` 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 <op> placeholder` / `placeholder <op> 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.