-module(sqlode@internal@query_analyzer@column_inferencer). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/sqlode/internal/query_analyzer/column_inferencer.gleam"). -export([extract_values_tables/1, extract_table_aliases/2, extract_values_tables_from_stmt/1, extract_table_aliases_from_stmt/2, extract_derived_tables/3, extract_cte_tables/3, extract_cte_tables_from_stmt/4, extract_derived_tables_from_stmt/4, infer_result_columns/6]). -export_type([extracted_column/0, function_class/0, top_level_pattern/0, case_collector_mode/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. ?MODULEDOC(false). -type extracted_column() :: {extracted_column, binary(), gleam@option:option(binary()), gleam@option:option(binary()), gleam@option:option(list(sqlode@internal@lexer:token()))}. -type function_class() :: fn_count | fn_aggregate_inner | fn_avg | fn_window_int | fn_window_float | fn_window_first_arg | fn_coalesce | fn_greatest_least | fn_string | fn_length | fn_math | fn_datetime | fn_nullif | fn_unknown. -type top_level_pattern() :: pat_bool | pat_concat | pat_arithmetic | pat_json | pat_json_text | pat_none. -type case_collector_mode() :: scanning | in_then | in_else. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 117). ?DOC(false). -spec has_compound_keyword_loop(list(sqlode@internal@lexer:token()), integer()) -> boolean(). has_compound_keyword_loop(Tokens, Depth) -> case Tokens of [] -> false; [l_paren | Rest] -> has_compound_keyword_loop(Rest, Depth + 1); [r_paren | Rest@1] -> has_compound_keyword_loop(Rest@1, case Depth > 0 of true -> Depth - 1; false -> 0 end); [{keyword, K} | Rest@2] when Depth =:= 0 -> case K of <<"union"/utf8>> -> true; <<"except"/utf8>> -> true; <<"intersect"/utf8>> -> true; _ -> has_compound_keyword_loop(Rest@2, 0) end; [_ | Rest@3] -> has_compound_keyword_loop(Rest@3, Depth) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 112). ?DOC(false). -spec has_compound_keyword(list(sqlode@internal@lexer:token())) -> boolean(). has_compound_keyword(Tokens) -> has_compound_keyword_loop(Tokens, 0). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 136). ?DOC(false). -spec has_embed_items(list(sqlode@internal@query_ir:select_item())) -> boolean(). has_embed_items(Items) -> gleam@list:any(Items, fun(Item) -> case Item of {expression_item, Expr_tokens, _} -> gleam@list:any(Expr_tokens, fun(Tok) -> case Tok of {ident, Name} -> string:lowercase(Name) =:= <<"embed"/utf8>>; _ -> false end end); _ -> false end end). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 152). ?DOC(false). -spec extract_ir_table_names( list(sqlode@internal@query_ir:from_item()), list(sqlode@internal@query_ir:join_clause()) ) -> list(binary()). extract_ir_table_names(From_items, Joins) -> From_names = gleam@list:filter_map(From_items, fun(Item) -> case Item of {table_ref, _, {some, A}} -> {ok, A}; {table_ref, Name, none} -> {ok, Name}; {subquery_ref, _, {some, A@1}} -> {ok, A@1}; {subquery_ref, _, none} -> {error, nil} end end), Join_names = gleam@list:map(Joins, fun(J) -> case erlang:element(3, J) of {some, A@2} -> A@2; none -> erlang:element(2, J) end end), lists:append(From_names, Join_names). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 251). ?DOC(false). -spec strip_trailing_alias(list(sqlode@internal@lexer:token())) -> list(sqlode@internal@lexer:token()). strip_trailing_alias(Tokens) -> case lists:reverse(Tokens) of [{ident, _}, {keyword, <<"as"/utf8>>} | Rest] -> lists:reverse(Rest); [{quoted_ident, _}, {keyword, <<"as"/utf8>>} | Rest@1] -> lists:reverse(Rest@1); _ -> Tokens end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 260). ?DOC(false). -spec derive_column_name_from_tokens(list(sqlode@internal@lexer:token())) -> binary(). derive_column_name_from_tokens(Tokens) -> case Tokens of [{ident, Name}] -> Name; [{quoted_ident, Name@1}] -> Name@1; [{ident, _}, dot, {ident, Col}] -> Col; [{ident, _}, dot, {quoted_ident, Col@1}] -> Col@1; _ -> _pipe = Tokens, _pipe@1 = gleam@list:filter_map(_pipe, fun(Tok) -> case Tok of {ident, N} -> {ok, N}; {quoted_ident, N@1} -> {ok, N@1}; _ -> {error, nil} end end), _pipe@2 = gleam@list:last(_pipe@1), gleam@result:unwrap(_pipe@2, <<"?column?"/utf8>>) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 282). ?DOC(false). -spec derive_source_table_from_tokens(list(sqlode@internal@lexer:token())) -> gleam@option:option(binary()). derive_source_table_from_tokens(Tokens) -> case Tokens of [{ident, Table}, dot, {ident, _}] -> {some, Table}; [{ident, Table@1}, dot, {quoted_ident, _}] -> {some, Table@1}; _ -> none end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 177). ?DOC(false). -spec ir_select_items_to_extracted( list(sqlode@internal@query_ir:select_item()), list(binary()) ) -> list(extracted_column()). ir_select_items_to_extracted(Items, _) -> gleam@list:flat_map(Items, fun(Item) -> case Item of {star_item, none} -> [{extracted_column, <<"*"/utf8>>, none, none, none}]; {star_item, {some, Prefix}} -> [{extracted_column, <>, {some, Prefix}, none, none}]; {expression_item, [star], _} -> [{extracted_column, <<"*"/utf8>>, none, none, none}]; {expression_item, [{ident, T}, dot, star], _} -> [{extracted_column, <>, {some, T}, none, none}]; {expression_item, Expr_tokens, {some, Alias}} -> Stripped = strip_trailing_alias(Expr_tokens), Source = derive_source_table_from_tokens(Stripped), [{extracted_column, Alias, Source, none, {some, Stripped}}]; {expression_item, Expr_tokens@1, none} -> Name = derive_column_name_from_tokens(Expr_tokens@1), Source@1 = derive_source_table_from_tokens(Expr_tokens@1), [{extracted_column, Name, Source@1, none, case Source@1 of {some, _} -> none; none -> {some, Expr_tokens@1} end}] end end). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 476). ?DOC(false). -spec effective_tables(list(binary()), list(binary())) -> list(binary()). effective_tables(Inner_tables, Outer_tables) -> case Inner_tables of [] -> Outer_tables; _ -> Inner_tables end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 567). ?DOC(false). -spec parse_cte_column_list(list(sqlode@internal@lexer:token())) -> list(binary()). parse_cte_column_list(Tokens) -> _pipe = sqlode@internal@query_analyzer@token_utils:split_on_commas(Tokens), gleam@list:filter_map(_pipe, fun(Group) -> case Group of [{ident, N}] -> {ok, string:lowercase(N)}; [{quoted_ident, N@1}] -> {ok, string:lowercase(N@1)}; _ -> {error, nil} end end). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 590). ?DOC(false). -spec rename_columns(list(sqlode@internal@model:column()), list(binary())) -> list(sqlode@internal@model:column()). rename_columns(Columns, Names) -> case {Columns, Names} of {[], _} -> []; {Cols, []} -> Cols; {[Col | Rest_cols], [Name | Rest_names]} -> [{column, Name, erlang:element(3, Col), erlang:element(4, Col)} | rename_columns(Rest_cols, Rest_names)] end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 580). ?DOC(false). -spec apply_explicit_column_names( list(sqlode@internal@model:column()), list(binary()) ) -> list(sqlode@internal@model:column()). apply_explicit_column_names(Columns, Names) -> case Names of [] -> Columns; _ -> rename_columns(Columns, Names) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 608). ?DOC(false). -spec augment_catalog_with( sqlode@internal@model:catalog(), list(sqlode@internal@model:table()) ) -> sqlode@internal@model:catalog(). augment_catalog_with(Catalog, Vtables) -> case Vtables of [] -> Catalog; _ -> {catalog, lists:append(erlang:element(2, Catalog), Vtables), erlang:element(3, Catalog)} end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 656). ?DOC(false). -spec parse_values_alias(list(sqlode@internal@lexer:token())) -> gleam@option:option({binary(), list(binary()), list(sqlode@internal@lexer:token())}). parse_values_alias(Tokens) -> After_as = case Tokens of [{keyword, <<"as"/utf8>>} | Rest] -> Rest; _ -> Tokens end, case After_as of [{ident, Alias}, l_paren | Rest_after_lp] -> {Col_tokens, After_cols} = sqlode@internal@query_analyzer@token_utils:collect_paren_contents( Rest_after_lp ), case parse_cte_column_list(Col_tokens) of [] -> none; Names -> {some, {string:lowercase(Alias), Names, After_cols}} end; [{quoted_ident, Alias@1}, l_paren | Rest_after_lp@1] -> {Col_tokens@1, After_cols@1} = sqlode@internal@query_analyzer@token_utils:collect_paren_contents( Rest_after_lp@1 ), case parse_cte_column_list(Col_tokens@1) of [] -> none; Names@1 -> {some, {string:lowercase(Alias@1), Names@1, After_cols@1}} end; _ -> none end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 715). ?DOC(false). -spec number_scalar_type(binary()) -> sqlode@internal@model:scalar_type(). number_scalar_type(N) -> case (gleam_stdlib:contains_string(N, <<"."/utf8>>) orelse gleam_stdlib:contains_string( N, <<"e"/utf8>> )) orelse gleam_stdlib:contains_string(N, <<"E"/utf8>>) of true -> float_type; false -> int_type end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 698). ?DOC(false). -spec infer_literal_type_with_nullability(list(sqlode@internal@lexer:token())) -> {ok, {sqlode@internal@model:scalar_type(), boolean()}} | {error, nil}. infer_literal_type_with_nullability(Tokens) -> case Tokens of [{number_lit, N}] -> {ok, {number_scalar_type(N), false}}; [{operator, <<"-"/utf8>>}, {number_lit, N@1}] -> {ok, {number_scalar_type(N@1), false}}; [{operator, <<"+"/utf8>>}, {number_lit, N@2}] -> {ok, {number_scalar_type(N@2), false}}; [{string_lit, _}] -> {ok, {string_type, false}}; [{keyword, <<"true"/utf8>>}] -> {ok, {bool_type, false}}; [{keyword, <<"false"/utf8>>}] -> {ok, {bool_type, false}}; [{keyword, <<"null"/utf8>>}] -> {ok, {string_type, true}}; _ -> {error, nil} end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 684). ?DOC(false). -spec infer_values_first_row_types(list(sqlode@internal@lexer:token())) -> {ok, list({sqlode@internal@model:scalar_type(), boolean()})} | {error, nil}. infer_values_first_row_types(Values_body) -> case Values_body of [{keyword, <<"values"/utf8>>}, l_paren | Rest_after_lp] -> {First_row, _} = sqlode@internal@query_analyzer@token_utils:collect_paren_contents( Rest_after_lp ), Items = sqlode@internal@query_analyzer@token_utils:split_on_commas( First_row ), gleam@list:try_map(Items, fun infer_literal_type_with_nullability/1); _ -> {error, nil} end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 726). ?DOC(false). -spec build_values_columns( list(binary()), list({sqlode@internal@model:scalar_type(), boolean()}) ) -> list(sqlode@internal@model:column()). build_values_columns(Names, Types) -> case {Names, Types} of {[], _} -> []; {_, []} -> []; {[Name | Rest_names], [{St, Nullable} | Rest_types]} -> [{column, Name, St, Nullable} | build_values_columns(Rest_names, Rest_types)] end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 630). ?DOC(false). -spec find_values_tables( list(sqlode@internal@lexer:token()), list(sqlode@internal@model:table()) ) -> list(sqlode@internal@model:table()). find_values_tables(Tokens, Acc) -> case Tokens of [] -> lists:reverse(Acc); [l_paren, {keyword, <<"values"/utf8>>} | Rest] -> {Values_body, After_rp} = sqlode@internal@query_analyzer@token_utils:collect_paren_contents( [{keyword, <<"values"/utf8>>} | Rest] ), case parse_values_alias(After_rp) of {some, {Alias, Col_names, Remaining}} -> case infer_values_first_row_types(Values_body) of {ok, Types} -> Columns = build_values_columns(Col_names, Types), Table = {table, Alias, Columns}, find_values_tables(Remaining, [Table | Acc]); {error, _} -> find_values_tables(Remaining, Acc) end; none -> find_values_tables(After_rp, Acc) end; [_ | Rest@1] -> find_values_tables(Rest@1, Acc) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 626). ?DOC(false). -spec extract_values_tables(list(sqlode@internal@lexer:token())) -> list(sqlode@internal@model:table()). extract_values_tables(Tokens) -> find_values_tables(Tokens, []). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 798). ?DOC(false). -spec read_table_with_alias(list(sqlode@internal@lexer:token())) -> {gleam@option:option({binary(), binary()}), list(sqlode@internal@lexer:token())}. read_table_with_alias(Tokens) -> case Tokens of [{ident, _}, dot, {ident, Name}, {keyword, <<"as"/utf8>>}, {ident, Alias} | Rest] -> {{some, {string:lowercase(Name), string:lowercase(Alias)}}, Rest}; [{ident, _}, dot, {ident, Name@1}, {ident, Alias@1} | Rest@1] when ((Alias@1 =/= <<"on"/utf8>>) andalso (Alias@1 =/= <<"where"/utf8>>)) andalso (Alias@1 =/= <<"using"/utf8>>) -> {{some, {string:lowercase(Name@1), string:lowercase(Alias@1)}}, Rest@1}; [{ident, Name@2}, {keyword, <<"as"/utf8>>}, {ident, Alias@2} | Rest@2] -> {{some, {string:lowercase(Name@2), string:lowercase(Alias@2)}}, Rest@2}; [{quoted_ident, Name@3}, {keyword, <<"as"/utf8>>}, {ident, Alias@3} | Rest@3] -> {{some, {string:lowercase(Name@3), string:lowercase(Alias@3)}}, Rest@3}; [{ident, Name@4}, {ident, Alias@4} | Rest@4] -> {{some, {string:lowercase(Name@4), string:lowercase(Alias@4)}}, Rest@4}; [{ident, Name@5} | Rest@5] -> {{some, {string:lowercase(Name@5), string:lowercase(Name@5)}}, Rest@5}; _ -> {none, Tokens} end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 756). ?DOC(false). -spec scan_aliases_loop( list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog(), list(sqlode@internal@model:table()) ) -> list(sqlode@internal@model:table()). scan_aliases_loop(Tokens, Catalog, Acc) -> case Tokens of [] -> Acc; [{keyword, Kw} | Rest] when (((Kw =:= <<"from"/utf8>>) orelse (Kw =:= <<"join"/utf8>>)) orelse (Kw =:= <<"update"/utf8>>)) orelse (Kw =:= <<"into"/utf8>>) -> {Alias_pair, Remaining} = read_table_with_alias(Rest), Next_acc = case Alias_pair of {some, {Table_name, Alias_name}} -> case Alias_name /= Table_name of true -> case gleam@list:find( erlang:element(2, Catalog), fun(T) -> erlang:element(2, T) =:= Table_name end ) of {ok, Found} -> case gleam@list:any( Acc, fun(T@1) -> erlang:element(2, T@1) =:= Alias_name end ) of true -> Acc; false -> case gleam@list:any( erlang:element(2, Catalog), fun(T@2) -> erlang:element(2, T@2) =:= Alias_name end ) of true -> Acc; false -> [{table, Alias_name, erlang:element( 3, Found )} | Acc] end end; {error, _} -> Acc end; false -> Acc end; none -> Acc end, scan_aliases_loop(Remaining, Catalog, Next_acc); [_ | Rest@1] -> scan_aliases_loop(Rest@1, Catalog, Acc) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 748). ?DOC(false). -spec extract_table_aliases( list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog() ) -> list(sqlode@internal@model:table()). extract_table_aliases(Tokens, Catalog) -> _pipe = scan_aliases_loop(Tokens, Catalog, []), lists:reverse(_pipe). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 914). ?DOC(false). -spec parse_derived_alias(list(sqlode@internal@lexer:token())) -> gleam@option:option({binary(), list(binary()), list(sqlode@internal@lexer:token())}). parse_derived_alias(Tokens) -> After_as = case Tokens of [{keyword, <<"as"/utf8>>} | Rest] -> Rest; _ -> Tokens end, case After_as of [{ident, Alias}, l_paren | Rest_after_lp] -> {Col_tokens, After_cols} = sqlode@internal@query_analyzer@token_utils:collect_paren_contents( Rest_after_lp ), Names = parse_cte_column_list(Col_tokens), {some, {string:lowercase(Alias), Names, After_cols}}; [{quoted_ident, Alias@1}, l_paren | Rest_after_lp@1] -> {Col_tokens@1, After_cols@1} = sqlode@internal@query_analyzer@token_utils:collect_paren_contents( Rest_after_lp@1 ), Names@1 = parse_cte_column_list(Col_tokens@1), {some, {string:lowercase(Alias@1), Names@1, After_cols@1}}; [{ident, Alias@2} | Rest@1] -> {some, {string:lowercase(Alias@2), [], Rest@1}}; [{quoted_ident, Alias@3} | Rest@2] -> {some, {string:lowercase(Alias@3), [], Rest@2}}; _ -> none end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 985). ?DOC(false). -spec stmt_ctes(sqlode@internal@query_ir:stmt()) -> list(sqlode@internal@query_ir:cte_def()). stmt_ctes(Stmt) -> case Stmt of {select_stmt, Ctes, _} -> Ctes; {insert_stmt, Ctes@1, _, _, _, _, _} -> Ctes@1; {update_stmt, Ctes@2, _, _, _, _, _, _} -> Ctes@2; {delete_stmt, Ctes@3, _, _, _, _, _} -> Ctes@3; {unstructured_stmt, _, _} -> [] end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1029). ?DOC(false). -spec ir_literal_expr_to_type(sqlode@internal@query_ir:expr()) -> {ok, {sqlode@internal@model:scalar_type(), boolean()}} | {error, nil}. ir_literal_expr_to_type(Expr) -> case Expr of {number_lit, N} -> {ok, {number_scalar_type(N), false}}; {unary, <<"-"/utf8>>, {number_lit, N@1}} -> {ok, {number_scalar_type(N@1), false}}; {unary, <<"+"/utf8>>, {number_lit, N@2}} -> {ok, {number_scalar_type(N@2), false}}; {string_lit, _} -> {ok, {string_type, false}}; {bool_lit, _} -> {ok, {bool_type, false}}; null_lit -> {ok, {string_type, true}}; _ -> {error, nil} end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1005). ?DOC(false). -spec ir_from_item_to_values_table(sqlode@internal@query_ir:from_item_ex()) -> {ok, sqlode@internal@model:table()} | {error, nil}. ir_from_item_to_values_table(From_item) -> case From_item of {from_values, Rows, Alias, Column_aliases} -> case {Alias, Column_aliases, Rows} of {<<""/utf8>>, _, _} -> {error, nil}; {_, [], _} -> {error, nil}; {_, _, []} -> {error, nil}; {_, Names, [First_row | _]} -> gleam@result:'try'( gleam@list:try_map( First_row, fun ir_literal_expr_to_type/1 ), fun(Types) -> {ok, {table, string:lowercase(Alias), build_values_columns(Names, Types)}} end ) end; _ -> {error, nil} end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1063). ?DOC(false). -spec is_from_subquery(sqlode@internal@query_ir:from_item_ex()) -> boolean(). is_from_subquery(From_item) -> case From_item of {from_subquery, _, _, _} -> true; _ -> false end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1132). ?DOC(false). -spec flatten_from_item(sqlode@internal@query_ir:from_item_ex()) -> list(sqlode@internal@query_ir:from_item_ex()). flatten_from_item(From) -> case From of {from_join, Left, Right, _, _, _} -> lists:append(flatten_from_item(Left), flatten_from_item(Right)); _ -> [From] end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1121). ?DOC(false). -spec collect_from_items(sqlode@internal@query_ir:stmt()) -> list(sqlode@internal@query_ir:from_item_ex()). collect_from_items(Stmt) -> case Stmt of {select_stmt, _, Core} -> gleam@list:flat_map( erlang:element(4, Core), fun flatten_from_item/1 ); {update_stmt, _, _, _, _, From, _, _} -> gleam@list:flat_map(From, fun flatten_from_item/1); {delete_stmt, _, _, _, Using, _, _} -> gleam@list:flat_map(Using, fun flatten_from_item/1); {insert_stmt, _, _, _, _, _, _} -> []; {unstructured_stmt, _, _} -> [] end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1001). ?DOC(false). -spec extract_values_tables_from_stmt(sqlode@internal@query_ir:stmt()) -> list(sqlode@internal@model:table()). extract_values_tables_from_stmt(Stmt) -> gleam@list:filter_map( collect_from_items(Stmt), fun ir_from_item_to_values_table/1 ). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1146). ?DOC(false). -spec collect_aliased_tables(sqlode@internal@query_ir:stmt()) -> list({binary(), binary()}). collect_aliased_tables(Stmt) -> From_aliases = gleam@list:filter_map( collect_from_items(Stmt), fun(Item) -> case Item of {from_table, Name, {some, A}} -> Table_name = string:lowercase(Name), Alias_name = string:lowercase(A), case {Table_name, Alias_name} of {<<""/utf8>>, _} -> {error, nil}; {_, <<""/utf8>>} -> {error, nil}; {_, _} -> {ok, {Table_name, Alias_name}} end; _ -> {error, nil} end end ), Dml_target = case Stmt of {update_stmt, _, Table, {some, A@1}, _, _, _, _} -> Table_name@1 = string:lowercase(Table), Alias_name@1 = string:lowercase(A@1), case {Table_name@1, Alias_name@1} of {<<""/utf8>>, _} -> []; {_, <<""/utf8>>} -> []; {_, _} -> [{Table_name@1, Alias_name@1}] end; {delete_stmt, _, Table@1, {some, A@2}, _, _, _} -> Table_name@2 = string:lowercase(Table@1), Alias_name@2 = string:lowercase(A@2), case {Table_name@2, Alias_name@2} of {<<""/utf8>>, _} -> []; {_, <<""/utf8>>} -> []; {_, _} -> [{Table_name@2, Alias_name@2}] end; _ -> [] end, lists:append(Dml_target, From_aliases). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1078). ?DOC(false). -spec extract_table_aliases_from_stmt( sqlode@internal@query_ir:stmt(), sqlode@internal@model:catalog() ) -> list(sqlode@internal@model:table()). extract_table_aliases_from_stmt(Stmt, Catalog) -> Aliased_pairs = collect_aliased_tables(Stmt), Acc_init = [], _pipe = gleam@list:fold( Aliased_pairs, Acc_init, fun(Acc, Pair) -> {Table_name, Alias_name} = Pair, case Alias_name =:= Table_name of true -> Acc; false -> case gleam@list:find( erlang:element(2, Catalog), fun(T) -> erlang:element(2, T) =:= Table_name end ) of {ok, Found} -> case gleam@list:any( Acc, fun(T@1) -> erlang:element(2, T@1) =:= Alias_name end ) of true -> Acc; false -> case gleam@list:any( erlang:element(2, Catalog), fun(T@2) -> erlang:element(2, T@2) =:= Alias_name end ) of true -> Acc; false -> [{table, Alias_name, erlang:element(3, Found)} | Acc] end end; {error, _} -> Acc end end end ), lists:reverse(_pipe). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1201). ?DOC(false). -spec tok_find_returning_tokens(list(sqlode@internal@lexer:token())) -> gleam@option:option(list(sqlode@internal@lexer:token())). tok_find_returning_tokens(Tokens) -> case Tokens of [] -> none; [{keyword, <<"returning"/utf8>>} | Rest] -> Filtered = gleam@list:filter(Rest, fun(Tok) -> case Tok of semicolon -> false; _ -> true end end), case Filtered of [] -> none; _ -> {some, Filtered} end; [_ | Rest@1] -> tok_find_returning_tokens(Rest@1) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1676). ?DOC(false). -spec classify_function(binary()) -> function_class(). classify_function(Lowered_name) -> case Lowered_name of <<"count"/utf8>> -> fn_count; <<"last_insert_rowid"/utf8>> -> fn_count; <<"sum"/utf8>> -> fn_aggregate_inner; <<"min"/utf8>> -> fn_aggregate_inner; <<"max"/utf8>> -> fn_aggregate_inner; <<"avg"/utf8>> -> fn_avg; <<"row_number"/utf8>> -> fn_window_int; <<"rank"/utf8>> -> fn_window_int; <<"dense_rank"/utf8>> -> fn_window_int; <<"ntile"/utf8>> -> fn_window_int; <<"percent_rank"/utf8>> -> fn_window_float; <<"cume_dist"/utf8>> -> fn_window_float; <<"lag"/utf8>> -> fn_window_first_arg; <<"lead"/utf8>> -> fn_window_first_arg; <<"first_value"/utf8>> -> fn_window_first_arg; <<"last_value"/utf8>> -> fn_window_first_arg; <<"nth_value"/utf8>> -> fn_window_first_arg; <<"coalesce"/utf8>> -> fn_coalesce; <<"greatest"/utf8>> -> fn_greatest_least; <<"least"/utf8>> -> fn_greatest_least; <<"replace"/utf8>> -> fn_string; <<"lower"/utf8>> -> fn_string; <<"upper"/utf8>> -> fn_string; <<"trim"/utf8>> -> fn_string; <<"ltrim"/utf8>> -> fn_string; <<"rtrim"/utf8>> -> fn_string; <<"substr"/utf8>> -> fn_string; <<"substring"/utf8>> -> fn_string; <<"concat"/utf8>> -> fn_string; <<"reverse"/utf8>> -> fn_string; <<"lpad"/utf8>> -> fn_string; <<"rpad"/utf8>> -> fn_string; <<"left"/utf8>> -> fn_string; <<"right"/utf8>> -> fn_string; <<"repeat"/utf8>> -> fn_string; <<"initcap"/utf8>> -> fn_string; <<"translate"/utf8>> -> fn_string; <<"to_char"/utf8>> -> fn_string; <<"format"/utf8>> -> fn_string; <<"quote_literal"/utf8>> -> fn_string; <<"quote_ident"/utf8>> -> fn_string; <<"md5"/utf8>> -> fn_string; <<"encode"/utf8>> -> fn_string; <<"decode"/utf8>> -> fn_string; <<"length"/utf8>> -> fn_length; <<"char_length"/utf8>> -> fn_length; <<"character_length"/utf8>> -> fn_length; <<"octet_length"/utf8>> -> fn_length; <<"bit_length"/utf8>> -> fn_length; <<"position"/utf8>> -> fn_length; <<"strpos"/utf8>> -> fn_length; <<"ascii"/utf8>> -> fn_length; <<"abs"/utf8>> -> fn_math; <<"round"/utf8>> -> fn_math; <<"floor"/utf8>> -> fn_math; <<"ceil"/utf8>> -> fn_math; <<"ceiling"/utf8>> -> fn_math; <<"mod"/utf8>> -> fn_math; <<"power"/utf8>> -> fn_math; <<"sqrt"/utf8>> -> fn_math; <<"sign"/utf8>> -> fn_math; <<"trunc"/utf8>> -> fn_math; <<"log"/utf8>> -> fn_math; <<"ln"/utf8>> -> fn_math; <<"exp"/utf8>> -> fn_math; <<"random"/utf8>> -> fn_math; <<"pi"/utf8>> -> fn_math; <<"degrees"/utf8>> -> fn_math; <<"radians"/utf8>> -> fn_math; <<"div"/utf8>> -> fn_math; <<"now"/utf8>> -> fn_datetime; <<"current_timestamp"/utf8>> -> fn_datetime; <<"current_date"/utf8>> -> fn_datetime; <<"current_time"/utf8>> -> fn_datetime; <<"date"/utf8>> -> fn_datetime; <<"time"/utf8>> -> fn_datetime; <<"timestamp"/utf8>> -> fn_datetime; <<"date_trunc"/utf8>> -> fn_datetime; <<"date_part"/utf8>> -> fn_datetime; <<"extract"/utf8>> -> fn_datetime; <<"age"/utf8>> -> fn_datetime; <<"make_date"/utf8>> -> fn_datetime; <<"make_time"/utf8>> -> fn_datetime; <<"make_timestamp"/utf8>> -> fn_datetime; <<"to_timestamp"/utf8>> -> fn_datetime; <<"to_date"/utf8>> -> fn_datetime; <<"clock_timestamp"/utf8>> -> fn_datetime; <<"statement_timestamp"/utf8>> -> fn_datetime; <<"timeofday"/utf8>> -> fn_datetime; <<"localtime"/utf8>> -> fn_datetime; <<"localtimestamp"/utf8>> -> fn_datetime; <<"nullif"/utf8>> -> fn_nullif; <<"ifnull"/utf8>> -> fn_nullif; <<"nvl"/utf8>> -> fn_nullif; _ -> fn_unknown end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1763). ?DOC(false). -spec is_datetime_identifier(binary()) -> boolean(). is_datetime_identifier(Lowered) -> case Lowered of <<"now"/utf8>> -> true; <<"current_timestamp"/utf8>> -> true; <<"current_date"/utf8>> -> true; <<"current_time"/utf8>> -> true; <<"localtime"/utf8>> -> true; <<"localtimestamp"/utf8>> -> true; _ -> false end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1775). ?DOC(false). -spec infer_datetime_return_type(binary()) -> sqlode@internal@model:scalar_type(). infer_datetime_return_type(Lowered) -> case Lowered of <<"current_date"/utf8>> -> date_type; <<"date"/utf8>> -> date_type; <<"make_date"/utf8>> -> date_type; <<"to_date"/utf8>> -> date_type; <<"current_time"/utf8>> -> time_type; <<"time"/utf8>> -> time_type; <<"make_time"/utf8>> -> time_type; <<"localtime"/utf8>> -> time_type; <<"date_part"/utf8>> -> float_type; <<"extract"/utf8>> -> float_type; <<"to_char"/utf8>> -> string_type; _ -> date_time_type end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1898). ?DOC(false). -spec is_unary_context(gleam@option:option(sqlode@internal@lexer:token())) -> boolean(). is_unary_context(Prev) -> case Prev of none -> true; {some, l_paren} -> true; {some, {operator, _}} -> true; {some, {keyword, _}} -> true; {some, comma} -> true; _ -> false end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1863). ?DOC(false). -spec split_arithmetic_loop( list(sqlode@internal@lexer:token()), integer(), gleam@option:option(sqlode@internal@lexer:token()), list(sqlode@internal@lexer:token()), list(list(sqlode@internal@lexer:token())) ) -> list(list(sqlode@internal@lexer:token())). split_arithmetic_loop(Tokens, Depth, Prev, Current, Acc) -> case Tokens of [] -> [lists:reverse(Current) | Acc]; [l_paren = T | Rest] -> split_arithmetic_loop( Rest, Depth + 1, {some, T}, [T | Current], Acc ); [r_paren = T@1 | Rest@1] -> split_arithmetic_loop( Rest@1, Depth - 1, {some, T@1}, [T@1 | Current], Acc ); [{operator, Op} = T@2 | Rest@2] when (Depth =:= 0) andalso ((((Op =:= <<"+"/utf8>>) orelse (Op =:= <<"-"/utf8>>)) orelse (Op =:= <<"*"/utf8>>)) orelse (Op =:= <<"/"/utf8>>)) -> case is_unary_context(Prev) of true -> split_arithmetic_loop( Rest@2, Depth, {some, T@2}, [T@2 | Current], Acc ); false -> split_arithmetic_loop( Rest@2, Depth, {some, T@2}, [], [lists:reverse(Current) | Acc] ) end; [star = T@3 | Rest@3] when Depth =:= 0 -> split_arithmetic_loop( Rest@3, Depth, {some, T@3}, [], [lists:reverse(Current) | Acc] ); [T@4 | Rest@4] -> split_arithmetic_loop( Rest@4, Depth, {some, T@4}, [T@4 | Current], Acc ) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1855). ?DOC(false). -spec split_arithmetic_operands(list(sqlode@internal@lexer:token())) -> list(list(sqlode@internal@lexer:token())). split_arithmetic_operands(Tokens) -> _pipe = split_arithmetic_loop(Tokens, 0, none, [], []), _pipe@1 = lists:reverse(_pipe), gleam@list:filter(_pipe@1, fun(Operand) -> Operand /= [] end). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1975). ?DOC(false). -spec merge_scalar_types( gleam@option:option(sqlode@internal@model:scalar_type()), gleam@option:option(sqlode@internal@model:scalar_type()) ) -> {ok, gleam@option:option(sqlode@internal@model:scalar_type())} | {error, nil}. merge_scalar_types(A, B) -> case {A, B} of {none, Other} -> {ok, Other}; {Other@1, none} -> {ok, Other@1}; {{some, int_type}, {some, int_type}} -> {ok, {some, int_type}}; {{some, float_type}, {some, float_type}} -> {ok, {some, float_type}}; {{some, int_type}, {some, float_type}} -> {ok, {some, float_type}}; {{some, float_type}, {some, int_type}} -> {ok, {some, float_type}}; {{some, X}, {some, Y}} -> case X =:= Y of true -> {ok, {some, X}}; false -> {error, nil} end end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1958). ?DOC(false). -spec unify_types_loop( list({gleam@option:option(sqlode@internal@model:scalar_type()), boolean()}), gleam@option:option(sqlode@internal@model:scalar_type()), boolean() ) -> {ok, {gleam@option:option(sqlode@internal@model:scalar_type()), boolean()}} | {error, nil}. unify_types_loop(Items, Acc_type, Acc_nullable) -> case Items of [] -> {ok, {Acc_type, Acc_nullable}}; [{Next_type, Next_nullable} | Rest] -> Merged_nullable = Acc_nullable orelse Next_nullable, case merge_scalar_types(Acc_type, Next_type) of {ok, Merged_type} -> unify_types_loop(Rest, Merged_type, Merged_nullable); {error, nil} -> {error, nil} end end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1942). ?DOC(false). -spec unify_types_nullable( list({gleam@option:option(sqlode@internal@model:scalar_type()), boolean()}) ) -> {ok, gleam@option:option({sqlode@internal@model:scalar_type(), boolean()})} | {error, nil}. unify_types_nullable(Items) -> case Items of [] -> {ok, none}; [{T, Nullable} | Rest] -> _pipe = unify_types_loop(Rest, T, Nullable), gleam@result:map(_pipe, fun(Pair) -> case erlang:element(1, Pair) of {some, Scalar_type} -> {some, {Scalar_type, erlang:element(2, Pair)}}; none -> none end end) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1996). ?DOC(false). -spec resolve_column_type_nullable_from_tokens( list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog(), list(binary()) ) -> gleam@option:option({sqlode@internal@model:scalar_type(), boolean()}). resolve_column_type_nullable_from_tokens(Tokens, Catalog, Table_names) -> case Tokens of [{ident, Table}, dot, {ident, Col}] -> case sqlode@internal@query_analyzer@context:find_column( Catalog, Table, Col ) of {some, Column} -> {some, {erlang:element(3, Column), erlang:element(4, Column)}}; none -> none end; [{ident, Col@1}] -> case sqlode@internal@query_analyzer@context:find_column_in_tables( Catalog, Table_names, Col@1 ) of {ok, {some, {_, Column@1}}} -> {some, {erlang:element(3, Column@1), erlang:element(4, Column@1)}}; _ -> none end; _ -> none end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2059). ?DOC(false). -spec classify_top_level_token(sqlode@internal@lexer:token()) -> top_level_pattern(). classify_top_level_token(Token) -> case Token of {operator, Op} when (((((((((((Op =:= <<"="/utf8>>) orelse (Op =:= <<"!="/utf8>>)) orelse (Op =:= <<"<>"/utf8>>)) orelse (Op =:= <<"<"/utf8>>)) orelse (Op =:= <<">"/utf8>>)) orelse (Op =:= <<"<="/utf8>>)) orelse (Op =:= <<">="/utf8>>)) orelse (Op =:= <<"@>"/utf8>>)) orelse (Op =:= <<"<@"/utf8>>)) orelse (Op =:= <<"?|"/utf8>>)) orelse (Op =:= <<"?&"/utf8>>)) orelse (Op =:= <<"&&"/utf8>>) -> pat_bool; {keyword, Kw} when ((((((((Kw =:= <<"and"/utf8>>) orelse (Kw =:= <<"or"/utf8>>)) orelse (Kw =:= <<"between"/utf8>>)) orelse (Kw =:= <<"in"/utf8>>)) orelse (Kw =:= <<"like"/utf8>>)) orelse (Kw =:= <<"ilike"/utf8>>)) orelse (Kw =:= <<"is"/utf8>>)) orelse (Kw =:= <<"not"/utf8>>)) orelse (Kw =:= <<"exists"/utf8>>) -> pat_bool; {operator, Op@1} when (Op@1 =:= <<"->>"/utf8>>) orelse (Op@1 =:= <<"#>>"/utf8>>) -> pat_json_text; {operator, Op@2} when (Op@2 =:= <<"->"/utf8>>) orelse (Op@2 =:= <<"#>"/utf8>>) -> pat_json; {operator, <<"||"/utf8>>} -> pat_concat; {operator, Op@3} when (((Op@3 =:= <<"+"/utf8>>) orelse (Op@3 =:= <<"-"/utf8>>)) orelse (Op@3 =:= <<"*"/utf8>>)) orelse (Op@3 =:= <<"/"/utf8>>) -> pat_arithmetic; star -> pat_arithmetic; _ -> pat_none end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2020). ?DOC(false). -spec find_pattern_loop( list(sqlode@internal@lexer:token()), integer(), top_level_pattern() ) -> top_level_pattern(). find_pattern_loop(Tokens, Depth, Found) -> case Tokens of [] -> Found; [l_paren | Rest] -> find_pattern_loop(Rest, Depth + 1, Found); [r_paren | Rest@1] -> find_pattern_loop(Rest@1, Depth - 1, Found); [Token | Rest@2] when Depth =:= 0 -> case classify_top_level_token(Token) of pat_bool -> pat_bool; pat_json -> find_pattern_loop(Rest@2, Depth, case Found of pat_none -> pat_json; _ -> Found end); pat_json_text -> find_pattern_loop(Rest@2, Depth, case Found of pat_none -> pat_json_text; _ -> Found end); pat_concat -> find_pattern_loop(Rest@2, Depth, case Found of pat_none -> pat_concat; _ -> Found end); pat_arithmetic -> find_pattern_loop(Rest@2, Depth, case Found of pat_none -> pat_arithmetic; _ -> Found end); pat_none -> find_pattern_loop(Rest@2, Depth, Found) end; [_ | Rest@3] -> find_pattern_loop(Rest@3, Depth, Found) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2016). ?DOC(false). -spec find_top_level_pattern(list(sqlode@internal@lexer:token())) -> top_level_pattern(). find_top_level_pattern(Tokens) -> find_pattern_loop(Tokens, 0, pat_none). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2105). ?DOC(false). -spec tok_first_comma_group(list(sqlode@internal@lexer:token())) -> list(sqlode@internal@lexer:token()). tok_first_comma_group(Tokens) -> case sqlode@internal@query_analyzer@token_utils:split_on_commas(Tokens) of [First | _] -> First; [] -> [] end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2113). ?DOC(false). -spec tok_find_as_type(list(sqlode@internal@lexer:token())) -> gleam@option:option(binary()). tok_find_as_type(Tokens) -> case Tokens of [] -> none; [{keyword, <<"as"/utf8>>} | Rest] -> Type_text = begin _pipe = Rest, _pipe@1 = gleam@list:filter_map(_pipe, fun(T) -> case T of {ident, N} -> {ok, N}; {keyword, K} -> {ok, K}; _ -> {error, nil} end end), gleam@string:join(_pipe@1, <<" "/utf8>>) end, case Type_text of <<""/utf8>> -> none; _ -> {some, string:lowercase(Type_text)} end; [_ | Rest@1] -> tok_find_as_type(Rest@1) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2323). ?DOC(false). -spec append_if_in_branch( sqlode@internal@lexer:token(), list(sqlode@internal@lexer:token()), boolean() ) -> list(sqlode@internal@lexer:token()). append_if_in_branch(Token, Acc, In_branch) -> case In_branch of true -> [Token | Acc]; false -> Acc end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2334). ?DOC(false). -spec flush_current_branch( case_collector_mode(), list(sqlode@internal@lexer:token()), list(list(sqlode@internal@lexer:token())), gleam@option:option(list(sqlode@internal@lexer:token())) ) -> {list(list(sqlode@internal@lexer:token())), gleam@option:option(list(sqlode@internal@lexer:token()))}. flush_current_branch(Mode, Acc, Then_branches_rev, Else_branch) -> case Mode of in_then -> {[lists:reverse(Acc) | Then_branches_rev], Else_branch}; in_else -> {Then_branches_rev, {some, lists:reverse(Acc)}}; scanning -> {Then_branches_rev, Else_branch} end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2205). ?DOC(false). -spec collect_case_loop( list(sqlode@internal@lexer:token()), integer(), integer(), case_collector_mode(), list(sqlode@internal@lexer:token()), list(list(sqlode@internal@lexer:token())), gleam@option:option(list(sqlode@internal@lexer:token())) ) -> {list(list(sqlode@internal@lexer:token())), gleam@option:option(list(sqlode@internal@lexer:token()))}. collect_case_loop( Tokens, Paren_depth, Case_depth, Mode, Acc, Then_branches_rev, Else_branch ) -> case Tokens of [] -> {Then_branches_rev@1, Else_branch@1} = flush_current_branch( Mode, Acc, Then_branches_rev, Else_branch ), {lists:reverse(Then_branches_rev@1), Else_branch@1}; [First | Rest] -> In_branch = case Mode of scanning -> false; _ -> true end, At_top = (Paren_depth =:= 0) andalso (Case_depth =:= 0), case First of l_paren -> collect_case_loop( Rest, Paren_depth + 1, Case_depth, Mode, append_if_in_branch(First, Acc, In_branch), Then_branches_rev, Else_branch ); r_paren -> collect_case_loop( Rest, Paren_depth - 1, Case_depth, Mode, append_if_in_branch(First, Acc, In_branch), Then_branches_rev, Else_branch ); {keyword, <<"case"/utf8>>} -> collect_case_loop( Rest, Paren_depth, Case_depth + 1, Mode, append_if_in_branch(First, Acc, In_branch), Then_branches_rev, Else_branch ); {keyword, <<"end"/utf8>>} when At_top -> {Then_branches_rev@2, Else_branch@2} = flush_current_branch( Mode, Acc, Then_branches_rev, Else_branch ), {lists:reverse(Then_branches_rev@2), Else_branch@2}; {keyword, <<"end"/utf8>>} -> collect_case_loop( Rest, Paren_depth, Case_depth - 1, Mode, append_if_in_branch(First, Acc, In_branch), Then_branches_rev, Else_branch ); {keyword, <<"when"/utf8>>} when At_top -> {Then_branches_rev@3, Else_branch@3} = flush_current_branch( Mode, Acc, Then_branches_rev, Else_branch ), collect_case_loop( Rest, Paren_depth, Case_depth, scanning, [], Then_branches_rev@3, Else_branch@3 ); {keyword, <<"then"/utf8>>} when At_top -> collect_case_loop( Rest, Paren_depth, Case_depth, in_then, [], Then_branches_rev, Else_branch ); {keyword, <<"else"/utf8>>} when At_top -> {Then_branches_rev@4, Else_branch@4} = flush_current_branch( Mode, Acc, Then_branches_rev, Else_branch ), collect_case_loop( Rest, Paren_depth, Case_depth, in_else, [], Then_branches_rev@4, Else_branch@4 ); _ -> collect_case_loop( Rest, Paren_depth, Case_depth, Mode, append_if_in_branch(First, Acc, In_branch), Then_branches_rev, Else_branch ) end end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2199). ?DOC(false). -spec collect_case_branches(list(sqlode@internal@lexer:token())) -> {list(list(sqlode@internal@lexer:token())), gleam@option:option(list(sqlode@internal@lexer:token()))}. collect_case_branches(Tokens) -> collect_case_loop(Tokens, 0, 0, scanning, [], [], none). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2348). ?DOC(false). -spec resolve_column_type_from_tokens( list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog(), list(binary()) ) -> gleam@option:option(sqlode@internal@model:scalar_type()). resolve_column_type_from_tokens(Tokens, Catalog, Table_names) -> case Tokens of [{ident, Table}, dot, {ident, Col}] -> case sqlode@internal@query_analyzer@context:find_column( Catalog, Table, Col ) of {some, Column} -> {some, erlang:element(3, Column)}; none -> none end; [{ident, Col@1}] -> case sqlode@internal@query_analyzer@context:find_column_in_tables( Catalog, Table_names, Col@1 ) of {ok, {some, {_, Column@1}}} -> {some, erlang:element(3, Column@1)}; _ -> none end; [star] -> none; _ -> none end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2381). ?DOC(false). -spec tok_skip_cte_defs(list(sqlode@internal@lexer:token())) -> list(sqlode@internal@lexer:token()). tok_skip_cte_defs(Tokens) -> case Tokens of [] -> []; [{keyword, Kw} | _] when (((Kw =:= <<"select"/utf8>>) orelse (Kw =:= <<"insert"/utf8>>)) orelse (Kw =:= <<"update"/utf8>>)) orelse (Kw =:= <<"delete"/utf8>>) -> Tokens; [l_paren | Rest] -> Remaining = sqlode@internal@query_analyzer@token_utils:skip_parens( Rest, 1 ), tok_skip_cte_defs(Remaining); [_ | Rest@1] -> tok_skip_cte_defs(Rest@1) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2374). ?DOC(false). -spec tok_strip_cte(list(sqlode@internal@lexer:token())) -> list(sqlode@internal@lexer:token()). tok_strip_cte(Tokens) -> case Tokens of [{keyword, <<"with"/utf8>>} | Rest] -> tok_skip_cte_defs(Rest); _ -> Tokens end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 432). ?DOC(false). -spec detect_dml_target(list(sqlode@internal@lexer:token())) -> gleam@option:option(binary()). detect_dml_target(Tokens) -> Stripped = tok_strip_cte(Tokens), case Stripped of [{keyword, <<"insert"/utf8>>} | Rest] -> case sqlode@internal@query_analyzer@token_utils:strip_insert_or_action( Rest ) of [{keyword, <<"into"/utf8>>} | After_into] -> {Name, _} = sqlode@internal@query_analyzer@token_utils:read_table_name( After_into ), Name; _ -> none end; [{keyword, <<"update"/utf8>>} | Rest@1] -> {Name@1, _} = sqlode@internal@query_analyzer@token_utils:read_table_name( Rest@1 ), Name@1; [{keyword, <<"delete"/utf8>>}, {keyword, <<"from"/utf8>>} | Rest@2] -> {Name@2, _} = sqlode@internal@query_analyzer@token_utils:read_table_name( Rest@2 ), Name@2; _ -> none end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2400). ?DOC(false). -spec tok_strip_compound_loop( list(sqlode@internal@lexer:token()), integer(), list(sqlode@internal@lexer:token()) ) -> list(sqlode@internal@lexer:token()). tok_strip_compound_loop(Tokens, Depth, Acc) -> case Tokens of [] -> lists:reverse(Acc); [l_paren | Rest] -> tok_strip_compound_loop(Rest, Depth + 1, [l_paren | Acc]); [r_paren | Rest@1] -> tok_strip_compound_loop(Rest@1, Depth - 1, [r_paren | Acc]); [{keyword, Kw} | _] when Depth =:= 0 -> case ((Kw =:= <<"union"/utf8>>) orelse (Kw =:= <<"intersect"/utf8>>)) orelse (Kw =:= <<"except"/utf8>>) of true -> lists:reverse(Acc); false -> case Tokens of [Token | Rest@2] -> tok_strip_compound_loop( Rest@2, Depth, [Token | Acc] ); _ -> lists:reverse(Acc) end end; [Token@1 | Rest@3] -> tok_strip_compound_loop(Rest@3, Depth, [Token@1 | Acc]) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2396). ?DOC(false). -spec tok_strip_compound(list(sqlode@internal@lexer:token())) -> list(sqlode@internal@lexer:token()). tok_strip_compound(Tokens) -> tok_strip_compound_loop(Tokens, 0, []). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2466). ?DOC(false). -spec tok_split_compound_branches_loop( list(sqlode@internal@lexer:token()), integer(), list(sqlode@internal@lexer:token()), list(list(sqlode@internal@lexer:token())) ) -> list(list(sqlode@internal@lexer:token())). tok_split_compound_branches_loop(Tokens, Depth, Current, Acc) -> case Tokens of [] -> case Current of [] -> lists:reverse(Acc); _ -> lists:reverse([lists:reverse(Current) | Acc]) end; [l_paren | Rest] -> tok_split_compound_branches_loop( Rest, Depth + 1, [l_paren | Current], Acc ); [r_paren | Rest@1] -> tok_split_compound_branches_loop( Rest@1, Depth - 1, [r_paren | Current], Acc ); [{keyword, Kw} | Rest@2] when Depth =:= 0 -> case ((Kw =:= <<"union"/utf8>>) orelse (Kw =:= <<"intersect"/utf8>>)) orelse (Kw =:= <<"except"/utf8>>) of true -> Rest2 = case Rest@2 of [{keyword, <<"all"/utf8>>} | R] -> R; _ -> Rest@2 end, Branch = lists:reverse(Current), tok_split_compound_branches_loop( Rest2, 0, [], [Branch | Acc] ); false -> tok_split_compound_branches_loop( Rest@2, Depth, [{keyword, Kw} | Current], Acc ) end; [Token | Rest@3] -> tok_split_compound_branches_loop( Rest@3, Depth, [Token | Current], Acc ) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2460). ?DOC(false). -spec tok_split_compound_branches(list(sqlode@internal@lexer:token())) -> list(list(sqlode@internal@lexer:token())). tok_split_compound_branches(Tokens) -> tok_split_compound_branches_loop(Tokens, 0, [], []). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2604). ?DOC(false). -spec tok_skip_keyword(list(sqlode@internal@lexer:token()), binary()) -> list(sqlode@internal@lexer:token()). tok_skip_keyword(Tokens, Kw) -> case Tokens of [{keyword, K} | Rest] when K =:= Kw -> Rest; _ -> Tokens end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2532). ?DOC(false). -spec tok_nullable_loop( list(sqlode@internal@lexer:token()), list(binary()), boolean() ) -> {list(binary()), boolean()}. tok_nullable_loop(Tokens, Acc, Primary_nullable) -> case Tokens of [] -> {Acc, Primary_nullable}; [{keyword, <<"left"/utf8>>} | Rest] -> Rest2 = tok_skip_keyword(Rest, <<"outer"/utf8>>), case Rest2 of [{keyword, <<"join"/utf8>>}, {keyword, <<"lateral"/utf8>>}, l_paren | After_lp] -> Remaining = sqlode@internal@query_analyzer@token_utils:skip_parens( After_lp, 1 ), case sqlode@internal@query_analyzer@token_utils:read_subquery_alias( Remaining ) of {{some, N}, After_alias} -> tok_nullable_loop( After_alias, [N | Acc], Primary_nullable ); {none, After} -> tok_nullable_loop(After, Acc, Primary_nullable) end; [{keyword, <<"join"/utf8>>}, l_paren | After_lp@1] -> Remaining@1 = sqlode@internal@query_analyzer@token_utils:skip_parens( After_lp@1, 1 ), case sqlode@internal@query_analyzer@token_utils:read_subquery_alias( Remaining@1 ) of {{some, N@1}, After_alias@1} -> tok_nullable_loop( After_alias@1, [N@1 | Acc], Primary_nullable ); {none, After@1} -> tok_nullable_loop(After@1, Acc, Primary_nullable) end; [{keyword, <<"join"/utf8>>} | After_join] -> {Name, Remaining@2} = sqlode@internal@query_analyzer@token_utils:read_table_name( After_join ), case Name of {some, N@2} -> tok_nullable_loop( Remaining@2, [N@2 | Acc], Primary_nullable ); none -> tok_nullable_loop( Remaining@2, Acc, Primary_nullable ) end; _ -> tok_nullable_loop(Rest, Acc, Primary_nullable) end; [{keyword, <<"full"/utf8>>} | Rest@1] -> Rest2@1 = tok_skip_keyword(Rest@1, <<"outer"/utf8>>), case Rest2@1 of [{keyword, <<"join"/utf8>>} | After_join@1] -> {Name@1, Remaining@3} = sqlode@internal@query_analyzer@token_utils:read_table_name( After_join@1 ), case Name@1 of {some, N@3} -> tok_nullable_loop(Remaining@3, [N@3 | Acc], true); none -> tok_nullable_loop(Remaining@3, Acc, true) end; _ -> tok_nullable_loop(Rest@1, Acc, Primary_nullable) end; [{keyword, <<"right"/utf8>>} | Rest@2] -> Rest2@2 = tok_skip_keyword(Rest@2, <<"outer"/utf8>>), case Rest2@2 of [{keyword, <<"join"/utf8>>} | After_join@2] -> tok_nullable_loop(After_join@2, Acc, true); _ -> tok_nullable_loop(Rest@2, Acc, Primary_nullable) end; [_ | Rest@3] -> tok_nullable_loop(Rest@3, Acc, Primary_nullable) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2516). ?DOC(false). -spec tok_extract_nullable_tables( list(sqlode@internal@lexer:token()), list(binary()) ) -> list(binary()). tok_extract_nullable_tables(Tokens, All_table_names) -> Primary = case All_table_names of [P | _] -> {some, P}; [] -> none end, {Nullable_joined, Primary_nullable} = tok_nullable_loop(Tokens, [], false), case {Primary_nullable, Primary} of {true, {some, Name}} -> [Name | Nullable_joined]; {_, _} -> Nullable_joined end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2649). ?DOC(false). -spec tok_collect_until_from( list(sqlode@internal@lexer:token()), integer(), list(sqlode@internal@lexer:token()) ) -> gleam@option:option(list(sqlode@internal@lexer:token())). tok_collect_until_from(Tokens, Depth, Acc) -> case Tokens of [] -> case Acc of [] -> none; _ -> {some, lists:reverse(Acc)} end; [{keyword, <<"from"/utf8>>} | _] when Depth =:= 0 -> case Acc of [] -> none; _ -> {some, lists:reverse(Acc)} end; [l_paren | Rest] -> tok_collect_until_from(Rest, Depth + 1, [l_paren | Acc]); [r_paren | Rest@1] -> tok_collect_until_from(Rest@1, Depth - 1, [r_paren | Acc]); [Token | Rest@2] -> tok_collect_until_from(Rest@2, Depth, [Token | Acc]) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2624). ?DOC(false). -spec tok_find_select_to_from(list(sqlode@internal@lexer:token())) -> gleam@option:option(list(sqlode@internal@lexer:token())). tok_find_select_to_from(Tokens) -> case Tokens of [] -> none; [{keyword, <<"select"/utf8>>} | Rest] -> Rest2 = case Rest of [{keyword, <<"distinct"/utf8>>}, {keyword, <<"on"/utf8>>}, l_paren | After_on] -> sqlode@internal@query_analyzer@token_utils:skip_parens( After_on, 1 ); [{keyword, <<"distinct"/utf8>>} | R] -> R; [{keyword, <<"all"/utf8>>} | R@1] -> R@1; _ -> Rest end, tok_collect_until_from(Rest2, 0, []); [_ | Rest@1] -> tok_find_select_to_from(Rest@1) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2452). ?DOC(false). -spec count_branch_columns(list(sqlode@internal@lexer:token())) -> integer(). count_branch_columns(Branch) -> case tok_find_select_to_from(Branch) of {some, Col_tokens} -> erlang:length( sqlode@internal@query_analyzer@token_utils:split_on_commas( Col_tokens ) ); none -> 0 end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2427). ?DOC(false). -spec validate_compound_column_counts( binary(), list(sqlode@internal@lexer:token()) ) -> {ok, nil} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. validate_compound_column_counts(Query_name, Tokens) -> Branches = tok_split_compound_branches(Tokens), case Branches of [] -> {ok, nil}; [_] -> {ok, nil}; [First | Rest] -> First_count = count_branch_columns(First), gleam@list:try_each( Rest, fun(Branch) -> Branch_count = count_branch_columns(Branch), case Branch_count =:= First_count of true -> {ok, nil}; false -> {error, {compound_column_count_mismatch, Query_name, First_count, Branch_count}} end end ) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2700). ?DOC(false). -spec tok_reconstruct_macro_call(list(sqlode@internal@lexer:token())) -> binary(). tok_reconstruct_macro_call(Tokens) -> _pipe = Tokens, _pipe@1 = gleam@list:map(_pipe, fun(T) -> case T of {ident, N} -> N; dot -> <<"."/utf8>>; l_paren -> <<"("/utf8>>; r_paren -> <<")"/utf8>>; {keyword, K} -> K; _ -> <<""/utf8>> end end), erlang:list_to_binary(_pipe@1). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2798). ?DOC(false). -spec tok_find_last_as_idx( list(sqlode@internal@lexer:token()), integer(), gleam@option:option(integer()), integer() ) -> gleam@option:option(integer()). tok_find_last_as_idx(Tokens, Depth, Last, Idx) -> case Tokens of [] -> Last; [l_paren | Rest] -> tok_find_last_as_idx(Rest, Depth + 1, Last, Idx + 1); [r_paren | Rest@1] -> tok_find_last_as_idx(Rest@1, Depth - 1, Last, Idx + 1); [{keyword, <<"as"/utf8>>} | Rest@2] when Depth =:= 0 -> tok_find_last_as_idx(Rest@2, Depth, {some, Idx}, Idx + 1); [_ | Rest@3] -> tok_find_last_as_idx(Rest@3, Depth, Last, Idx + 1) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2788). ?DOC(false). -spec tok_split_on_last_as(list(sqlode@internal@lexer:token())) -> gleam@option:option({list(sqlode@internal@lexer:token()), list(sqlode@internal@lexer:token())}). tok_split_on_last_as(Tokens) -> Last_idx = tok_find_last_as_idx(Tokens, 0, none, 0), case Last_idx of none -> none; {some, Pos} -> {some, {gleam@list:take(Tokens, Pos), gleam@list:drop(Tokens, Pos + 1)}} end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2816). ?DOC(false). -spec tok_tokens_to_text(list(sqlode@internal@lexer:token())) -> binary(). tok_tokens_to_text(Tokens) -> sqlode@internal@lexer:tokens_to_string( Tokens, {token_render_options, false, false, none} ). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2716). ?DOC(false). -spec tok_parse_regular_column(list(sqlode@internal@lexer:token())) -> extracted_column(). tok_parse_regular_column(Tokens) -> case tok_split_on_last_as(Tokens) of {some, {Expr_tokens, Alias_tokens}} -> Alias_name = tok_tokens_to_text(Alias_tokens), Expr_text = tok_tokens_to_text(Expr_tokens), Table = case Expr_tokens of [{ident, T}, dot, {ident, _}] -> {some, string:lowercase(T)}; _ -> none end, {extracted_column, Alias_name, Table, {some, Expr_text}, {some, Expr_tokens}}; none -> case Tokens of [{ident, Table@1}, dot, {ident, Col}] -> {extracted_column, Col, {some, string:lowercase(Table@1)}, none, none}; [{ident, Name}] -> {extracted_column, Name, none, none, none}; [{keyword, Name@1}] -> {extracted_column, Name@1, none, none, none}; [{ident, Table@2}, dot, {keyword, Col@1}] -> {extracted_column, Col@1, {some, string:lowercase(Table@2)}, none, none}; [star] -> {extracted_column, <<"*"/utf8>>, none, none, none}; _ -> Text = tok_tokens_to_text(Tokens), {extracted_column, Text, none, {some, Text}, {some, Tokens}} end end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2674). ?DOC(false). -spec tok_parse_column_item(list(sqlode@internal@lexer:token())) -> extracted_column(). tok_parse_column_item(Tokens) -> case Tokens of [{ident, Name}, dot, {ident, Fn_name}, l_paren | _] -> case (string:lowercase(Name) =:= <<"sqlode"/utf8>>) andalso (string:lowercase( Fn_name ) =:= <<"embed"/utf8>>) of true -> Text = tok_reconstruct_macro_call(Tokens), {extracted_column, Text, none, none, none}; false -> tok_parse_regular_column(Tokens) end; _ -> tok_parse_regular_column(Tokens) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1187). ?DOC(false). -spec tok_extract_returning_columns(list(sqlode@internal@lexer:token())) -> gleam@option:option(list(extracted_column())). tok_extract_returning_columns(Tokens) -> case tok_find_returning_tokens(Tokens) of {some, Col_tokens} -> {some, begin _pipe = sqlode@internal@query_analyzer@token_utils:split_on_commas( Col_tokens ), gleam@list:map(_pipe, fun tok_parse_column_item/1) end}; none -> none end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2612). ?DOC(false). -spec tok_extract_select_columns(list(sqlode@internal@lexer:token())) -> list(extracted_column()). tok_extract_select_columns(Tokens) -> case tok_find_select_to_from(Tokens) of {some, Col_tokens} -> _pipe = sqlode@internal@query_analyzer@token_utils:split_on_commas( Col_tokens ), gleam@list:map(_pipe, fun tok_parse_column_item/1); none -> [] end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 872). ?DOC(false). -spec handle_derived( binary(), list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog(), list(sqlode@internal@model:table()) ) -> {ok, list(sqlode@internal@model:table())} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. handle_derived(Query_name, Tokens_after_select_kw, Catalog, Acc) -> {Body, After_rp} = sqlode@internal@query_analyzer@token_utils:collect_paren_contents( [{keyword, <<"select"/utf8>>} | Tokens_after_select_kw] ), case parse_derived_alias(After_rp) of {some, {Alias, Explicit_names, Remaining}} -> gleam@result:'try'( infer_columns_from_tokens(Query_name, Body, Catalog), fun(Cols) -> Body_columns = gleam@list:filter_map( Cols, fun(Item) -> case Item of {scalar_result, Rc} -> {ok, {column, erlang:element(2, Rc), erlang:element(3, Rc), erlang:element(4, Rc)}}; _ -> {error, nil} end end ), Columns = apply_explicit_column_names( Body_columns, Explicit_names ), Table = {table, Alias, Columns}, find_derived_tables( Query_name, Remaining, Catalog, [Table | Acc] ) end ); none -> find_derived_tables(Query_name, After_rp, Catalog, Acc) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 293). ?DOC(false). -spec infer_columns_from_tokens( binary(), list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog() ) -> {ok, list(sqlode@internal@model:result_item())} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. infer_columns_from_tokens(Query_name, Tokens, Catalog) -> infer_columns_from_tokens_scoped(Query_name, Tokens, Catalog, []). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 310). ?DOC(false). -spec infer_columns_from_tokens_scoped( binary(), list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog(), list(binary()) ) -> {ok, list(sqlode@internal@model:result_item())} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. infer_columns_from_tokens_scoped(Query_name, Tokens, Catalog, Outer_tables) -> gleam@result:'try'( augment_with_subquery_tables(Query_name, Tokens, Catalog), fun(Augmented) -> case tok_extract_returning_columns(Tokens) of {some, Returning_cols} -> Dml_target = detect_dml_target(Tokens), Scope_tables = case Dml_target of {some, Target} -> [Target]; none -> sqlode@internal@query_analyzer@token_utils:extract_table_names( Tokens ) end, Nullable_tables = tok_extract_nullable_tables( Tokens, Scope_tables ), case effective_tables(Scope_tables, Outer_tables) of [] -> {ok, []}; [Primary | _] = Tables -> resolve_select_columns( Query_name, Returning_cols, Augmented, Primary, Tables, Nullable_tables ) end; none -> Main_tokens = tok_strip_cte(Tokens), gleam@result:'try'( validate_compound_column_counts(Query_name, Main_tokens), fun(_) -> Main_tokens2 = tok_strip_compound(Main_tokens), Table_names = sqlode@internal@query_analyzer@token_utils:extract_table_names( Main_tokens2 ), Nullable_tables@1 = tok_extract_nullable_tables( Main_tokens2, Table_names ), case effective_tables(Table_names, Outer_tables) of [] -> infer_table_less_columns( Query_name, Main_tokens2, Augmented ); [Primary@1 | _] = Tables@1 -> Select_columns = tok_extract_select_columns( Main_tokens2 ), resolve_select_columns( Query_name, Select_columns, Augmented, Primary@1, Tables@1, Nullable_tables@1 ) end end ) end end ). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 455). ?DOC(false). -spec augment_with_subquery_tables( binary(), list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog() ) -> {ok, sqlode@internal@model:catalog()} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. augment_with_subquery_tables(Query_name, Tokens, Catalog) -> Values = extract_values_tables(Tokens), With_values = augment_catalog_with(Catalog, Values), gleam@result:'try'( extract_derived_tables(Query_name, Tokens, With_values), fun(Derived) -> With_derived = augment_catalog_with(With_values, Derived), Aliases = extract_table_aliases(Tokens, With_derived), {ok, augment_catalog_with(With_derived, Aliases)} end ). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 839). ?DOC(false). -spec extract_derived_tables( binary(), list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog() ) -> {ok, list(sqlode@internal@model:table())} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. extract_derived_tables(Query_name, Tokens, Catalog) -> find_derived_tables(Query_name, Tokens, Catalog, []). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 847). ?DOC(false). -spec find_derived_tables( binary(), list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog(), list(sqlode@internal@model:table()) ) -> {ok, list(sqlode@internal@model:table())} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. find_derived_tables(Query_name, Tokens, Catalog, Acc) -> case Tokens of [] -> {ok, lists:reverse(Acc)}; [{keyword, <<"from"/utf8>>}, l_paren, {keyword, <<"select"/utf8>>} | Rest] -> handle_derived(Query_name, Rest, Catalog, Acc); [{keyword, <<"join"/utf8>>}, {keyword, <<"lateral"/utf8>>}, l_paren, {keyword, <<"select"/utf8>>} | Rest@1] -> handle_derived(Query_name, Rest@1, Catalog, Acc); [{keyword, <<"join"/utf8>>}, l_paren, {keyword, <<"select"/utf8>>} | Rest@2] -> handle_derived(Query_name, Rest@2, Catalog, Acc); [{keyword, <<"lateral"/utf8>>}, l_paren, {keyword, <<"select"/utf8>>} | Rest@3] -> handle_derived(Query_name, Rest@3, Catalog, Acc); [_ | Rest@4] -> find_derived_tables(Query_name, Rest@4, Catalog, Acc) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 506). ?DOC(false). -spec parse_cte_defs( binary(), list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog(), list(sqlode@internal@model:table()) ) -> {ok, list(sqlode@internal@model:table())} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. parse_cte_defs(Query_name, Tokens, Catalog, Acc) -> case Tokens of [{ident, Name} | Rest_after_name] -> {Explicit_names, After_optional_cols} = case Rest_after_name of [l_paren | After_lp] -> {Col_tokens, After_cols} = sqlode@internal@query_analyzer@token_utils:collect_paren_contents( After_lp ), Names = parse_cte_column_list(Col_tokens), {Names, After_cols}; _ -> {[], Rest_after_name} end, case After_optional_cols of [{keyword, <<"as"/utf8>>}, l_paren | After_as_lp] -> {Body, After_body} = sqlode@internal@query_analyzer@token_utils:collect_paren_contents( After_as_lp ), Augmented = augment_catalog_with(Catalog, Acc), gleam@result:'try'( infer_columns_from_tokens(Query_name, Body, Augmented), fun(Cols) -> Body_columns = gleam@list:filter_map( Cols, fun(Item) -> case Item of {scalar_result, Rc} -> {ok, {column, erlang:element(2, Rc), erlang:element(3, Rc), erlang:element(4, Rc)}}; _ -> {error, nil} end end ), Columns = apply_explicit_column_names( Body_columns, Explicit_names ), Table = {table, string:lowercase(Name), Columns}, New_acc = [Table | Acc], case After_body of [comma | More] -> parse_cte_defs( Query_name, More, Catalog, New_acc ); _ -> {ok, lists:reverse(New_acc)} end end ); _ -> {ok, lists:reverse(Acc)} end; _ -> {ok, lists:reverse(Acc)} end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 492). ?DOC(false). -spec extract_cte_tables( binary(), list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog() ) -> {ok, list(sqlode@internal@model:table())} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. extract_cte_tables(Query_name, Tokens, Catalog) -> case Tokens of [{keyword, <<"with"/utf8>>}, {keyword, <<"recursive"/utf8>>} | Rest] -> parse_cte_defs(Query_name, Rest, Catalog, []); [{keyword, <<"with"/utf8>>} | Rest@1] -> parse_cte_defs(Query_name, Rest@1, Catalog, []); _ -> {ok, []} end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 973). ?DOC(false). -spec extract_cte_tables_from_stmt( binary(), sqlode@internal@query_ir:stmt(), list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog() ) -> {ok, list(sqlode@internal@model:table())} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. extract_cte_tables_from_stmt(Query_name, Stmt, Tokens, Catalog) -> case stmt_ctes(Stmt) of [] -> {ok, []}; _ -> extract_cte_tables(Query_name, Tokens, Catalog) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1051). ?DOC(false). -spec extract_derived_tables_from_stmt( binary(), sqlode@internal@query_ir:stmt(), list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog() ) -> {ok, list(sqlode@internal@model:table())} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. extract_derived_tables_from_stmt(Query_name, Stmt, Tokens, Catalog) -> case gleam@list:any(collect_from_items(Stmt), fun is_from_subquery/1) of false -> {ok, []}; true -> extract_derived_tables(Query_name, Tokens, Catalog) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1446). ?DOC(false). -spec infer_expression_type_token_path( list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog(), list(binary()), binary() ) -> {ok, {sqlode@internal@model:scalar_type(), boolean()}} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. infer_expression_type_token_path(Tokens, Catalog, Table_names, Query_name) -> case Tokens of [{keyword, <<"cast"/utf8>>}, l_paren | Rest] -> {Inner_tokens, _} = sqlode@internal@query_analyzer@token_utils:collect_paren_contents( Rest ), case tok_find_as_type(Inner_tokens) of {some, Type_name} -> case sqlode@internal@model:parse_sql_type(Type_name) of {ok, Scalar_type} -> {ok, {Scalar_type, true}}; {error, _} -> {error, {unsupported_expression, Query_name, tok_tokens_to_text(Tokens)}} end; none -> {error, {unsupported_expression, Query_name, tok_tokens_to_text(Tokens)}} end; [{keyword, <<"exists"/utf8>>}, l_paren | _] -> {ok, {bool_type, false}}; [l_paren, {keyword, <<"select"/utf8>>} | Rest@1] -> {Inner, _} = sqlode@internal@query_analyzer@token_utils:collect_paren_contents( [{keyword, <<"select"/utf8>>} | Rest@1] ), case infer_columns_from_tokens_scoped( Query_name, Inner, Catalog, Table_names ) of {ok, [{scalar_result, Col} | _]} -> {ok, {erlang:element(3, Col), true}}; _ -> {error, {unsupported_expression, Query_name, tok_tokens_to_text(Tokens)}} end; [{keyword, <<"not"/utf8>>} | Rest@2] -> infer_expression_type_from_tokens( Rest@2, Catalog, Table_names, Query_name ); [{keyword, <<"case"/utf8>>} | Rest@3] -> infer_case_type_from_tokens( Rest@3, Catalog, Table_names, Query_name ); [{keyword, <<"replace"/utf8>>}, l_paren | _] -> {ok, {string_type, false}}; [{ident, Fn_name}, l_paren | Rest@4] -> Lowered = string:lowercase(Fn_name), case classify_function(Lowered) of fn_count -> {ok, {int_type, false}}; fn_avg -> {ok, {float_type, true}}; fn_aggregate_inner -> {Inner_tokens@1, _} = sqlode@internal@query_analyzer@token_utils:collect_paren_contents( Rest@4 ), First_arg = tok_first_comma_group(Inner_tokens@1), case resolve_column_type_from_tokens( First_arg, Catalog, Table_names ) of {some, T} -> {ok, {T, true}}; none -> {ok, {int_type, true}} end; fn_window_int -> {ok, {int_type, false}}; fn_window_float -> {ok, {float_type, false}}; fn_window_first_arg -> {Inner_tokens@2, _} = sqlode@internal@query_analyzer@token_utils:collect_paren_contents( Rest@4 ), First_arg@1 = tok_first_comma_group(Inner_tokens@2), case resolve_column_type_from_tokens( First_arg@1, Catalog, Table_names ) of {some, T@1} -> {ok, {T@1, true}}; none -> _pipe = infer_expression_type_from_tokens( First_arg@1, Catalog, Table_names, Query_name ), gleam@result:map( _pipe, fun(Pair) -> {erlang:element(1, Pair), true} end ) end; fn_coalesce -> {Inner_tokens@3, _} = sqlode@internal@query_analyzer@token_utils:collect_paren_contents( Rest@4 ), First_arg@2 = tok_first_comma_group(Inner_tokens@3), case resolve_column_type_from_tokens( First_arg@2, Catalog, Table_names ) of {some, T@2} -> {ok, {T@2, false}}; none -> _pipe@1 = infer_expression_type_from_tokens( First_arg@2, Catalog, Table_names, Query_name ), gleam@result:map( _pipe@1, fun(Pair@1) -> {erlang:element(1, Pair@1), false} end ) end; fn_greatest_least -> {Inner_tokens@4, _} = sqlode@internal@query_analyzer@token_utils:collect_paren_contents( Rest@4 ), First_arg@3 = tok_first_comma_group(Inner_tokens@4), case resolve_column_type_from_tokens( First_arg@3, Catalog, Table_names ) of {some, T@3} -> {ok, {T@3, true}}; none -> _pipe@2 = infer_expression_type_from_tokens( First_arg@3, Catalog, Table_names, Query_name ), gleam@result:map( _pipe@2, fun(Pair@2) -> {erlang:element(1, Pair@2), true} end ) end; fn_string -> {ok, {string_type, false}}; fn_length -> {ok, {int_type, false}}; fn_math -> {Inner_tokens@5, _} = sqlode@internal@query_analyzer@token_utils:collect_paren_contents( Rest@4 ), First_arg@4 = tok_first_comma_group(Inner_tokens@5), case resolve_column_type_from_tokens( First_arg@4, Catalog, Table_names ) of {some, T@4} -> {ok, {T@4, false}}; none -> {ok, {float_type, false}} end; fn_datetime -> {ok, {infer_datetime_return_type(Lowered), false}}; fn_nullif -> {Inner_tokens@6, _} = sqlode@internal@query_analyzer@token_utils:collect_paren_contents( Rest@4 ), First_arg@5 = tok_first_comma_group(Inner_tokens@6), case resolve_column_type_from_tokens( First_arg@5, Catalog, Table_names ) of {some, T@5} -> {ok, {T@5, true}}; none -> _pipe@3 = infer_expression_type_from_tokens( First_arg@5, Catalog, Table_names, Query_name ), gleam@result:map( _pipe@3, fun(Pair@3) -> {erlang:element(1, Pair@3), true} end ) end; fn_unknown -> infer_by_scanning(Tokens, Catalog, Table_names, Query_name) end; [{ident, Fn_name@1}] -> Lowered@1 = string:lowercase(Fn_name@1), case is_datetime_identifier(Lowered@1) of true -> {ok, {infer_datetime_return_type(Lowered@1), false}}; false -> {error, {unsupported_expression, Query_name, tok_tokens_to_text(Tokens)}} end; [{string_lit, _}] -> {ok, {string_type, false}}; [{number_lit, N}] -> case gleam_stdlib:contains_string(N, <<"."/utf8>>) of true -> {ok, {float_type, false}}; false -> {ok, {int_type, false}} end; [{keyword, <<"true"/utf8>>}] -> {ok, {bool_type, false}}; [{keyword, <<"false"/utf8>>}] -> {ok, {bool_type, false}}; [{keyword, <<"null"/utf8>>}] -> {error, {unsupported_expression, Query_name, tok_tokens_to_text(Tokens)}}; _ -> infer_by_scanning(Tokens, Catalog, Table_names, Query_name) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1798). ?DOC(false). -spec infer_by_scanning( list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog(), list(binary()), binary() ) -> {ok, {sqlode@internal@model:scalar_type(), boolean()}} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. infer_by_scanning(Tokens, Catalog, Table_names, Query_name) -> case find_top_level_pattern(Tokens) of pat_bool -> {ok, {bool_type, false}}; pat_concat -> {ok, {string_type, false}}; pat_arithmetic -> infer_arithmetic_from_tokens( Tokens, Catalog, Table_names, Query_name ); pat_json -> {ok, {json_type, true}}; pat_json_text -> {ok, {string_type, true}}; pat_none -> {error, {unsupported_expression, Query_name, tok_tokens_to_text(Tokens)}} end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1825). ?DOC(false). -spec infer_arithmetic_from_tokens( list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog(), list(binary()), binary() ) -> {ok, {sqlode@internal@model:scalar_type(), boolean()}} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. infer_arithmetic_from_tokens(Tokens, Catalog, Table_names, Query_name) -> Operands = split_arithmetic_operands(Tokens), gleam@result:'try'( gleam@list:try_map( Operands, fun(Operand_tokens) -> infer_atom_type( Operand_tokens, Catalog, Table_names, Query_name ) end ), fun(Atom_types) -> case unify_types_nullable(Atom_types) of {ok, {some, Result_type}} -> {ok, Result_type}; {ok, none} -> {error, {unsupported_expression, Query_name, tok_tokens_to_text(Tokens)}}; {error, nil} -> {error, {unsupported_expression, Query_name, tok_tokens_to_text(Tokens)}} end end ). -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1912). ?DOC(false). -spec infer_atom_type( list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog(), list(binary()), binary() ) -> {ok, {gleam@option:option(sqlode@internal@model:scalar_type()), boolean()}} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. infer_atom_type(Tokens, Catalog, Table_names, Query_name) -> case Tokens of [{keyword, <<"null"/utf8>>}] -> {ok, {none, true}}; _ -> case resolve_column_type_nullable_from_tokens( Tokens, Catalog, Table_names ) of {some, {T, Nullable}} -> {ok, {{some, T}, Nullable}}; none -> _pipe = infer_expression_type_from_tokens( Tokens, Catalog, Table_names, Query_name ), gleam@result:map( _pipe, fun(Pair) -> {{some, erlang:element(1, Pair)}, erlang:element(2, Pair)} end ) end end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1417). ?DOC(false). -spec infer_expression_type_from_tokens( list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog(), list(binary()), binary() ) -> {ok, {sqlode@internal@model:scalar_type(), boolean()}} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. infer_expression_type_from_tokens(Tokens, Catalog, Table_names, Query_name) -> Ir_expr = sqlode@internal@query_analyzer@expr_parser:parse_expr( Tokens, postgre_s_q_l ), case Ir_expr of {raw_expr, _, _} -> infer_expression_type_token_path( Tokens, Catalog, Table_names, Query_name ); _ -> Scope = sqlode@internal@query_analyzer@type_inference:scope( Query_name, Catalog, Table_names, [] ), case sqlode@internal@query_analyzer@type_inference:infer_expr_type( Scope, Ir_expr ) of {ok, {inferred_type, T, N}} -> {ok, {T, N}}; {error, _} -> infer_expression_type_token_path( Tokens, Catalog, Table_names, Query_name ) end end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 389). ?DOC(false). -spec infer_table_less_columns( binary(), list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog() ) -> {ok, list(sqlode@internal@model:result_item())} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. infer_table_less_columns(Query_name, Tokens, Catalog) -> Select_columns = tok_extract_select_columns(Tokens), case Select_columns of [] -> {ok, []}; _ -> gleam@list:try_map( Select_columns, fun(Col) -> case erlang:element(5, Col) of {some, Expr_tokens} -> gleam@result:'try'( infer_expression_type_from_tokens( Expr_tokens, Catalog, [], Query_name ), fun(_use0) -> {Scalar, Nullable} = _use0, {ok, {scalar_result, {result_column, erlang:element(2, Col), Scalar, Nullable, none}}} end ); none -> {error, {unsupported_expression, Query_name, <<"unresolved column without FROM clause: "/utf8, (erlang:element(2, Col))/binary>>}} end end ) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 1223). ?DOC(false). -spec resolve_select_columns( binary(), list(extracted_column()), sqlode@internal@model:catalog(), binary(), list(binary()), list(binary()) ) -> {ok, list(sqlode@internal@model:result_item())} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. resolve_select_columns( Query_name, Columns, Catalog, Primary_table, All_tables, Nullable_tables ) -> case Columns of [{extracted_column, <<"*"/utf8>>, _, _, _}] -> Result_columns = begin _pipe = All_tables, gleam@list:flat_map( _pipe, fun(Table_name) -> case gleam@list:find( erlang:element(2, Catalog), fun(T) -> erlang:element(2, T) =:= Table_name end ) of {ok, Table} -> gleam@list:map( erlang:element(3, Table), fun(Col) -> {scalar_result, {result_column, erlang:element(2, Col), erlang:element(3, Col), erlang:element(4, Col) orelse gleam@list:contains( Nullable_tables, Table_name ), {some, Table_name}}} end ); {error, _} -> [] end end ) end, case Result_columns of [] -> {error, {table_not_found, Query_name, Primary_table}}; _ -> {ok, Result_columns} end; _ -> _pipe@5 = gleam@list:try_map( Columns, fun(Extracted) -> Trimmed = gleam@string:trim(erlang:element(2, Extracted)), Lowered = string:lowercase(Trimmed), case gleam_stdlib:string_starts_with( Lowered, <<"sqlode.embed("/utf8>> ) of true -> Embed_name = begin _pipe@1 = Lowered, _pipe@2 = gleam@string:replace( _pipe@1, <<"sqlode.embed("/utf8>>, <<""/utf8>> ), _pipe@3 = gleam@string:replace( _pipe@2, <<")"/utf8>>, <<""/utf8>> ), gleam@string:trim(_pipe@3) end, case begin _pipe@4 = erlang:element(2, Catalog), gleam@list:find( _pipe@4, fun(Table@1) -> erlang:element(2, Table@1) =:= Embed_name end ) end of {ok, Table@2} -> {ok, [{embedded_result, {embedded_column, Embed_name, Embed_name, erlang:element(3, Table@2)}}]}; {error, _} -> {error, {table_not_found, Query_name, Embed_name}} end; false -> Normalized_name = string:lowercase(Trimmed), Qualified_lookup_column = case erlang:element( 5, Extracted ) of {some, [{ident, _}, dot, {ident, C}]} -> {some, C}; _ -> none end, case erlang:element(3, Extracted) of {some, Table@3} -> Catalog_column = case sqlode@internal@query_analyzer@context:find_column( Catalog, Table@3, Normalized_name ) of {some, C@1} -> {some, C@1}; none -> case Qualified_lookup_column of {some, Name} -> sqlode@internal@query_analyzer@context:find_column( Catalog, Table@3, Name ); none -> none end end, case Catalog_column of {some, Column} -> {ok, [{scalar_result, {result_column, Normalized_name, erlang:element( 3, Column ), erlang:element( 4, Column ) orelse gleam@list:contains( Nullable_tables, Table@3 ), {some, Table@3}}}]}; none -> case erlang:element(5, Extracted) of {some, Expr_tokens} -> gleam@result:'try'( infer_expression_type_from_tokens( Expr_tokens, Catalog, All_tables, Query_name ), fun(_use0) -> {Scalar_type, Nullable} = _use0, {ok, [{scalar_result, {result_column, Normalized_name, Scalar_type, Nullable, none}}]} end ); none -> {error, {column_not_found, Query_name, Table@3, Normalized_name}} end end; none -> case sqlode@internal@query_analyzer@context:find_column_in_tables( Catalog, All_tables, Normalized_name ) of {ok, {some, {Found_table, Column@1}}} -> {ok, [{scalar_result, {result_column, erlang:element( 2, Column@1 ), erlang:element( 3, Column@1 ), erlang:element( 4, Column@1 ) orelse gleam@list:contains( Nullable_tables, Found_table ), {some, Found_table}}}]}; {error, Matching_tables} -> {error, {ambiguous_column_name, Query_name, Normalized_name, Matching_tables}}; {ok, none} -> case erlang:element(5, Extracted) of {some, Expr_tokens@1} -> gleam@result:'try'( infer_expression_type_from_tokens( Expr_tokens@1, Catalog, All_tables, Query_name ), fun(_use0@1) -> {Scalar_type@1, Nullable@1} = _use0@1, {ok, [{scalar_result, {result_column, Normalized_name, Scalar_type@1, Nullable@1, none}}]} end ); none -> {error, {column_not_found, Query_name, Primary_table, Normalized_name}} end end end end end ), gleam@result:map(_pipe@5, fun lists:append/1) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 51). ?DOC(false). -spec infer_columns_from_ir( binary(), list(sqlode@internal@lexer:token()), sqlode@internal@query_ir:sql_statement(), sqlode@internal@model:catalog() ) -> {ok, list(sqlode@internal@model:result_item())} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. infer_columns_from_ir(Query_name, Tokens, Statement, Catalog) -> case Statement of {select_statement, Items, From_items, Join_clauses, _, _, _, _, _} -> case has_compound_keyword(Tokens) of true -> infer_columns_from_tokens(Query_name, Tokens, Catalog); false -> Ir_tables = extract_ir_table_names(From_items, Join_clauses), case Ir_tables of [] -> infer_columns_from_tokens( Query_name, Tokens, Catalog ); _ -> case has_embed_items(Items) of true -> infer_columns_from_tokens( Query_name, Tokens, Catalog ); false -> gleam@result:'try'( augment_with_subquery_tables( Query_name, Tokens, Catalog ), fun(Augmented) -> Nullable_tables = tok_extract_nullable_tables( Tokens, Ir_tables ), Select_columns = ir_select_items_to_extracted( Items, Ir_tables ), case Select_columns of [] -> infer_columns_from_tokens( Query_name, Tokens, Catalog ); _ -> resolve_select_columns( Query_name, Select_columns, Augmented, case Ir_tables of [First | _] -> First; [] -> <<""/utf8>> end, Ir_tables, Nullable_tables ) end end ) end end end; _ -> infer_columns_from_tokens(Query_name, Tokens, Catalog) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 26). ?DOC(false). -spec infer_result_columns( sqlode@internal@query_analyzer@context:analyzer_context(), sqlode@internal@model:engine(), sqlode@internal@model:parsed_query(), list(sqlode@internal@lexer:token()), sqlode@internal@query_ir:sql_statement(), sqlode@internal@model:catalog() ) -> {ok, list(sqlode@internal@model:result_item())} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. infer_result_columns(_, _, Query, Tokens, Statement, Catalog) -> case erlang:element(4, Query) of exec -> {ok, []}; exec_result -> {ok, []}; exec_rows -> {ok, []}; exec_last_id -> {ok, []}; batch_exec -> {ok, []}; copy_from -> {ok, []}; one -> infer_columns_from_ir( erlang:element(2, Query), Tokens, Statement, Catalog ); many -> infer_columns_from_ir( erlang:element(2, Query), Tokens, Statement, Catalog ); batch_one -> infer_columns_from_ir( erlang:element(2, Query), Tokens, Statement, Catalog ); batch_many -> infer_columns_from_ir( erlang:element(2, Query), Tokens, Statement, Catalog ) end. -file("src/sqlode/internal/query_analyzer/column_inferencer.gleam", 2142). ?DOC(false). -spec infer_case_type_from_tokens( list(sqlode@internal@lexer:token()), sqlode@internal@model:catalog(), list(binary()), binary() ) -> {ok, {sqlode@internal@model:scalar_type(), boolean()}} | {error, sqlode@internal@query_analyzer@context:analysis_error()}. infer_case_type_from_tokens(Tokens, Catalog, Table_names, Query_name) -> {Then_branches, Else_branch} = collect_case_branches(Tokens), case Then_branches of [] -> {error, {unsupported_expression, Query_name, tok_tokens_to_text(Tokens)}}; _ -> Branch_tokens = case Else_branch of {some, E} -> lists:append(Then_branches, [E]); none -> Then_branches end, gleam@result:'try'( gleam@list:try_map( Branch_tokens, fun(Branch) -> infer_atom_type( Branch, Catalog, Table_names, Query_name ) end ), fun(Atom_types) -> case unify_types_nullable(Atom_types) of {ok, {some, {Scalar_type, Branches_nullable}}} -> Nullable = case Else_branch of {some, _} -> Branches_nullable; none -> true end, {ok, {Scalar_type, Nullable}}; {ok, none} -> {error, {unsupported_expression, Query_name, tok_tokens_to_text(Tokens)}}; {error, nil} -> {error, {unsupported_expression, Query_name, tok_tokens_to_text(Tokens)}} end end ) end.