Current section

Files

Jump to
sqlode src sqlode@query_analyzer.erl
Raw

src/sqlode@query_analyzer.erl

-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/1, 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", 19).
-spec analysis_error_to_string(sqlode@query_analyzer@context:analysis_error()) -> binary().
analysis_error_to_string(Error) ->
sqlode@query_analyzer@context:analysis_error_to_string(Error).
-file("src/sqlode/query_analyzer.gleam", 87).
?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", 111).
-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", 223).
-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", 231).
-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", 235).
-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", 125).
-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) ->
Inferences = begin
_pipe = lists:append(
sqlode@query_analyzer@param_inferencer:infer_insert_params_from_ir(
Engine,
Statement,
Catalog
),
sqlode@query_analyzer@param_inferencer:infer_equality_params(
Ctx,
Engine,
Tokens,
Catalog
)
),
lists:append(
_pipe,
sqlode@query_analyzer@param_inferencer:infer_in_params(
Ctx,
Engine,
Tokens,
Catalog
)
)
end,
gleam@result:'try'(
begin
_pipe@1 = sqlode@query_analyzer@param_inferencer:extract_type_casts(
Ctx,
Engine,
Tokens
),
gleam@result:map_error(
_pipe@1,
fun(Err) ->
{Index, Cast_type} = Err,
{unrecognized_cast_type,
erlang:element(2, Query),
Index,
Cast_type}
end
)
end,
fun(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@2 = sqlode@query_analyzer@placeholder:unique(
Occurrences
),
gleam@list:try_map(
_pipe@2,
fun(Occurrence) ->
Macro_info = begin
_pipe@3 = gleam_stdlib:map_get(
Macro_dict,
erlang:element(2, Occurrence)
),
gleam@option:from_result(_pipe@3)
end,
Inferred = begin
_pipe@4 = gleam_stdlib:map_get(
Inference_dict,
erlang:element(2, Occurrence)
),
gleam@option:from_result(_pipe@4)
end,
Cast_type@1 = begin
_pipe@5 = gleam_stdlib:map_get(
Cast_dict,
erlang:element(2, Occurrence)
),
gleam@option:from_result(_pipe@5)
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, Is_list} = case Macro_info of
{some, {macro_arg, _, Name}} ->
N = 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,
N,
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,
Is_list}}
end)
end
)
end
)
end
).
-file("src/sqlode/query_analyzer.gleam", 33).
-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),
gleam@result:'try'(
sqlode@query_analyzer@column_inferencer:extract_cte_tables(
erlang:element(2, Query),
Tokens,
Catalog
),
fun(Cte_tables) ->
With_ctes = merge_virtual_tables(Catalog, Cte_tables),
Values_tables = sqlode@query_analyzer@column_inferencer:extract_values_tables(
Tokens
),
With_values = merge_virtual_tables(With_ctes, Values_tables),
gleam@result:'try'(
sqlode@query_analyzer@column_inferencer:extract_derived_tables(
erlang:element(2, Query),
Tokens,
With_values
),
fun(Derived_tables) ->
Augmented = merge_virtual_tables(
With_values,
Derived_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", 23).
-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
).