-module(sqlode@query_analyzer). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/sqlode/query_analyzer.gleam"). -export([analysis_error_to_string/2, analyze_queries/4]). -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.gleam", 20). -spec analysis_error_to_string( sqlode@query_analyzer@context:analysis_error(), sqlode@model:engine() ) -> binary(). analysis_error_to_string(Error, Engine) -> sqlode@query_analyzer@context:analysis_error_to_string(Error, Engine). -file("src/sqlode/query_analyzer.gleam", 127). ?DOC( " Expand any `sqlode.embed(TABLE)` macro calls in the query's SQL into\n" " concrete column lists so the emitted runtime SQL is valid. The token\n" " list is the already-tokenized form of `query.sql`; reusing it avoids a\n" " redundant lex pass when no embed is present.\n" ). -spec rewrite_embed_sql( sqlode@model:engine(), sqlode@model:parsed_query(), list(sqlode@lexer:token()), list(sqlode@model:result_item()) ) -> sqlode@model:parsed_query(). rewrite_embed_sql(Engine, Query, Tokens, Result_columns) -> Rewritten_tokens = sqlode@query_analyzer@embed_rewriter:rewrite( Tokens, Result_columns ), case Rewritten_tokens =:= Tokens of true -> Query; false -> Rewritten_sql = sqlode@lexer:tokens_to_string( Rewritten_tokens, {token_render_options, false, true, {some, Engine}} ), {parsed_query, erlang:element(2, Query), erlang:element(3, Query), erlang:element(4, Query), Rewritten_sql, erlang:element(6, Query), erlang:element(7, Query), erlang:element(8, Query)} end. -file("src/sqlode/query_analyzer.gleam", 151). -spec merge_virtual_tables(sqlode@model:catalog(), list(sqlode@model:table())) -> sqlode@model:catalog(). merge_virtual_tables(Catalog, Vtables) -> case Vtables of [] -> Catalog; _ -> {catalog, lists:append(erlang:element(2, Catalog), Vtables), erlang:element(3, Catalog)} end. -file("src/sqlode/query_analyzer.gleam", 283). -spec macro_index(sqlode@model:macro()) -> integer(). macro_index(M) -> case M of {macro_arg, I, _} -> I; {macro_narg, I@1, _} -> I@1; {macro_slice, I@2, _} -> I@2 end. -file("src/sqlode/query_analyzer.gleam", 291). -spec build_macro_dict(list(sqlode@model:macro())) -> gleam@dict:dict(integer(), sqlode@model:macro()). build_macro_dict(Macros) -> gleam@list:fold( Macros, maps:new(), fun(D, M) -> gleam@dict:insert(D, macro_index(M), M) end ). -file("src/sqlode/query_analyzer.gleam", 295). -spec build_inference_dict(binary(), list({integer(), sqlode@model:column()})) -> {ok, gleam@dict:dict(integer(), sqlode@model:column())} | {error, sqlode@query_analyzer@context:analysis_error()}. build_inference_dict(Query_name, Inferences) -> gleam@list:try_fold( Inferences, maps:new(), fun(D, Entry) -> {Index, {column, _, New_type, _}} = Entry, case gleam_stdlib:map_get(D, Index) of {ok, {column, _, Existing_type, _}} -> case Existing_type =:= New_type of true -> {ok, D}; false -> {error, {parameter_type_conflict, Query_name, Index, Existing_type, New_type}} end; {error, _} -> {_, Column} = Entry, {ok, gleam@dict:insert(D, Index, Column)} end end ). -file("src/sqlode/query_analyzer.gleam", 165). -spec build_params( sqlode@query_analyzer@context:analyzer_context(), sqlode@model:engine(), sqlode@model:parsed_query(), list(sqlode@lexer:token()), sqlode@query_ir:sql_statement(), sqlode@model:catalog(), list(sqlode@query_analyzer@placeholder:placeholder_occurrence()) ) -> {ok, list(sqlode@model:query_param())} | {error, sqlode@query_analyzer@context:analysis_error()}. build_params(Ctx, Engine, Query, Tokens, Statement, Catalog, Occurrences) -> gleam@result:'try'( sqlode@query_analyzer@param_inferencer:infer_equality_params( Ctx, Engine, erlang:element(2, Query), Tokens, Catalog ), fun(Equality) -> gleam@result:'try'( sqlode@query_analyzer@param_inferencer:infer_in_params( Ctx, Engine, erlang:element(2, Query), Tokens, Catalog ), fun(In_params) -> Inferences = begin _pipe = sqlode@query_analyzer@param_inferencer:infer_insert_params_from_ir( Engine, Statement, Catalog ), _pipe@1 = lists:append(_pipe, Equality), lists:append(_pipe@1, In_params) end, gleam@result:'try'( begin _pipe@2 = sqlode@query_analyzer@param_inferencer:extract_type_casts( Ctx, Engine, Tokens ), gleam@result:map_error( _pipe@2, fun(Err) -> {Index, Cast_type} = Err, {unrecognized_cast_type, erlang:element(2, Query), Index, Cast_type} end ) end, fun(Cast_dict) -> Int_context_dict = sqlode@query_analyzer@param_inferencer:extract_int_context_params( Tokens, Engine ), Cast_dict@1 = maps:merge( Int_context_dict, Cast_dict ), Macro_dict = build_macro_dict( erlang:element(8, Query) ), gleam@result:'try'( build_inference_dict( erlang:element(2, Query), Inferences ), fun(Inference_dict) -> _pipe@3 = sqlode@query_analyzer@placeholder:unique( Occurrences ), gleam@list:try_map( _pipe@3, fun(Occurrence) -> Macro_info = begin _pipe@4 = gleam_stdlib:map_get( Macro_dict, erlang:element( 2, Occurrence ) ), gleam@option:from_result( _pipe@4 ) end, Inferred = begin _pipe@5 = gleam_stdlib:map_get( Inference_dict, erlang:element( 2, Occurrence ) ), gleam@option:from_result( _pipe@5 ) end, Cast_type@1 = begin _pipe@6 = gleam_stdlib:map_get( Cast_dict@1, erlang:element( 2, Occurrence ) ), gleam@option:from_result( _pipe@6 ) end, gleam@result:'try'(case Inferred of {some, Column} -> {ok, erlang:element( 3, Column )}; none -> case Cast_type@1 of {some, St} -> {ok, St}; none -> {error, {parameter_type_not_inferred, erlang:element( 2, Query ), erlang:element( 2, Occurrence )}} end end, fun(Inferred_type) -> {Field_name, Scalar_type, Nullable@1, Is_list} = case Macro_info of {some, {macro_arg, _, Name}} -> Nullable = case Inferred of {some, Column@1} -> erlang:element( 4, Column@1 ); none -> false end, {sqlode@naming:to_snake_case( erlang:element( 2, Ctx ), Name ), Inferred_type, Nullable, false}; {some, {macro_narg, _, Name@1}} -> {sqlode@naming:to_snake_case( erlang:element( 2, Ctx ), Name@1 ), Inferred_type, true, false}; {some, {macro_slice, _, Name@2}} -> {sqlode@naming:to_snake_case( erlang:element( 2, Ctx ), Name@2 ), Inferred_type, false, true}; none -> case Inferred of {some, Column@2} -> {sqlode@naming:to_snake_case( erlang:element( 2, Ctx ), erlang:element( 2, Column@2 ) ), erlang:element( 3, Column@2 ), erlang:element( 4, Column@2 ), false}; none -> {erlang:element( 4, Occurrence ), Inferred_type, false, false} end end, {ok, {query_param, erlang:element( 2, Occurrence ), Field_name, Scalar_type, Nullable@1, Is_list}} end) end ) end ) end ) end ) end ). -file("src/sqlode/query_analyzer.gleam", 37). -spec analyze_query( sqlode@query_analyzer@context:analyzer_context(), sqlode@model:engine(), sqlode@model:catalog(), sqlode@query_ir:tokenized_query() ) -> {ok, sqlode@model:analyzed_query()} | {error, sqlode@query_analyzer@context:analysis_error()}. analyze_query(Ctx, Engine, Catalog, Tq) -> {tokenized_query, Query, Tokens} = Tq, Statement = sqlode@query_analyzer@token_utils:structure_tokens(Tokens), Stmt = sqlode@query_analyzer@expr_parser:parse_stmt(Tokens, Engine), gleam@result:'try'(case Stmt of {unstructured_stmt, _, _} -> sqlode@query_analyzer@column_inferencer:extract_cte_tables( erlang:element(2, Query), Tokens, Catalog ); _ -> sqlode@query_analyzer@column_inferencer:extract_cte_tables_from_stmt( erlang:element(2, Query), Stmt, Tokens, Catalog ) end, fun(Cte_tables) -> With_ctes = merge_virtual_tables(Catalog, Cte_tables), Values_tables = case Stmt of {unstructured_stmt, _, _} -> sqlode@query_analyzer@column_inferencer:extract_values_tables( Tokens ); _ -> sqlode@query_analyzer@column_inferencer:extract_values_tables_from_stmt( Stmt ) end, With_values = merge_virtual_tables(With_ctes, Values_tables), gleam@result:'try'(case Stmt of {unstructured_stmt, _, _} -> sqlode@query_analyzer@column_inferencer:extract_derived_tables( erlang:element(2, Query), Tokens, With_values ); _ -> sqlode@query_analyzer@column_inferencer:extract_derived_tables_from_stmt( erlang:element(2, Query), Stmt, Tokens, With_values ) end, fun(Derived_tables) -> With_derived = merge_virtual_tables( With_values, Derived_tables ), Alias_tables = case Stmt of {unstructured_stmt, _, _} -> sqlode@query_analyzer@column_inferencer:extract_table_aliases( Tokens, With_derived ); _ -> sqlode@query_analyzer@column_inferencer:extract_table_aliases_from_stmt( Stmt, With_derived ) end, Augmented = merge_virtual_tables(With_derived, Alias_tables), Occurrences = sqlode@query_analyzer@placeholder:extract( Ctx, Engine, Tokens ), gleam@result:'try'( build_params( Ctx, Engine, Query, Tokens, Statement, Augmented, Occurrences ), fun(Params) -> gleam@result:'try'( sqlode@query_analyzer@column_inferencer:infer_result_columns( Ctx, Engine, Query, Tokens, Statement, Augmented ), fun(Result_columns) -> Rewritten = rewrite_embed_sql( Engine, Query, Tokens, Result_columns ), {ok, {analyzed_query, Rewritten, Params, Result_columns}} end ) end ) end) end). -file("src/sqlode/query_analyzer.gleam", 27). -spec analyze_queries( sqlode@model:engine(), sqlode@model:catalog(), sqlode@naming:naming_context(), list(sqlode@query_ir:tokenized_query()) ) -> {ok, list(sqlode@model:analyzed_query())} | {error, sqlode@query_analyzer@context:analysis_error()}. analyze_queries(Engine, Catalog, Naming_ctx, Queries) -> Ctx = sqlode@query_analyzer@context:new(Naming_ctx), gleam@list:try_map( Queries, fun(_capture) -> analyze_query(Ctx, Engine, Catalog, _capture) end ).