Current section

Files

Jump to
sqlode src sqlode@query_validation.erl
Raw

src/sqlode@query_validation.erl

-module(sqlode@query_validation).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/sqlode/query_validation.gleam").
-export([validate_no_duplicate_names/1, validate_unsupported_annotations/1, validate_array_engine_support/2, validate_native_annotations/1, error_to_string/1]).
-export_type([validation_error/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(
" Post-parse query validation shared by `generate` and `verify`.\n"
"\n"
" These checks reject configurations that would otherwise reach\n"
" the codegen stage in a state the generator cannot express\n"
" safely. Centralising them here means `verify` and `generate`\n"
" reach the same verdict for any given config: `verify` can be\n"
" used as a trustworthy pre-generation CI gate, and the checks\n"
" cannot drift apart again by accident.\n"
).
-type validation_error() :: {duplicate_name, binary(), list(binary())} |
{normalized_name_collision, binary(), list(binary()), list(binary())} |
{unsupported_annotation, binary(), binary(), binary()} |
{unsupported_array_for_engine, binary(), binary()}.
-file("src/sqlode/query_validation.gleam", 64).
-spec find_duplicate_group(
list(sqlode@query_ir:tokenized_query()),
fun((sqlode@query_ir:tokenized_query()) -> binary())
) -> {ok, {binary(), list(sqlode@query_ir:tokenized_query())}} | {error, nil}.
find_duplicate_group(Queries, Key) ->
_pipe = gleam@list:group(Queries, Key),
_pipe@1 = maps:to_list(_pipe),
gleam@list:find(
_pipe@1,
fun(Entry) -> erlang:length(erlang:element(2, Entry)) > 1 end
).
-file("src/sqlode/query_validation.gleam", 39).
?DOC(
" Reject two tokenized queries that declare the same annotation\n"
" name, or whose names normalize to the same Gleam identifier\n"
" (the snake_case `function_name` that queries/params/decoders\n"
" are derived from). Either shape would leave the generator\n"
" emitting duplicate declarations and fail the Gleam compile.\n"
" Literal duplicates are reported first so operators see the\n"
" simpler diagnostic when both cases hit at once.\n"
).
-spec validate_no_duplicate_names(list(sqlode@query_ir:tokenized_query())) -> {ok,
nil} |
{error, validation_error()}.
validate_no_duplicate_names(Queries) ->
case find_duplicate_group(
Queries,
fun(Q) -> erlang:element(2, erlang:element(2, Q)) end
) of
{ok, {Name, Dupes}} ->
Paths = begin
_pipe = Dupes,
_pipe@1 = gleam@list:map(
_pipe,
fun(Q@1) -> erlang:element(6, erlang:element(2, Q@1)) end
),
gleam@list:unique(_pipe@1)
end,
{error, {duplicate_name, Name, Paths}};
{error, nil} ->
case find_duplicate_group(
Queries,
fun(Q@2) -> erlang:element(3, erlang:element(2, Q@2)) end
) of
{ok, {Function_name, Colliding}} ->
Names = begin
_pipe@2 = Colliding,
_pipe@3 = gleam@list:map(
_pipe@2,
fun(Q@3) ->
erlang:element(2, erlang:element(2, Q@3))
end
),
gleam@list:unique(_pipe@3)
end,
Paths@1 = begin
_pipe@4 = Colliding,
_pipe@5 = gleam@list:map(
_pipe@4,
fun(Q@4) ->
erlang:element(6, erlang:element(2, Q@4))
end
),
gleam@list:unique(_pipe@5)
end,
{error,
{normalized_name_collision,
Function_name,
Names,
Paths@1}};
{error, nil} ->
{ok, nil}
end
end.
-file("src/sqlode/query_validation.gleam", 77).
?DOC(
" Reject annotations that are not yet supported by any codegen\n"
" path (`:batchone`, `:batchmany`, `:batchexec`, `:copyfrom`).\n"
" The failure names a supported alternative so operators can fix\n"
" the annotation without digging through docs.\n"
).
-spec validate_unsupported_annotations(list(sqlode@model:analyzed_query())) -> {ok,
nil} |
{error, validation_error()}.
validate_unsupported_annotations(Queries) ->
Unsupported = fun(Command) -> case Command of
query_batch_one ->
true;
query_batch_many ->
true;
query_batch_exec ->
true;
query_copy_from ->
true;
_ ->
false
end end,
case gleam@list:find(
Queries,
fun(Q) -> Unsupported(erlang:element(4, erlang:element(2, Q))) end
) of
{ok, Q@1} ->
{Command@1, Alternative} = case erlang:element(
4,
erlang:element(2, Q@1)
) of
query_batch_one ->
{<<":batchone"/utf8>>, <<":one"/utf8>>};
query_batch_many ->
{<<":batchmany"/utf8>>, <<":many"/utf8>>};
query_batch_exec ->
{<<":batchexec"/utf8>>, <<":exec"/utf8>>};
query_copy_from ->
{<<":copyfrom"/utf8>>, <<":exec"/utf8>>};
_ ->
{<<""/utf8>>, <<":exec"/utf8>>}
end,
{error,
{unsupported_annotation,
erlang:element(2, erlang:element(2, Q@1)),
Command@1,
<<<<<<Command@1/binary, " is not yet supported. Use "/utf8>>/binary,
Alternative/binary>>/binary,
" instead, or add '-- sqlode:skip' before the annotation to bypass this query"/utf8>>}};
{error, _} ->
{ok, nil}
end.
-file("src/sqlode/query_validation.gleam", 114).
?DOC(
" Reject array parameters for engines that cannot carry them.\n"
" Only PostgreSQL supports native array binding at the adapter\n"
" layer today.\n"
).
-spec validate_array_engine_support(
sqlode@model:engine(),
list(sqlode@model:analyzed_query())
) -> {ok, nil} | {error, validation_error()}.
validate_array_engine_support(Engine, Queries) ->
case Engine of
postgre_s_q_l ->
{ok, nil};
_ ->
Has_array = fun(Q) ->
gleam@list:any(
erlang:element(3, Q),
fun(P) -> case erlang:element(4, P) of
{array_type, _} ->
true;
_ ->
false
end end
)
end,
case gleam@list:find(Queries, Has_array) of
{ok, Q@1} ->
{error,
{unsupported_array_for_engine,
erlang:element(2, erlang:element(2, Q@1)),
sqlode@model:engine_to_string(Engine)}};
{error, _} ->
{ok, nil}
end
end.
-file("src/sqlode/query_validation.gleam", 145).
?DOC(
" Reject annotations that clash with the native runtime. Call\n"
" only when the block targets the native runtime; raw-runtime\n"
" projects can still emit `:execresult` and should not be\n"
" filtered by this check.\n"
).
-spec validate_native_annotations(list(sqlode@model:analyzed_query())) -> {ok,
nil} |
{error, validation_error()}.
validate_native_annotations(Queries) ->
case gleam@list:find(
Queries,
fun(Q) ->
erlang:element(4, erlang:element(2, Q)) =:= query_exec_result
end
) of
{ok, Q@1} ->
{error,
{unsupported_annotation,
erlang:element(2, erlang:element(2, Q@1)),
<<":execresult"/utf8>>,
<<":execresult is not supported with native runtime. Use :exec, :execrows, or :execlastid instead"/utf8>>}};
{error, _} ->
{ok, nil}
end.
-file("src/sqlode/query_validation.gleam", 185).
-spec quote_join(list(binary())) -> binary().
quote_join(Names) ->
_pipe = Names,
_pipe@1 = gleam@list:map(
_pipe,
fun(N) -> <<<<"\""/utf8, N/binary>>/binary, "\""/utf8>> end
),
gleam@string:join(_pipe@1, <<", "/utf8>>).
-file("src/sqlode/query_validation.gleam", 159).
-spec error_to_string(validation_error()) -> binary().
error_to_string(Error) ->
case Error of
{duplicate_name, Name, Paths} ->
<<<<<<"duplicate query name \""/utf8, Name/binary>>/binary,
"\" found in: "/utf8>>/binary,
(gleam@string:join(Paths, <<", "/utf8>>))/binary>>;
{normalized_name_collision, Function_name, Names, Paths@1} ->
<<<<<<<<<<<<"query names "/utf8, (quote_join(Names))/binary>>/binary,
" all normalize to the generated identifier \""/utf8>>/binary,
Function_name/binary>>/binary,
"\" (found in: "/utf8>>/binary,
(gleam@string:join(Paths@1, <<", "/utf8>>))/binary>>/binary,
"). Rename one of them so the derived function, params, row, and decoder identifiers are unique."/utf8>>;
{unsupported_annotation, Query_name, Command, Detail} ->
<<<<<<<<<<"Query "/utf8, Query_name/binary>>/binary, " uses "/utf8>>/binary,
Command/binary>>/binary,
": "/utf8>>/binary,
Detail/binary>>;
{unsupported_array_for_engine, Query_name@1, Engine} ->
<<<<<<<<"Query \""/utf8, Query_name@1/binary>>/binary,
"\": array parameters are not supported for engine \""/utf8>>/binary,
Engine/binary>>/binary,
"\". Arrays are only supported with PostgreSQL"/utf8>>
end.