Current section

Files

Jump to
sqlode src sqlode@query_parser.erl
Raw

src/sqlode@query_parser.erl

-module(sqlode@query_parser).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/sqlode/query_parser.gleam").
-export([error_to_string/1, parse_file/4]).
-export_type([parse_error/0, pending_query/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.
-type parse_error() :: {invalid_annotation, binary(), integer(), binary()} |
{missing_sql, binary(), integer(), binary()} |
{invalid_placeholder,
binary(),
integer(),
binary(),
sqlode@model:engine(),
binary()} |
{wrong_engine_upsert,
binary(),
integer(),
binary(),
sqlode@model:engine(),
binary()} |
{sparse_numbered_placeholders,
binary(),
integer(),
binary(),
list(integer())}.
-type pending_query() :: {pending_query,
binary(),
binary(),
sqlode@runtime:query_command(),
integer(),
list(binary())}.
-file("src/sqlode/query_parser.gleam", 355).
-spec extract_line_annotation(binary()) -> gleam@option:option(binary()).
extract_line_annotation(Line) ->
case gleam_stdlib:string_starts_with(Line, <<"-- name:"/utf8>>) of
false ->
none;
true ->
_pipe = Line,
_pipe@1 = gleam@string:replace(
_pipe,
<<"-- name:"/utf8>>,
<<""/utf8>>
),
_pipe@2 = gleam@string:trim(_pipe@1),
{some, _pipe@2}
end.
-file("src/sqlode/query_parser.gleam", 366).
-spec extract_block_annotation(binary()) -> gleam@option:option(binary()).
extract_block_annotation(Line) ->
case gleam_stdlib:string_starts_with(Line, <<"/*"/utf8>>) andalso gleam_stdlib:string_ends_with(
Line,
<<"*/"/utf8>>
) of
false ->
none;
true ->
Inner = begin
_pipe = Line,
_pipe@1 = gleam@string:drop_start(_pipe, 2),
gleam@string:drop_end(_pipe@1, 2)
end,
case gleam_stdlib:contains_string(Inner, <<"*/"/utf8>>) of
true ->
none;
false ->
Inner_trimmed = gleam@string:trim(Inner),
case gleam_stdlib:string_starts_with(
Inner_trimmed,
<<"name:"/utf8>>
) of
false ->
none;
true ->
_pipe@2 = Inner_trimmed,
_pipe@3 = gleam@string:replace(
_pipe@2,
<<"name:"/utf8>>,
<<""/utf8>>
),
_pipe@4 = gleam@string:trim(_pipe@3),
{some, _pipe@4}
end
end
end.
-file("src/sqlode/query_parser.gleam", 348).
?DOC(
" Extracts the annotation payload (the text after `name:` and before any\n"
" terminator) from a trimmed line. Supports both `-- name: ...` and\n"
" `/* name: ... */` forms. Returns None for non-annotation lines so callers\n"
" can treat them as SQL body.\n"
).
-spec extract_annotation_payload(binary()) -> gleam@option:option(binary()).
extract_annotation_payload(Line) ->
case extract_line_annotation(Line) of
{some, Payload} ->
{some, Payload};
none ->
extract_block_annotation(Line)
end.
-file("src/sqlode/query_parser.gleam", 291).
-spec parse_annotation(
sqlode@naming:naming_context(),
binary(),
binary(),
integer()
) -> {ok, gleam@option:option(pending_query())} | {error, parse_error()}.
parse_annotation(Naming_ctx, Line, Path, Line_number) ->
case extract_annotation_payload(Line) of
none ->
{ok, none};
{some, Rest} ->
Parts = begin
_pipe = gleam@string:split(Rest, <<" "/utf8>>),
_pipe@1 = gleam@list:map(_pipe, fun gleam@string:trim/1),
gleam@list:filter(_pipe@1, fun(Part) -> Part /= <<""/utf8>> end)
end,
case Parts of
[Name, Command_text] ->
case gleam_stdlib:string_starts_with(
Command_text,
<<":"/utf8>>
) of
true ->
gleam@result:'try'(
begin
_pipe@2 = sqlode@model:parse_query_command(
Command_text
),
gleam@result:map_error(
_pipe@2,
fun(Detail) ->
{invalid_annotation,
Path,
Line_number,
Detail}
end
)
end,
fun(Command) ->
{ok,
{some,
{pending_query,
Name,
sqlode@naming:to_snake_case(
Naming_ctx,
Name
),
Command,
Line_number,
[]}}}
end
);
false ->
{ok, none}
end;
_ ->
{ok, none}
end
end.
-file("src/sqlode/query_parser.gleam", 393).
-spec is_skip_annotation(binary()) -> boolean().
is_skip_annotation(Line) ->
Line =:= <<"-- sqlode:skip"/utf8>>.
-file("src/sqlode/query_parser.gleam", 452).
-spec is_marker_placeholder(binary()) -> boolean().
is_marker_placeholder(P) ->
gleam_stdlib:string_starts_with(P, <<"__sqlode_param_"/utf8>>) orelse gleam_stdlib:string_starts_with(
P,
<<"__sqlode_slice_"/utf8>>
).
-file("src/sqlode/query_parser.gleam", 457).
-spec extract_marker_index(binary()) -> {ok, integer()} | {error, nil}.
extract_marker_index(P) ->
Body = case gleam_stdlib:string_starts_with(P, <<"__sqlode_param_"/utf8>>) of
true ->
gleam@string:drop_start(P, 15);
false ->
case gleam_stdlib:string_starts_with(P, <<"__sqlode_slice_"/utf8>>) of
true ->
gleam@string:drop_start(P, 15);
false ->
P
end
end,
Without_suffix = case gleam_stdlib:string_ends_with(Body, <<"__"/utf8>>) of
true ->
gleam@string:drop_end(Body, 2);
false ->
Body
end,
gleam_stdlib:parse_int(Without_suffix).
-file("src/sqlode/query_parser.gleam", 404).
?DOC(
" Count parameters from an already-expanded token list.\n"
"\n"
" After macro/`@name` expansion every parameter is represented by a\n"
" `__sqlode_param_<idx>__` or `__sqlode_slice_<idx>__` marker, so the\n"
" parameter count is the highest index that appears. Raw engine\n"
" placeholders (`$1`, `?1`, bare `?`) written directly by the user are\n"
" also counted for back-compat with handwritten SQL.\n"
).
-spec count_parameters_from_tokens(
sqlode@model:engine(),
list(sqlode@lexer:token())
) -> integer().
count_parameters_from_tokens(Engine, Tokens) ->
Placeholders = begin
_pipe = Tokens,
gleam@list:filter_map(_pipe, fun(Token) -> case Token of
{placeholder, P} ->
{ok, P};
_ ->
{error, nil}
end end)
end,
{Markers, Raw} = gleam@list:partition(
Placeholders,
fun(P@1) -> is_marker_placeholder(P@1) end
),
Marker_count = begin
_pipe@1 = Markers,
_pipe@2 = gleam@list:filter_map(_pipe@1, fun extract_marker_index/1),
_pipe@3 = gleam@list:unique(_pipe@2),
erlang:length(_pipe@3)
end,
Raw_count = case Engine of
postgre_s_q_l ->
_pipe@4 = Raw,
_pipe@6 = gleam@list:filter_map(
_pipe@4,
fun(P@2) ->
_pipe@5 = gleam@string:replace(
P@2,
<<"$"/utf8>>,
<<""/utf8>>
),
gleam_stdlib:parse_int(_pipe@5)
end
),
gleam@list:fold(_pipe@6, 0, fun(Max_idx, V) -> case V > Max_idx of
true ->
V;
false ->
Max_idx
end end);
my_s_q_l ->
erlang:length(Raw);
s_q_lite ->
{Anon, Named} = gleam@list:partition(
Raw,
fun(P@3) -> P@3 =:= <<"?"/utf8>> end
),
erlang:length(Anon) + erlang:length(gleam@list:unique(Named))
end,
Marker_count + Raw_count.
-file("src/sqlode/query_parser.gleam", 258).
-spec finalize_ok(
list(sqlode@query_ir:tokenized_query()),
binary(),
binary(),
sqlode@runtime:query_command(),
binary(),
list(sqlode@lexer:token()),
list(sqlode@model:macro()),
sqlode@model:engine()
) -> list(sqlode@query_ir:tokenized_query()).
finalize_ok(
Parsed_rev,
Name,
Function_name,
Command,
Path,
Tokens,
Macros,
Engine
) ->
Expanded_sql = sqlode@lexer:tokens_to_string(
Tokens,
{token_render_options, false, true, {some, Engine}}
),
Param_count = count_parameters_from_tokens(Engine, Tokens),
Parsed = {parsed_query,
Name,
Function_name,
Command,
Expanded_sql,
Path,
Param_count,
Macros},
[{tokenized_query, Parsed, Tokens} | Parsed_rev].
-file("src/sqlode/query_parser.gleam", 524).
-spec format_indices(list(integer())) -> binary().
format_indices(Indices) ->
_pipe = Indices,
_pipe@1 = gleam@list:sort(_pipe, fun gleam@int:compare/2),
_pipe@2 = gleam@list:map(
_pipe@1,
fun(I) -> <<"?"/utf8, (erlang:integer_to_binary(I))/binary>> end
),
gleam@string:join(_pipe@2, <<", "/utf8>>).
-file("src/sqlode/query_parser.gleam", 531).
-spec upsert_hint(sqlode@model:engine()) -> binary().
upsert_hint(Engine) ->
case Engine of
postgre_s_q_l ->
<<"use `ON CONFLICT ... DO UPDATE` or `ON CONFLICT ... DO NOTHING`"/utf8>>;
s_q_lite ->
<<"use `ON CONFLICT ... DO UPDATE` or `ON CONFLICT ... DO NOTHING`"/utf8>>;
my_s_q_l ->
<<"use `ON DUPLICATE KEY UPDATE`"/utf8>>
end.
-file("src/sqlode/query_parser.gleam", 541).
-spec allowed_placeholders_hint(sqlode@model:engine()) -> binary().
allowed_placeholders_hint(Engine) ->
case Engine of
postgre_s_q_l ->
<<"PostgreSQL accepts `$N` or sqlode macros (`@name`, `sqlode.arg(name)`)"/utf8>>;
my_s_q_l ->
<<"MySQL accepts positional `?` or sqlode macros (`sqlode.arg(name)`)"/utf8>>;
s_q_lite ->
<<"SQLite accepts `?`, `?N`, `:name`, `@name`, `$name`, or sqlode macros (`sqlode.arg(name)`)"/utf8>>
end.
-file("src/sqlode/query_parser.gleam", 473).
-spec error_to_string(parse_error()) -> binary().
error_to_string(Error) ->
case Error of
{invalid_annotation, Path, Line, Detail} ->
<<<<<<<<Path/binary, ":"/utf8>>/binary,
(erlang:integer_to_binary(Line))/binary>>/binary,
": invalid query annotation: "/utf8>>/binary,
Detail/binary>>;
{missing_sql, Path@1, Line@1, Name} ->
<<<<<<<<<<Path@1/binary, ":"/utf8>>/binary,
(erlang:integer_to_binary(Line@1))/binary>>/binary,
": query "/utf8>>/binary,
Name/binary>>/binary,
" is missing SQL body"/utf8>>;
{invalid_placeholder, Path@2, Line@2, Name@1, Engine, Token} ->
<<<<<<<<<<<<<<<<<<<<Path@2/binary, ":"/utf8>>/binary,
(erlang:integer_to_binary(
Line@2
))/binary>>/binary,
": query "/utf8>>/binary,
Name@1/binary>>/binary,
": placeholder `"/utf8>>/binary,
Token/binary>>/binary,
"` is not valid for engine "/utf8>>/binary,
(sqlode@model:engine_to_string(Engine))/binary>>/binary,
"; "/utf8>>/binary,
(allowed_placeholders_hint(Engine))/binary>>;
{wrong_engine_upsert, Path@3, Line@3, Name@2, Engine@1, Tail} ->
<<<<<<<<<<<<<<<<<<<<Path@3/binary, ":"/utf8>>/binary,
(erlang:integer_to_binary(
Line@3
))/binary>>/binary,
": query "/utf8>>/binary,
Name@2/binary>>/binary,
": `"/utf8>>/binary,
Tail/binary>>/binary,
"` is not valid for engine "/utf8>>/binary,
(sqlode@model:engine_to_string(Engine@1))/binary>>/binary,
"; "/utf8>>/binary,
(upsert_hint(Engine@1))/binary>>;
{sparse_numbered_placeholders, Path@4, Line@4, Name@3, Indices} ->
<<<<<<<<<<<<<<Path@4/binary, ":"/utf8>>/binary,
(erlang:integer_to_binary(Line@4))/binary>>/binary,
": query "/utf8>>/binary,
Name@3/binary>>/binary,
": sparse SQLite numbered placeholders "/utf8>>/binary,
(format_indices(Indices))/binary>>/binary,
"; numbered placeholders must form a contiguous set starting from ?1 (e.g. ?1, ?2, ?3)"/utf8>>
end.
-file("src/sqlode/query_parser.gleam", 598).
-spec is_positive_int(binary()) -> boolean().
is_positive_int(S) ->
case gleam_stdlib:parse_int(S) of
{ok, N} when N > 0 ->
true;
_ ->
false
end.
-file("src/sqlode/query_parser.gleam", 579).
-spec is_dollar_numbered(binary()) -> boolean().
is_dollar_numbered(P) ->
case gleam_stdlib:string_starts_with(P, <<"$"/utf8>>) of
false ->
false;
true ->
is_positive_int(gleam@string:drop_start(P, 1))
end.
-file("src/sqlode/query_parser.gleam", 586).
-spec is_sqlite_placeholder_syntax(binary()) -> boolean().
is_sqlite_placeholder_syntax(P) ->
case P of
<<"?"/utf8>> ->
true;
_ ->
case gleam@string:first(P) of
{ok, <<"?"/utf8>>} ->
is_positive_int(gleam@string:drop_start(P, 1));
{ok, <<":"/utf8>>} ->
string:length(P) > 1;
{ok, <<"@"/utf8>>} ->
string:length(P) > 1;
{ok, <<"$"/utf8>>} ->
string:length(P) > 1;
_ ->
false
end
end.
-file("src/sqlode/query_parser.gleam", 571).
-spec is_valid_raw_placeholder(sqlode@model:engine(), binary()) -> boolean().
is_valid_raw_placeholder(Engine, P) ->
case Engine of
postgre_s_q_l ->
is_dollar_numbered(P);
my_s_q_l ->
P =:= <<"?"/utf8>>;
s_q_lite ->
is_sqlite_placeholder_syntax(P)
end.
-file("src/sqlode/query_parser.gleam", 556).
?DOC(
" Validate that every raw placeholder token matches the configured engine.\n"
" Sqlode markers (`__sqlode_param_*` / `__sqlode_slice_*`) are always\n"
" accepted because they are produced by macro expansion and are rewritten\n"
" to engine-specific placeholders at prepare-time.\n"
).
-spec validate_placeholder_syntax(
sqlode@model:engine(),
list(sqlode@lexer:token())
) -> {ok, nil} | {error, binary()}.
validate_placeholder_syntax(Engine, Tokens) ->
case Tokens of
[] ->
{ok, nil};
[{placeholder, P} | Rest] ->
case is_marker_placeholder(P) orelse is_valid_raw_placeholder(
Engine,
P
) of
true ->
validate_placeholder_syntax(Engine, Rest);
false ->
{error, P}
end;
[_ | Rest@1] ->
validate_placeholder_syntax(Engine, Rest@1)
end.
-file("src/sqlode/query_parser.gleam", 638).
-spec extract_sqlite_numbered_index(sqlode@lexer:token()) -> {ok, integer()} |
{error, nil}.
extract_sqlite_numbered_index(Token) ->
case Token of
{placeholder, P} ->
case {gleam@string:first(P), string:length(P) > 1} of
{{ok, <<"?"/utf8>>}, true} ->
gleam_stdlib:parse_int(gleam@string:drop_start(P, 1));
{_, _} ->
{error, nil}
end;
_ ->
{error, nil}
end.
-file("src/sqlode/query_parser.gleam", 613).
?DOC(
" Reject sparse SQLite numbered placeholders.\n"
"\n"
" SQLite accepts `?N` with explicit indices, but sqlode's parameter count\n"
" and runtime expansion assume the declared indices form a contiguous set\n"
" starting from 1. A query that uses `?2` alone, or `?1` and `?3` with no\n"
" `?2`, passes lexing but yields generated metadata that does not match the\n"
" SQL text. Flag the mismatch at parse time so users can either renumber\n"
" their placeholders or switch to the bare `?` / `?1, ?2, ...` forms.\n"
).
-spec validate_sqlite_numbered_placeholders(
sqlode@model:engine(),
list(sqlode@lexer:token())
) -> {ok, nil} | {error, list(integer())}.
validate_sqlite_numbered_placeholders(Engine, Tokens) ->
case Engine of
s_q_lite ->
Indices = begin
_pipe = Tokens,
_pipe@1 = gleam@list:filter_map(
_pipe,
fun extract_sqlite_numbered_index/1
),
gleam@list:unique(_pipe@1)
end,
case Indices of
[] ->
{ok, nil};
_ ->
Max_idx = gleam@list:fold(Indices, 0, fun gleam@int:max/2),
case Max_idx =:= erlang:length(Indices) of
true ->
{ok, nil};
false ->
{error, Indices}
end
end;
_ ->
{ok, nil}
end.
-file("src/sqlode/query_parser.gleam", 654).
?DOC(
" Scan tokens for UPSERT tails that do not belong to the configured engine.\n"
" MySQL uses `ON DUPLICATE KEY UPDATE`; PostgreSQL and SQLite use `ON\n"
" CONFLICT ... DO UPDATE/NOTHING`. Accepting the wrong form silently leaks\n"
" dialect-incompatible SQL into generated code, so reject it early with a\n"
" diagnostic that names the offending tail.\n"
).
-spec validate_upsert_tails(sqlode@model:engine(), list(sqlode@lexer:token())) -> {ok,
nil} |
{error, binary()}.
validate_upsert_tails(Engine, Tokens) ->
case Tokens of
[] ->
{ok, nil};
[{keyword, <<"on"/utf8>>}, {ident, Word} | Rest] ->
case {string:lowercase(Word), Engine} of
{<<"duplicate"/utf8>>, my_s_q_l} ->
validate_upsert_tails(Engine, Rest);
{<<"duplicate"/utf8>>, _} ->
{error, <<"ON DUPLICATE KEY UPDATE"/utf8>>};
{_, _} ->
validate_upsert_tails(Engine, Rest)
end;
[{keyword, <<"on"/utf8>>}, {keyword, <<"conflict"/utf8>>} | Rest@1] ->
case Engine of
my_s_q_l ->
{error, <<"ON CONFLICT"/utf8>>};
_ ->
validate_upsert_tails(Engine, Rest@1)
end;
[_ | Rest@2] ->
validate_upsert_tails(Engine, Rest@2)
end.
-file("src/sqlode/query_parser.gleam", 810).
?DOC(" Collect tokens inside macro parens until the matching RParen.\n").
-spec collect_macro_arg_tokens(
list(sqlode@lexer:token()),
integer(),
list(sqlode@lexer:token())
) -> {list(sqlode@lexer:token()), list(sqlode@lexer:token())}.
collect_macro_arg_tokens(Tokens, Depth, Acc) ->
case Depth =< 0 of
true ->
{lists:reverse(Acc), Tokens};
false ->
case Tokens of
[] ->
{lists:reverse(Acc), []};
[l_paren | Rest] ->
collect_macro_arg_tokens(Rest, Depth + 1, [l_paren | Acc]);
[r_paren | Rest@1] ->
case Depth =:= 1 of
true ->
{lists:reverse(Acc), Rest@1};
false ->
collect_macro_arg_tokens(
Rest@1,
Depth - 1,
[r_paren | Acc]
)
end;
[Token | Rest@2] ->
collect_macro_arg_tokens(Rest@2, Depth, [Token | Acc])
end
end.
-file("src/sqlode/query_parser.gleam", 835).
?DOC(
" Extract the argument name from macro arg tokens.\n"
" Handles: Ident(name), StringLit(name), QuotedIdent(name).\n"
).
-spec extract_arg_name(list(sqlode@lexer:token())) -> binary().
extract_arg_name(Tokens) ->
case Tokens of
[{ident, Name}] ->
Name;
[{string_lit, Name@1}] ->
Name@1;
[{quoted_ident, Name@2}] ->
Name@2;
_ ->
_pipe = Tokens,
_pipe@1 = gleam@list:filter_map(_pipe, fun(T) -> case T of
{ident, N} ->
{ok, N};
{string_lit, N@1} ->
{ok, N@1};
{quoted_ident, N@2} ->
{ok, N@2};
_ ->
{error, nil}
end end),
gleam@string:join(_pipe@1, <<""/utf8>>)
end.
-file("src/sqlode/query_parser.gleam", 855).
-spec engine_placeholder(sqlode@model:engine(), integer()) -> binary().
engine_placeholder(_, Index) ->
sqlode@runtime:param_marker(Index).
-file("src/sqlode/query_parser.gleam", 695).
-spec expand_at_loop(
sqlode@model:engine(),
list(sqlode@lexer:token()),
integer(),
gleam@dict:dict(binary(), integer()),
list(sqlode@lexer:token()),
list(sqlode@model:macro())
) -> {list(sqlode@lexer:token()), list(sqlode@model:macro()), integer()}.
expand_at_loop(Engine, Tokens, Idx, Seen, Token_acc, Macro_acc) ->
case Tokens of
[] ->
{lists:reverse(Token_acc), lists:reverse(Macro_acc), Idx};
[{placeholder, P} | Rest] ->
case gleam_stdlib:string_starts_with(P, <<"@"/utf8>>) of
true ->
Name = gleam@string:drop_start(P, 1),
case {Engine, gleam_stdlib:map_get(Seen, Name)} of
{s_q_lite, {ok, Existing_idx}} ->
Placeholder = engine_placeholder(
Engine,
Existing_idx
),
expand_at_loop(
Engine,
Rest,
Idx,
Seen,
[{placeholder, Placeholder} | Token_acc],
Macro_acc
);
{_, _} ->
Placeholder@1 = engine_placeholder(Engine, Idx),
New_seen = case Engine of
s_q_lite ->
gleam@dict:insert(Seen, Name, Idx);
_ ->
Seen
end,
expand_at_loop(
Engine,
Rest,
Idx + 1,
New_seen,
[{placeholder, Placeholder@1} | Token_acc],
[{macro_arg, Idx, Name} | Macro_acc]
)
end;
false ->
expand_at_loop(
Engine,
Rest,
Idx,
Seen,
[{placeholder, P} | Token_acc],
Macro_acc
)
end;
[Token | Rest@1] ->
expand_at_loop(
Engine,
Rest@1,
Idx,
Seen,
[Token | Token_acc],
Macro_acc
)
end.
-file("src/sqlode/query_parser.gleam", 684).
?DOC(
" Expand @name shorthands in the token list.\n"
" For non-MySQL engines, Placeholder(\"@name\") tokens are replaced\n"
" with engine-appropriate placeholders ($N, ?N).\n"
" The lexer only emits Placeholder(\"@...\") when @ is followed by\n"
" alpha/underscore, so @1 is never matched (it becomes Operator+NumberLit).\n"
).
-spec expand_at_name_tokens(
sqlode@model:engine(),
list(sqlode@lexer:token()),
integer()
) -> {list(sqlode@lexer:token()), list(sqlode@model:macro()), integer()}.
expand_at_name_tokens(Engine, Tokens, Start_idx) ->
case Engine of
my_s_q_l ->
{Tokens, [], Start_idx};
_ ->
expand_at_loop(Engine, Tokens, Start_idx, maps:new(), [], [])
end.
-file("src/sqlode/query_parser.gleam", 864).
-spec engine_slice_placeholder(sqlode@model:engine(), integer()) -> binary().
engine_slice_placeholder(_, Index) ->
sqlode@runtime:slice_marker(Index).
-file("src/sqlode/query_parser.gleam", 768).
-spec expand_macro_loop(
sqlode@model:engine(),
list(sqlode@lexer:token()),
integer(),
list(sqlode@lexer:token()),
list(sqlode@model:macro())
) -> {list(sqlode@lexer:token()), list(sqlode@model:macro())}.
expand_macro_loop(Engine, Tokens, Idx, Token_acc, Macro_acc) ->
case Tokens of
[] ->
{lists:reverse(Token_acc), lists:reverse(Macro_acc)};
[{ident, Mod}, dot, {ident, Kind}, l_paren | Rest] when (((Mod =:= <<"sqlode"/utf8>>) orelse (Mod =:= <<"Sqlode"/utf8>>)) orelse (Mod =:= <<"SQLODE"/utf8>>)) andalso (((Kind =:= <<"arg"/utf8>>) orelse (Kind =:= <<"narg"/utf8>>)) orelse (Kind =:= <<"slice"/utf8>>)) ->
{Arg_tokens, Remaining} = collect_macro_arg_tokens(Rest, 1, []),
Name = extract_arg_name(Arg_tokens),
Placeholder = case Kind of
<<"slice"/utf8>> ->
engine_slice_placeholder(Engine, Idx);
_ ->
engine_placeholder(Engine, Idx)
end,
Macro_entry = case Kind of
<<"narg"/utf8>> ->
{macro_narg, Idx, Name};
<<"slice"/utf8>> ->
{macro_slice, Idx, Name};
_ ->
{macro_arg, Idx, Name}
end,
expand_macro_loop(
Engine,
Remaining,
Idx + 1,
[{placeholder, Placeholder} | Token_acc],
[Macro_entry | Macro_acc]
);
[Token | Rest@1] ->
expand_macro_loop(
Engine,
Rest@1,
Idx,
[Token | Token_acc],
Macro_acc
)
end.
-file("src/sqlode/query_parser.gleam", 760).
?DOC(
" Expand sqlode.arg(name), sqlode.narg(name), sqlode.slice(name) macros\n"
" by walking the token list and replacing the token span with a Placeholder.\n"
" Pattern: Ident(\"sqlode\") Dot Ident(\"arg\"|\"narg\"|\"slice\") LParen ...arg... RParen\n"
).
-spec expand_macro_tokens(
sqlode@model:engine(),
list(sqlode@lexer:token()),
integer()
) -> {list(sqlode@lexer:token()), list(sqlode@model:macro())}.
expand_macro_tokens(Engine, Tokens, Start_idx) ->
expand_macro_loop(Engine, Tokens, Start_idx, [], []).
-file("src/sqlode/query_parser.gleam", 177).
-spec finalize_pending(
sqlode@naming:naming_context(),
gleam@option:option(pending_query()),
binary(),
sqlode@model:engine(),
list(sqlode@query_ir:tokenized_query())
) -> {ok, list(sqlode@query_ir:tokenized_query())} | {error, parse_error()}.
finalize_pending(_, Pending, Path, Engine, Parsed_rev) ->
case Pending of
none ->
{ok, Parsed_rev};
{some,
{pending_query, Name, Function_name, Command, Start_line, Body_rev}} ->
Sql = begin
_pipe = Body_rev,
_pipe@1 = lists:reverse(_pipe),
_pipe@2 = gleam@string:join(_pipe@1, <<"\n"/utf8>>),
gleam@string:trim(_pipe@2)
end,
case Sql =:= <<""/utf8>> of
true ->
{error, {missing_sql, Path, Start_line, Name}};
false ->
Tokens = sqlode@lexer:tokenize(Sql, Engine),
{Tokens@1, At_macros, Next_idx} = expand_at_name_tokens(
Engine,
Tokens,
1
),
{Tokens@2, Expanded_macros} = expand_macro_tokens(
Engine,
Tokens@1,
Next_idx
),
Macros = lists:append(At_macros, Expanded_macros),
case validate_placeholder_syntax(Engine, Tokens@2) of
{error, Token} ->
{error,
{invalid_placeholder,
Path,
Start_line,
Name,
Engine,
Token}};
{ok, nil} ->
case validate_upsert_tails(Engine, Tokens@2) of
{error, Tail} ->
{error,
{wrong_engine_upsert,
Path,
Start_line,
Name,
Engine,
Tail}};
{ok, nil} ->
case validate_sqlite_numbered_placeholders(
Engine,
Tokens@2
) of
{error, Indices} ->
{error,
{sparse_numbered_placeholders,
Path,
Start_line,
Name,
Indices}};
{ok, nil} ->
{ok,
finalize_ok(
Parsed_rev,
Name,
Function_name,
Command,
Path,
Tokens@2,
Macros,
Engine
)}
end
end
end
end
end.
-file("src/sqlode/query_parser.gleam", 67).
-spec parse_lines(
sqlode@naming:naming_context(),
list(binary()),
binary(),
sqlode@model:engine(),
integer(),
gleam@option:option(pending_query()),
list(sqlode@query_ir:tokenized_query()),
boolean()
) -> {ok, list(sqlode@query_ir:tokenized_query())} | {error, parse_error()}.
parse_lines(
Naming_ctx,
Lines,
Path,
Engine,
Line_number,
Pending,
Parsed_rev,
Skip_next
) ->
case Lines of
[] ->
finalize_pending(Naming_ctx, Pending, Path, Engine, Parsed_rev);
[Line | Rest] ->
Trimmed = gleam@string:trim(Line),
case is_skip_annotation(Trimmed) of
true ->
gleam@result:'try'(
finalize_pending(
Naming_ctx,
Pending,
Path,
Engine,
Parsed_rev
),
fun(Parsed_rev@1) ->
parse_lines(
Naming_ctx,
Rest,
Path,
Engine,
Line_number + 1,
none,
Parsed_rev@1,
true
)
end
);
false ->
case parse_annotation(
Naming_ctx,
Trimmed,
Path,
Line_number
) of
{ok, {some, Next_pending}} ->
gleam@result:'try'(
finalize_pending(
Naming_ctx,
Pending,
Path,
Engine,
Parsed_rev
),
fun(Parsed_rev@2) -> case Skip_next of
true ->
parse_lines(
Naming_ctx,
Rest,
Path,
Engine,
Line_number + 1,
none,
Parsed_rev@2,
false
);
false ->
parse_lines(
Naming_ctx,
Rest,
Path,
Engine,
Line_number + 1,
{some, Next_pending},
Parsed_rev@2,
false
)
end end
);
{ok, none} ->
Pending@1 = case Pending of
{some,
{pending_query,
Name,
Function_name,
Command,
Start_line,
Body_rev}} ->
{some,
{pending_query,
Name,
Function_name,
Command,
Start_line,
[Line | Body_rev]}};
none ->
none
end,
parse_lines(
Naming_ctx,
Rest,
Path,
Engine,
Line_number + 1,
Pending@1,
Parsed_rev,
Skip_next
);
{error, Error} ->
{error, Error}
end
end
end.
-file("src/sqlode/query_parser.gleam", 48).
-spec parse_file(
binary(),
sqlode@model:engine(),
sqlode@naming:naming_context(),
binary()
) -> {ok, list(sqlode@query_ir:tokenized_query())} | {error, parse_error()}.
parse_file(Path, Engine, Naming_ctx, Content) ->
_pipe = parse_lines(
Naming_ctx,
gleam@string:split(Content, <<"\n"/utf8>>),
Path,
Engine,
1,
none,
[],
false
),
gleam@result:map(_pipe, fun lists:reverse/1).