-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___` or `__sqlode_slice___` 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} -> <<<<<<<>/binary, (erlang:integer_to_binary(Line))/binary>>/binary, ": invalid query annotation: "/utf8>>/binary, Detail/binary>>; {missing_sql, Path@1, Line@1, Name} -> <<<<<<<<<>/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} -> <<<<<<<<<<<<<<<<<<<>/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} -> <<<<<<<<<<<<<<<<<<<>/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} -> <<<<<<<<<<<<<>/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).