-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, infer_equality_params/5, infer_in_params/5, extract_type_casts/3]). -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", 64). -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}; _ -> none end, {Maybe_index, Next_occurrence, Updated_seen} = case Value_placeholder of {some, P@1} -> sqlode@query_analyzer@placeholder:resolve_index( Engine, P@1, 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", 16). -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", 42). ?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", 156). ?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", 113). -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 = sqlode@query_analyzer@token_utils:find_equality_patterns( Main_tokens ), _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", 234). -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 = lists:append( sqlode@query_analyzer@token_utils:find_in_patterns(Main_tokens), sqlode@query_analyzer@token_utils:find_quantified_patterns( Main_tokens ) ), _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", 293). -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", 266). -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.