-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()}. -type pending_query() :: {pending_query, binary(), binary(), sqlode@runtime:query_command(), integer(), list(binary())}. -file("src/sqlode/query_parser.gleam", 277). -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", 288). -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", 270). ?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", 220). -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] -> 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 ); _ -> {error, {invalid_annotation, Path, Line_number, <<"expected '-- name: ' or '/* name: */' where command is one of: :one, :many, :exec, :execresult, :execrows, :execlastid, :batchone, :batchmany, :batchexec, :copyfrom"/utf8>>}} end end. -file("src/sqlode/query_parser.gleam", 315). -spec is_skip_annotation(binary()) -> boolean(). is_skip_annotation(Line) -> Line =:= <<"-- sqlode:skip"/utf8>>. -file("src/sqlode/query_parser.gleam", 374). -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", 379). -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", 326). ?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", 395). -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>> end. -file("src/sqlode/query_parser.gleam", 548). ?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", 573). ?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", 593). -spec engine_placeholder(sqlode@model:engine(), integer()) -> binary(). engine_placeholder(_, Index) -> sqlode@runtime:param_marker(Index). -file("src/sqlode/query_parser.gleam", 433). -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", 422). ?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", 602). -spec engine_slice_placeholder(sqlode@model:engine(), integer()) -> binary(). engine_slice_placeholder(_, Index) -> sqlode@runtime:slice_marker(Index). -file("src/sqlode/query_parser.gleam", 506). -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", 498). ?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", 156). -spec finalize_pending( sqlode@naming:naming_context(), gleam@option:option(pending_query()), binary(), sqlode@model:engine(), list(sqlode@model:parsed_query()) ) -> {ok, list(sqlode@model:parsed_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), Expanded_sql = sqlode@lexer:tokens_to_string( Tokens@2, {token_render_options, false, true, {some, Engine}} ), Param_count = count_parameters_from_tokens(Engine, Tokens@2), {ok, [{parsed_query, Name, Function_name, Command, Expanded_sql, Path, Param_count, Macros} | Parsed_rev]} end end. -file("src/sqlode/query_parser.gleam", 46). -spec parse_lines( sqlode@naming:naming_context(), list(binary()), binary(), sqlode@model:engine(), integer(), gleam@option:option(pending_query()), list(sqlode@model:parsed_query()), boolean() ) -> {ok, list(sqlode@model:parsed_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", 27). -spec parse_file( binary(), sqlode@model:engine(), sqlode@naming:naming_context(), binary() ) -> {ok, list(sqlode@model:parsed_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).