-module(sqlode@query_analyzer@expr_parser). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/sqlode/query_analyzer/expr_parser.gleam"). -export([parse_stmt/2, parse_select_core/2, parse_expr/2]). -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( " Recursive-descent parser that turns a list of SQL tokens into the\n" " expression-aware IR (`query_ir.Expr` / `query_ir.SelectCore` /\n" " `query_ir.Stmt`).\n" "\n" " The parser is deliberately permissive: it models the SQL subset\n" " sqlode needs to reason about (the constructs exercised by the\n" " fixtures in `test/fixtures/complex_sql/`) and falls back to\n" " `query_ir.RawExpr` / `query_ir.UnstructuredStmt` with an explicit\n" " `reason` string when it hits a construct it does not understand.\n" " The downstream analyzer surfaces these as\n" " `AnalysisError.UnsupportedExpression`, so \"silent fallback to\n" " StringType\" never happens — every gap is tied to a concrete IR\n" " node an operator can point at.\n" "\n" " Every public parsing entry takes an `engine: model.Engine` parameter\n" " so MySQL-only constructs (`ON DUPLICATE KEY UPDATE`,\n" " `LIMIT offset, count`) can be recognised without polluting the\n" " PostgreSQL / SQLite paths. The engine is threaded through every\n" " internal helper that (directly or transitively) parses another\n" " expression, select core, or statement. Pure token-shape helpers\n" " (paren collection, comma splitting, keyword scanning, etc.) do not\n" " receive it — they are dialect-agnostic.\n" "\n" " Precedence roughly follows PostgreSQL's operator table:\n" "\n" " 1. OR\n" " 2. AND\n" " 3. NOT\n" " 4. IS [NOT] NULL/TRUE/FALSE, IS [NOT] DISTINCT FROM\n" " 5. =, <>, !=, <, >, <=, >=, LIKE, ILIKE, IN, BETWEEN, SIMILAR TO,\n" " @>, <@, ?|, ?&, &&\n" " 6. +, -, ||, JSON ops (->, ->>, #>, #>>)\n" " 7. *, /, %\n" " 8. unary -, +\n" " 9. ::type cast\n" " 10. function calls / atoms\n" ). -file("src/sqlode/query_analyzer/expr_parser.gleam", 75). -spec attach_recursive(list(sqlode@query_ir:cte_def()), boolean()) -> list(sqlode@query_ir:cte_def()). attach_recursive(Ctes, Recursive) -> case Recursive of false -> Ctes; true -> gleam@list:map( Ctes, fun(Cte) -> {cte_def, erlang:element(2, Cte), erlang:element(3, Cte), erlang:element(4, Cte), true} end ) end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 438). -spec is_expr_end(sqlode@lexer:token()) -> boolean(). is_expr_end(Token) -> case Token of r_paren -> true; {ident, _} -> true; {quoted_ident, _} -> true; {string_lit, _} -> true; {number_lit, _} -> true; {placeholder, _} -> true; star -> true; _ -> false end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 411). -spec split_trailing_alias(list(sqlode@lexer:token())) -> {list(sqlode@lexer:token()), gleam@option:option(binary())}. split_trailing_alias(Tokens) -> case lists:reverse(Tokens) of [{ident, Name}, {keyword, <<"as"/utf8>>} | Rest] -> {lists:reverse(Rest), {some, sqlode@naming:normalize_identifier(Name)}}; [{quoted_ident, Name@1}, {keyword, <<"as"/utf8>>} | Rest@1] -> {lists:reverse(Rest@1), {some, sqlode@naming:normalize_identifier(Name@1)}}; [{ident, Name@2}, Prev | Rest@2] -> case is_expr_end(Prev) of true -> {lists:reverse([Prev | Rest@2]), {some, sqlode@naming:normalize_identifier(Name@2)}}; false -> {Tokens, none} end; _ -> {Tokens, none} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 640). -spec is_reserved_after_table(binary()) -> boolean(). is_reserved_after_table(Name) -> Lowered = string:lowercase(Name), gleam@list:contains( [<<"on"/utf8>>, <<"where"/utf8>>, <<"group"/utf8>>, <<"having"/utf8>>, <<"order"/utf8>>, <<"limit"/utf8>>, <<"offset"/utf8>>, <<"union"/utf8>>, <<"intersect"/utf8>>, <<"except"/utf8>>, <<"returning"/utf8>>, <<"using"/utf8>>, <<"lateral"/utf8>>, <<"left"/utf8>>, <<"right"/utf8>>, <<"inner"/utf8>>, <<"outer"/utf8>>, <<"cross"/utf8>>, <<"full"/utf8>>, <<"natural"/utf8>>, <<"join"/utf8>>, <<"window"/utf8>>], Lowered ). -file("src/sqlode/query_analyzer/expr_parser.gleam", 618). -spec parse_optional_alias(list(sqlode@lexer:token())) -> {gleam@option:option(binary()), list(sqlode@lexer:token())}. parse_optional_alias(Tokens) -> case Tokens of [{keyword, <<"as"/utf8>>}, {ident, Name} | Rest] -> {{some, string:lowercase(Name)}, Rest}; [{keyword, <<"as"/utf8>>}, {quoted_ident, Name@1} | Rest@1] -> {{some, string:lowercase(Name@1)}, Rest@1}; [{ident, Name@2} | Rest@2] -> case is_reserved_after_table(Name@2) of true -> {none, Tokens}; false -> {{some, string:lowercase(Name@2)}, Rest@2} end; [{quoted_ident, Name@3} | Rest@3] -> {{some, string:lowercase(Name@3)}, Rest@3}; _ -> {none, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 678). -spec detect_join(list(sqlode@lexer:token())) -> gleam@option:option({sqlode@query_ir:join_kind(), list(sqlode@lexer:token()), boolean()}). detect_join(Tokens) -> case Tokens of [{keyword, <<"join"/utf8>>}, {keyword, <<"lateral"/utf8>>} | Rest] -> {some, {inner_join, Rest, true}}; [{keyword, <<"join"/utf8>>} | Rest@1] -> {some, {inner_join, Rest@1, false}}; [{keyword, <<"inner"/utf8>>}, {keyword, <<"join"/utf8>>} | Rest@2] -> {some, {inner_join, Rest@2, false}}; [{keyword, <<"cross"/utf8>>}, {keyword, <<"join"/utf8>>} | Rest@3] -> {some, {cross_join, Rest@3, false}}; [{keyword, <<"left"/utf8>>}, {keyword, <<"join"/utf8>>}, {keyword, <<"lateral"/utf8>>} | Rest@4] -> {some, {left_join, Rest@4, true}}; [{keyword, <<"left"/utf8>>}, {keyword, <<"outer"/utf8>>}, {keyword, <<"join"/utf8>>}, {keyword, <<"lateral"/utf8>>} | Rest@5] -> {some, {left_join, Rest@5, true}}; [{keyword, <<"left"/utf8>>}, {keyword, <<"outer"/utf8>>}, {keyword, <<"join"/utf8>>} | Rest@6] -> {some, {left_join, Rest@6, false}}; [{keyword, <<"left"/utf8>>}, {keyword, <<"join"/utf8>>} | Rest@7] -> {some, {left_join, Rest@7, false}}; [{keyword, <<"right"/utf8>>}, {keyword, <<"outer"/utf8>>}, {keyword, <<"join"/utf8>>} | Rest@8] -> {some, {right_join, Rest@8, false}}; [{keyword, <<"right"/utf8>>}, {keyword, <<"join"/utf8>>} | Rest@9] -> {some, {right_join, Rest@9, false}}; [{keyword, <<"full"/utf8>>}, {keyword, <<"outer"/utf8>>}, {keyword, <<"join"/utf8>>} | Rest@10] -> {some, {full_join, Rest@10, false}}; [{keyword, <<"full"/utf8>>}, {keyword, <<"join"/utf8>>} | Rest@11] -> {some, {full_join, Rest@11, false}}; _ -> none end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1282). -spec detect_slice_macro(list(sqlode@lexer:token())) -> gleam@option:option(binary()). detect_slice_macro(Tokens) -> case Tokens of [{ident, S}, dot, {ident, Sl}, l_paren, {ident, Name}, r_paren] -> case {string:lowercase(S), string:lowercase(Sl)} of {<<"sqlode"/utf8>>, <<"slice"/utf8>>} -> {some, string:lowercase(Name)}; {_, _} -> none end; _ -> none end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1486). -spec render_type_parens(list(sqlode@lexer:token())) -> binary(). render_type_parens(Tokens) -> _pipe = Tokens, _pipe@1 = gleam@list:map(_pipe, fun(T) -> case T of {ident, N} -> N; {keyword, K} -> K; {number_lit, N@1} -> N@1; comma -> <<","/utf8>>; _ -> <<""/utf8>> end end), gleam@string:join(_pipe@1, <<""/utf8>>). -file("src/sqlode/query_analyzer/expr_parser.gleam", 1844). -spec split_commas_loop( list(sqlode@lexer:token()), integer(), list(sqlode@lexer:token()), list(list(sqlode@lexer:token())) ) -> list(list(sqlode@lexer:token())). split_commas_loop(Tokens, Depth, Current, Acc) -> case Tokens of [] -> case Current of [] -> lists:reverse(Acc); _ -> lists:reverse([lists:reverse(Current) | Acc]) end; [comma | Rest] when Depth =:= 0 -> case Current of [] -> split_commas_loop(Rest, 0, [], Acc); _ -> split_commas_loop( Rest, 0, [], [lists:reverse(Current) | Acc] ) end; [l_paren | Rest@1] -> split_commas_loop(Rest@1, Depth + 1, [l_paren | Current], Acc); [r_paren | Rest@2] -> split_commas_loop(Rest@2, Depth - 1, [r_paren | Current], Acc); [T | Rest@3] -> split_commas_loop(Rest@3, Depth, [T | Current], Acc) end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1840). -spec split_on_top_commas(list(sqlode@lexer:token())) -> list(list(sqlode@lexer:token())). split_on_top_commas(Tokens) -> split_commas_loop(Tokens, 0, [], []). -file("src/sqlode/query_analyzer/expr_parser.gleam", 172). -spec parse_ident_list(list(sqlode@lexer:token())) -> list(binary()). parse_ident_list(Tokens) -> _pipe = Tokens, _pipe@1 = split_on_top_commas(_pipe), gleam@list:filter_map(_pipe@1, fun(Group) -> case Group of [{ident, N}] -> {ok, string:lowercase(N)}; [{quoted_ident, N@1}] -> {ok, string:lowercase(N@1)}; _ -> {error, nil} end end). -file("src/sqlode/query_analyzer/expr_parser.gleam", 1875). -spec collect_paren_loop( list(sqlode@lexer:token()), integer(), list(sqlode@lexer:token()) ) -> {list(sqlode@lexer:token()), list(sqlode@lexer:token())}. collect_paren_loop(Tokens, Depth, Acc) -> case Depth =< 0 of true -> {lists:reverse(Acc), Tokens}; false -> case Tokens of [] -> {lists:reverse(Acc), []}; [l_paren | Rest] -> collect_paren_loop(Rest, Depth + 1, [l_paren | Acc]); [r_paren | Rest@1] -> case Depth =:= 1 of true -> {lists:reverse(Acc), Rest@1}; false -> collect_paren_loop( Rest@1, Depth - 1, [r_paren | Acc] ) end; [T | Rest@2] -> collect_paren_loop(Rest@2, Depth, [T | Acc]) end end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1869). -spec collect_parens(list(sqlode@lexer:token())) -> {list(sqlode@lexer:token()), list(sqlode@lexer:token())}. collect_parens(Tokens) -> collect_paren_loop(Tokens, 1, []). -file("src/sqlode/query_analyzer/expr_parser.gleam", 592). -spec parse_aliased_column_list(list(sqlode@lexer:token())) -> {gleam@option:option(binary()), list(binary()), list(sqlode@lexer:token())}. parse_aliased_column_list(Tokens) -> After_as = case Tokens of [{keyword, <<"as"/utf8>>} | Rest] -> Rest; _ -> Tokens end, case After_as of [{ident, Alias}, l_paren | After_lp] -> {Cols, After} = collect_parens(After_lp), {{some, string:lowercase(Alias)}, parse_ident_list(Cols), After}; [{quoted_ident, Alias@1}, l_paren | After_lp@1] -> {Cols@1, After@1} = collect_parens(After_lp@1), {{some, string:lowercase(Alias@1)}, parse_ident_list(Cols@1), After@1}; [{ident, Alias@2} | Rest@1] -> {{some, string:lowercase(Alias@2)}, [], Rest@1}; [{quoted_ident, Alias@3} | Rest@2] -> {{some, string:lowercase(Alias@3)}, [], Rest@2}; _ -> {none, [], After_as} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 568). -spec parse_subquery_alias( sqlode@query_ir:select_core(), list(sqlode@lexer:token()), boolean() ) -> {sqlode@query_ir:from_item_ex(), list(sqlode@lexer:token())}. parse_subquery_alias(Core, After, Lateral) -> {Alias_opt, Col_aliases, Rest} = parse_aliased_column_list(After), Alias = gleam@option:unwrap(Alias_opt, <<""/utf8>>), Item = {from_subquery, Core, Alias, Col_aliases}, case Lateral of true -> {{from_join, {from_table, <<""/utf8>>, none}, Item, cross_join, join_no_condition, true}, Rest}; false -> {Item, Rest} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 873). ?DOC( " Walk past VALUES rows without consuming the tokens that follow the\n" " final row. Mirrors `collect_values_rows` but returns the leftover\n" " tokens instead of the parsed expression lists.\n" ). -spec skip_values_rows(list(sqlode@lexer:token())) -> list(sqlode@lexer:token()). skip_values_rows(Tokens) -> case Tokens of [l_paren | Rest] -> {_, After} = collect_parens(Rest), case After of [comma | More] -> skip_values_rows(More); _ -> After end; _ -> Tokens end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1459). -spec collect_type_tokens(list(sqlode@lexer:token()), list(binary())) -> {binary(), list(sqlode@lexer:token())}. collect_type_tokens(Tokens, Acc) -> case Tokens of [{ident, N} | Rest] -> collect_type_tokens(Rest, [string:lowercase(N) | Acc]); [{keyword, K} | Rest@1] -> collect_type_tokens(Rest@1, [K | Acc]); [l_paren | Rest@2] -> {Inside, After} = collect_parens(Rest@2), Inside_text = render_type_parens(Inside), Last_acc = case Acc of [First | Rest_acc] -> [<<<<<>/binary, Inside_text/binary>>/binary, ")"/utf8>> | Rest_acc]; [] -> [<<<<"("/utf8, Inside_text/binary>>/binary, ")"/utf8>>] end, collect_type_tokens(After, Last_acc); [{operator, <<"[]"/utf8>>} | Rest@3] -> case Acc of [First@1 | Rest_acc@1] -> collect_type_tokens( Rest@3, [<> | Rest_acc@1] ); [] -> collect_type_tokens(Rest@3, [<<"[]"/utf8>>]) end; _ -> {begin _pipe = lists:reverse(Acc), _pipe@1 = gleam@string:join(_pipe, <<" "/utf8>>), gleam@string:trim(_pipe@1) end, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1455). -spec read_type_tokens(list(sqlode@lexer:token())) -> {binary(), list(sqlode@lexer:token())}. read_type_tokens(Tokens) -> collect_type_tokens(Tokens, []). -file("src/sqlode/query_analyzer/expr_parser.gleam", 1442). -spec parse_cast_tail(sqlode@query_ir:expr(), list(sqlode@lexer:token())) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_cast_tail(Atom, Tokens) -> case Tokens of [{operator, <<"::"/utf8>>} | Rest] -> {Type_name, After} = read_type_tokens(Rest), parse_cast_tail({cast, Atom, Type_name}, After); _ -> {Atom, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1693). -spec collect_parens_after_where(list(sqlode@lexer:token())) -> {list(sqlode@lexer:token()), list(sqlode@lexer:token())}. collect_parens_after_where(Tokens) -> collect_parens(Tokens). -file("src/sqlode/query_analyzer/expr_parser.gleam", 1903). -spec collect_bracket_loop( list(sqlode@lexer:token()), integer(), list(sqlode@lexer:token()) ) -> {list(sqlode@lexer:token()), list(sqlode@lexer:token())}. collect_bracket_loop(Tokens, Depth, Acc) -> case Depth =< 0 of true -> {lists:reverse(Acc), Tokens}; false -> case Tokens of [] -> {lists:reverse(Acc), []}; [{operator, <<"["/utf8>>} | Rest] -> collect_bracket_loop( Rest, Depth + 1, [{operator, <<"["/utf8>>} | Acc] ); [{operator, <<"]"/utf8>>} | Rest@1] -> case Depth =:= 1 of true -> {lists:reverse(Acc), Rest@1}; false -> collect_bracket_loop( Rest@1, Depth - 1, [{operator, <<"]"/utf8>>} | Acc] ) end; [T | Rest@2] -> collect_bracket_loop(Rest@2, Depth, [T | Acc]) end end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1897). -spec collect_brackets(list(sqlode@lexer:token())) -> {list(sqlode@lexer:token()), list(sqlode@lexer:token())}. collect_brackets(Tokens) -> collect_bracket_loop(Tokens, 1, []). -file("src/sqlode/query_analyzer/expr_parser.gleam", 1933). -spec collect_until_loop( list(sqlode@lexer:token()), list(binary()), integer(), list(sqlode@lexer:token()) ) -> {list(sqlode@lexer:token()), list(sqlode@lexer:token())}. collect_until_loop(Tokens, Stop, Depth, Acc) -> case Tokens of [] -> {lists:reverse(Acc), []}; [l_paren | Rest] -> collect_until_loop(Rest, Stop, Depth + 1, [l_paren | Acc]); [r_paren | Rest@1] -> collect_until_loop(Rest@1, Stop, Depth - 1, [r_paren | Acc]); [{keyword, Kw} | Rest@2] = T when Depth =:= 0 -> case gleam@list:contains(Stop, Kw) of true -> {lists:reverse(Acc), T}; false -> collect_until_loop( Rest@2, Stop, Depth, [{keyword, Kw} | Acc] ) end; [T@1 | Rest@3] -> collect_until_loop(Rest@3, Stop, Depth, [T@1 | Acc]) end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1926). -spec collect_until_keyword(list(sqlode@lexer:token()), list(binary())) -> {list(sqlode@lexer:token()), list(sqlode@lexer:token())}. collect_until_keyword(Tokens, Stop) -> collect_until_loop(Tokens, Stop, 0, []). -file("src/sqlode/query_analyzer/expr_parser.gleam", 1956). -spec read_qualified_name(list(sqlode@lexer:token())) -> {binary(), list(sqlode@lexer:token())}. read_qualified_name(Tokens) -> case Tokens of [{ident, _}, dot, {ident, Name} | Rest] -> {string:lowercase(Name), Rest}; [{ident, Name@1} | Rest@1] -> {string:lowercase(Name@1), Rest@1}; [{quoted_ident, Name@2} | Rest@2] -> {string:lowercase(Name@2), Rest@2}; _ -> {<<""/utf8>>, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1970). -spec drop_first(list(sqlode@lexer:token())) -> list(sqlode@lexer:token()). drop_first(Tokens) -> case Tokens of [_ | Rest] -> Rest; [] -> [] end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1977). -spec empty_core() -> sqlode@query_ir:select_core(). empty_core() -> {select_core, false, [], [], none, [], none, [], none, none, none}. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1992). -spec trim_noise(list(sqlode@lexer:token())) -> list(sqlode@lexer:token()). trim_noise(Tokens) -> gleam@list:filter(Tokens, fun(T) -> case T of semicolon -> false; _ -> true end end). -file("src/sqlode/query_analyzer/expr_parser.gleam", 2007). -spec split_on_as_loop( list(sqlode@lexer:token()), integer(), list(sqlode@lexer:token()) ) -> {list(sqlode@lexer:token()), list(sqlode@lexer:token())}. split_on_as_loop(Tokens, Depth, Acc) -> case Tokens of [] -> {lists:reverse(Acc), []}; [l_paren | Rest] -> split_on_as_loop(Rest, Depth + 1, [l_paren | Acc]); [r_paren | Rest@1] -> split_on_as_loop(Rest@1, Depth - 1, [r_paren | Acc]); [{keyword, <<"as"/utf8>>} | Rest@2] when Depth =:= 0 -> {lists:reverse(Acc), Rest@2}; [T | Rest@3] -> split_on_as_loop(Rest@3, Depth, [T | Acc]) end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 2001). -spec split_on_as(list(sqlode@lexer:token())) -> {list(sqlode@lexer:token()), list(sqlode@lexer:token())}. split_on_as(Tokens) -> split_on_as_loop(Tokens, 0, []). -file("src/sqlode/query_analyzer/expr_parser.gleam", 2023). -spec skip_to_returning_or_end(list(sqlode@lexer:token())) -> list(sqlode@lexer:token()). skip_to_returning_or_end(Tokens) -> case Tokens of [] -> []; [{keyword, <<"returning"/utf8>>} | _] = T -> T; [_ | Rest] -> skip_to_returning_or_end(Rest) end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 2031). -spec decode_placeholder(binary()) -> integer(). decode_placeholder(Raw) -> Digits = case gleam@string:to_graphemes(Raw) of [] -> <<""/utf8>>; [_ | Rest] -> erlang:list_to_binary(Rest) end, case gleam_stdlib:parse_int(Digits) of {ok, N} -> N; {error, _} -> 0 end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 142). -spec parse_cte_after_name( binary(), list(sqlode@lexer:token()), sqlode@model:engine() ) -> gleam@option:option({sqlode@query_ir:cte_def(), list(sqlode@lexer:token())}). parse_cte_after_name(Name, Tokens, Engine) -> {Columns, After_cols} = case Tokens of [l_paren | After_lp] -> {Inside, After} = collect_parens(After_lp), {parse_ident_list(Inside), After}; _ -> {[], Tokens} end, case After_cols of [{keyword, <<"as"/utf8>>}, l_paren | After_as_lp] -> {Inner, After@1} = collect_parens(After_as_lp), Body = parse_stmt(Inner, Engine), {some, {{cte_def, string:lowercase(Name), Columns, Body, false}, After@1}}; _ -> none end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 51). ?DOC( " Parse a full statement from its token list. Never fails; unknown\n" " constructs surface as `UnstructuredStmt(reason, tokens)` with the\n" " raw tokens preserved for legacy passes and the reason string\n" " bubbled up to analyzer diagnostics.\n" ). -spec parse_stmt(list(sqlode@lexer:token()), sqlode@model:engine()) -> sqlode@query_ir:stmt(). parse_stmt(Tokens, Engine) -> {Ctes, Recursive, After_ctes} = parse_with_clause(Tokens, Engine), case After_ctes of [{keyword, <<"select"/utf8>>} | _] -> {Core, _} = parse_select_core(After_ctes, Engine), {select_stmt, attach_recursive(Ctes, Recursive), Core}; [{keyword, <<"insert"/utf8>>} | Rest] -> parse_insert_body(attach_recursive(Ctes, Recursive), Rest, Engine); [{keyword, <<"update"/utf8>>} | Rest@1] -> parse_update_body(attach_recursive(Ctes, Recursive), Rest@1, Engine); [{keyword, <<"delete"/utf8>>} | Rest@2] -> parse_delete_body(attach_recursive(Ctes, Recursive), Rest@2, Engine); _ -> {unstructured_stmt, <<"unrecognised top-level statement"/utf8>>, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 97). -spec parse_with_clause(list(sqlode@lexer:token()), sqlode@model:engine()) -> {list(sqlode@query_ir:cte_def()), boolean(), list(sqlode@lexer:token())}. parse_with_clause(Tokens, Engine) -> case Tokens of [{keyword, <<"with"/utf8>>}, {keyword, <<"recursive"/utf8>>} | Rest] -> {Ctes, Remaining} = parse_cte_list(Rest, [], Engine), {Ctes, true, Remaining}; [{keyword, <<"with"/utf8>>} | Rest@1] -> {Ctes@1, Remaining@1} = parse_cte_list(Rest@1, [], Engine), {Ctes@1, false, Remaining@1}; _ -> {[], false, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 114). -spec parse_cte_list( list(sqlode@lexer:token()), list(sqlode@query_ir:cte_def()), sqlode@model:engine() ) -> {list(sqlode@query_ir:cte_def()), list(sqlode@lexer:token())}. parse_cte_list(Tokens, Acc, Engine) -> case parse_single_cte(Tokens, Engine) of {some, {Cte, After}} -> case After of [comma | More] -> parse_cte_list(More, [Cte | Acc], Engine); _ -> {lists:reverse([Cte | Acc]), After} end; none -> {lists:reverse(Acc), Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 129). -spec parse_single_cte(list(sqlode@lexer:token()), sqlode@model:engine()) -> gleam@option:option({sqlode@query_ir:cte_def(), list(sqlode@lexer:token())}). parse_single_cte(Tokens, Engine) -> case Tokens of [{ident, Name} | After_name] -> parse_cte_after_name(Name, After_name, Engine); [{quoted_ident, Name@1} | After_name@1] -> parse_cte_after_name(Name@1, After_name@1, Engine); _ -> none end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 318). -spec parse_set_op(list(sqlode@lexer:token()), sqlode@model:engine()) -> {gleam@option:option(sqlode@query_ir:set_op()), list(sqlode@lexer:token())}. parse_set_op(Tokens, Engine) -> case Tokens of [{keyword, Kw} | Rest] when ((Kw =:= <<"union"/utf8>>) orelse (Kw =:= <<"intersect"/utf8>>)) orelse (Kw =:= <<"except"/utf8>>) -> {All, Rest2} = case Rest of [{keyword, <<"all"/utf8>>} | More] -> {true, More}; _ -> {false, Rest} end, {Core, After} = parse_select_core(Rest2, Engine), Kind = case Kw of <<"union"/utf8>> -> union; <<"intersect"/utf8>> -> intersect; _ -> except end, {{some, {set_op, Kind, All, Core}}, After}; _ -> {none, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 188). -spec parse_select_core(list(sqlode@lexer:token()), sqlode@model:engine()) -> {sqlode@query_ir:select_core(), list(sqlode@lexer:token())}. parse_select_core(Tokens, Engine) -> {Distinct, After_distinct} = case Tokens of [{keyword, <<"select"/utf8>>}, {keyword, <<"distinct"/utf8>>} | Rest] -> {true, Rest}; [{keyword, <<"select"/utf8>>} | Rest@1] -> {false, Rest@1}; _ -> {false, Tokens} end, {Select_tokens, Rest_after_select} = collect_until_keyword( After_distinct, [<<"from"/utf8>>, <<"where"/utf8>>, <<"group"/utf8>>, <<"having"/utf8>>, <<"order"/utf8>>, <<"limit"/utf8>>, <<"offset"/utf8>>, <<"union"/utf8>>, <<"intersect"/utf8>>, <<"except"/utf8>>, <<"window"/utf8>>] ), Items = parse_select_items(Select_tokens, Engine), {From_list, Rest_after_from} = case Rest_after_select of [{keyword, <<"from"/utf8>>} | Rest@2] -> parse_from_clause(Rest@2, Engine); _ -> {[], Rest_after_select} end, {Where_expr, Rest_after_where} = case Rest_after_from of [{keyword, <<"where"/utf8>>} | Rest@3] -> {Where_toks, After} = collect_until_keyword( Rest@3, [<<"group"/utf8>>, <<"having"/utf8>>, <<"order"/utf8>>, <<"limit"/utf8>>, <<"offset"/utf8>>, <<"union"/utf8>>, <<"intersect"/utf8>>, <<"except"/utf8>>, <<"window"/utf8>>] ), {{some, parse_expr(Where_toks, Engine)}, After}; _ -> {none, Rest_after_from} end, {Group_by, Rest_after_group} = case Rest_after_where of [{keyword, <<"group"/utf8>>}, {keyword, <<"by"/utf8>>} | Rest@4] -> {Gb_toks, After@1} = collect_until_keyword( Rest@4, [<<"having"/utf8>>, <<"order"/utf8>>, <<"limit"/utf8>>, <<"offset"/utf8>>, <<"union"/utf8>>, <<"intersect"/utf8>>, <<"except"/utf8>>, <<"window"/utf8>>] ), {parse_expr_list(Gb_toks, Engine), After@1}; _ -> {[], Rest_after_where} end, {Having_expr, Rest_after_having} = case Rest_after_group of [{keyword, <<"having"/utf8>>} | Rest@5] -> {H_toks, After@2} = collect_until_keyword( Rest@5, [<<"order"/utf8>>, <<"limit"/utf8>>, <<"offset"/utf8>>, <<"union"/utf8>>, <<"intersect"/utf8>>, <<"except"/utf8>>, <<"window"/utf8>>] ), {{some, parse_expr(H_toks, Engine)}, After@2}; _ -> {none, Rest_after_group} end, Rest_after_window = case Rest_after_having of [{keyword, <<"window"/utf8>>} | Rest@6] -> {_, After@3} = collect_until_keyword( Rest@6, [<<"order"/utf8>>, <<"limit"/utf8>>, <<"offset"/utf8>>, <<"union"/utf8>>, <<"intersect"/utf8>>, <<"except"/utf8>>] ), After@3; _ -> Rest_after_having end, {Order_by, Rest_after_order} = case Rest_after_window of [{keyword, <<"order"/utf8>>}, {keyword, <<"by"/utf8>>} | Rest@7] -> {O_toks, After@4} = collect_until_keyword( Rest@7, [<<"limit"/utf8>>, <<"offset"/utf8>>, <<"union"/utf8>>, <<"intersect"/utf8>>, <<"except"/utf8>>] ), {parse_order_keys(O_toks, Engine), After@4}; _ -> {[], Rest_after_window} end, {Limit, Offset_from_limit, Rest_after_limit} = case Rest_after_order of [{keyword, <<"limit"/utf8>>} | Rest@8] -> {L_toks, After@5} = collect_until_keyword( Rest@8, [<<"offset"/utf8>>, <<"union"/utf8>>, <<"intersect"/utf8>>, <<"except"/utf8>>] ), case Engine of my_s_q_l -> case split_on_top_commas(L_toks) of [Offset_toks, Count_toks] -> {{some, parse_expr(Count_toks, Engine)}, {some, parse_expr(Offset_toks, Engine)}, After@5}; _ -> {{some, parse_expr(L_toks, Engine)}, none, After@5} end; _ -> {{some, parse_expr(L_toks, Engine)}, none, After@5} end; _ -> {none, none, Rest_after_order} end, {Offset, Rest_after_offset} = case Rest_after_limit of [{keyword, <<"offset"/utf8>>} | Rest@9] -> {O_toks@1, After@6} = collect_until_keyword( Rest@9, [<<"union"/utf8>>, <<"intersect"/utf8>>, <<"except"/utf8>>] ), {{some, parse_expr(O_toks@1, Engine)}, After@6}; _ -> {Offset_from_limit, Rest_after_limit} end, {Set_op, Remaining} = parse_set_op(Rest_after_offset, Engine), {{select_core, Distinct, Items, From_list, Where_expr, Group_by, Having_expr, Order_by, Limit, Offset, Set_op}, Remaining}. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1722). -spec parse_window_spec(list(sqlode@lexer:token()), sqlode@model:engine()) -> sqlode@query_ir:window_spec(). parse_window_spec(Tokens, Engine) -> {_, Rest@1} = case Tokens of [{ident, _} | Rest] -> {true, Rest}; _ -> {false, Tokens} end, {Partition_by, Rest2} = case Rest@1 of [{keyword, <<"partition"/utf8>>}, {keyword, <<"by"/utf8>>} | More] -> {P_toks, After} = collect_until_keyword( More, [<<"order"/utf8>>, <<"range"/utf8>>, <<"rows"/utf8>>, <<"groups"/utf8>>] ), {parse_expr_list(P_toks, Engine), After}; _ -> {[], Rest@1} end, {Order_by, Rest3} = case Rest2 of [{keyword, <<"order"/utf8>>}, {keyword, <<"by"/utf8>>} | More@1] -> {O_toks, After@1} = collect_until_keyword( More@1, [<<"range"/utf8>>, <<"rows"/utf8>>, <<"groups"/utf8>>] ), {parse_order_keys(O_toks, Engine), After@1}; _ -> {[], Rest2} end, Frame = case Rest3 of [] -> none; Other -> {some, Other} end, {window_spec, Partition_by, Order_by, Frame}. -file("src/sqlode/query_analyzer/expr_parser.gleam", 342). -spec parse_order_keys(list(sqlode@lexer:token()), sqlode@model:engine()) -> list(sqlode@query_ir:order_key()). parse_order_keys(Tokens, Engine) -> _pipe = Tokens, _pipe@1 = split_on_top_commas(_pipe), gleam@list:map(_pipe@1, fun(Group) -> parse_order_key(Group, Engine) end). -file("src/sqlode/query_analyzer/expr_parser.gleam", 351). -spec parse_order_key(list(sqlode@lexer:token()), sqlode@model:engine()) -> sqlode@query_ir:order_key(). parse_order_key(Tokens, Engine) -> Reversed = lists:reverse(Tokens), {Nulls, Reversed2} = case Reversed of [{keyword, <<"first"/utf8>>}, {keyword, <<"nulls"/utf8>>} | Rest] -> {{some, nulls_first}, Rest}; [{keyword, <<"last"/utf8>>}, {keyword, <<"nulls"/utf8>>} | Rest@1] -> {{some, nulls_last}, Rest@1}; _ -> {none, Reversed} end, {Descending, Reversed3} = case Reversed2 of [{keyword, <<"desc"/utf8>>} | Rest@2] -> {true, Rest@2}; [{keyword, <<"asc"/utf8>>} | Rest@3] -> {false, Rest@3}; _ -> {false, Reversed2} end, Expr_tokens = lists:reverse(Reversed3), {order_key, parse_expr(Expr_tokens, Engine), Descending, Nulls}. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1022). -spec parse_expr(list(sqlode@lexer:token()), sqlode@model:engine()) -> sqlode@query_ir:expr(). parse_expr(Tokens, Engine) -> {Expr, Rest} = parse_or(trim_noise(Tokens), Engine), case Rest of [] -> Expr; _ -> {raw_expr, <<"trailing tokens after expression"/utf8>>, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1046). -spec parse_or(list(sqlode@lexer:token()), sqlode@model:engine()) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_or(Tokens, Engine) -> {Left, Rest} = parse_and(Tokens, Engine), parse_or_tail(Left, Rest, Engine). -file("src/sqlode/query_analyzer/expr_parser.gleam", 1054). -spec parse_or_tail( sqlode@query_ir:expr(), list(sqlode@lexer:token()), sqlode@model:engine() ) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_or_tail(Left, Tokens, Engine) -> case Tokens of [{keyword, <<"or"/utf8>>} | Rest] -> {Right, After} = parse_and(Rest, Engine), parse_or_tail({binary, <<"or"/utf8>>, Left, Right}, After, Engine); _ -> {Left, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1072). -spec parse_and(list(sqlode@lexer:token()), sqlode@model:engine()) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_and(Tokens, Engine) -> {Left, Rest} = parse_not(Tokens, Engine), parse_and_tail(Left, Rest, Engine). -file("src/sqlode/query_analyzer/expr_parser.gleam", 1080). -spec parse_and_tail( sqlode@query_ir:expr(), list(sqlode@lexer:token()), sqlode@model:engine() ) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_and_tail(Left, Tokens, Engine) -> case Tokens of [{keyword, <<"and"/utf8>>} | Rest] -> {Right, After} = parse_not(Rest, Engine), parse_and_tail({binary, <<"and"/utf8>>, Left, Right}, After, Engine); _ -> {Left, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1098). -spec parse_not(list(sqlode@lexer:token()), sqlode@model:engine()) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_not(Tokens, Engine) -> case Tokens of [{keyword, <<"not"/utf8>>} | Rest] -> {Inner, After} = parse_not(Rest, Engine), {{unary, <<"not"/utf8>>, Inner}, After}; _ -> parse_comparison(Tokens, Engine) end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1111). -spec parse_comparison(list(sqlode@lexer:token()), sqlode@model:engine()) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_comparison(Tokens, Engine) -> {Left, Rest} = parse_additive(Tokens, Engine), parse_comparison_tail(Left, Rest, Engine). -file("src/sqlode/query_analyzer/expr_parser.gleam", 1119). -spec parse_comparison_tail( sqlode@query_ir:expr(), list(sqlode@lexer:token()), sqlode@model:engine() ) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_comparison_tail(Left, Tokens, Engine) -> case Tokens of [{operator, Op} | Rest] when (((((((((((Op =:= <<"="/utf8>>) orelse (Op =:= <<"<>"/utf8>>)) orelse (Op =:= <<"!="/utf8>>)) orelse (Op =:= <<"<"/utf8>>)) orelse (Op =:= <<">"/utf8>>)) orelse (Op =:= <<"<="/utf8>>)) orelse (Op =:= <<">="/utf8>>)) orelse (Op =:= <<"@>"/utf8>>)) orelse (Op =:= <<"<@"/utf8>>)) orelse (Op =:= <<"?|"/utf8>>)) orelse (Op =:= <<"?&"/utf8>>)) orelse (Op =:= <<"&&"/utf8>>) -> case Rest of [{keyword, <<"any"/utf8>>}, l_paren | After_lp] -> {Inner, After} = collect_parens(After_lp), Right = parse_expr(Inner, Engine), {{quantified, Op, Left, q_any, Right}, After}; [{keyword, <<"all"/utf8>>}, l_paren | After_lp@1] -> {Inner@1, After@1} = collect_parens(After_lp@1), Right@1 = parse_expr(Inner@1, Engine), {{quantified, Op, Left, q_all, Right@1}, After@1}; _ -> {Right@2, After@2} = parse_additive(Rest, Engine), {{binary, Op, Left, Right@2}, After@2} end; [{keyword, <<"is"/utf8>>}, {keyword, <<"not"/utf8>>}, {keyword, <<"null"/utf8>>} | Rest@1] -> {{is_check, Left, is_null, true}, Rest@1}; [{keyword, <<"is"/utf8>>}, {keyword, <<"null"/utf8>>} | Rest@2] -> {{is_check, Left, is_null, false}, Rest@2}; [{keyword, <<"is"/utf8>>}, {keyword, <<"not"/utf8>>}, {keyword, <<"true"/utf8>>} | Rest@3] -> {{is_check, Left, is_true, true}, Rest@3}; [{keyword, <<"is"/utf8>>}, {keyword, <<"true"/utf8>>} | Rest@4] -> {{is_check, Left, is_true, false}, Rest@4}; [{keyword, <<"is"/utf8>>}, {keyword, <<"not"/utf8>>}, {keyword, <<"false"/utf8>>} | Rest@5] -> {{is_check, Left, is_false, true}, Rest@5}; [{keyword, <<"is"/utf8>>}, {keyword, <<"false"/utf8>>} | Rest@6] -> {{is_check, Left, is_false, false}, Rest@6}; [{keyword, <<"is"/utf8>>}, {keyword, <<"not"/utf8>>}, {keyword, <<"unknown"/utf8>>} | Rest@7] -> {{is_check, Left, is_unknown, true}, Rest@7}; [{keyword, <<"is"/utf8>>}, {keyword, <<"unknown"/utf8>>} | Rest@8] -> {{is_check, Left, is_unknown, false}, Rest@8}; [{keyword, <<"not"/utf8>>}, {keyword, <<"in"/utf8>>} | Rest@9] -> parse_in_tail(Left, Rest@9, true, Engine); [{keyword, <<"in"/utf8>>} | Rest@10] -> parse_in_tail(Left, Rest@10, false, Engine); [{keyword, <<"not"/utf8>>}, {keyword, <<"between"/utf8>>} | Rest@11] -> parse_between_tail(Left, Rest@11, true, Engine); [{keyword, <<"between"/utf8>>} | Rest@12] -> parse_between_tail(Left, Rest@12, false, Engine); [{keyword, <<"not"/utf8>>}, {keyword, <<"like"/utf8>>} | Rest@13] -> parse_like_tail(Left, Rest@13, like, true, Engine); [{keyword, <<"like"/utf8>>} | Rest@14] -> parse_like_tail(Left, Rest@14, like, false, Engine); [{keyword, <<"not"/utf8>>}, {keyword, <<"ilike"/utf8>>} | Rest@15] -> parse_like_tail(Left, Rest@15, ilike, true, Engine); [{keyword, <<"ilike"/utf8>>} | Rest@16] -> parse_like_tail(Left, Rest@16, ilike, false, Engine); _ -> {Left, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1319). -spec parse_like_tail( sqlode@query_ir:expr(), list(sqlode@lexer:token()), sqlode@query_ir:like_op(), boolean(), sqlode@model:engine() ) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_like_tail(Left, Tokens, Op, Negated, Engine) -> {Pattern, Rest} = parse_additive(Tokens, Engine), {Escape, Rest2} = case Rest of [{keyword, <<"escape"/utf8>>} | More] -> {Esc, After} = parse_additive(More, Engine), {{some, Esc}, After}; _ -> {none, Rest} end, {{like_expr, Left, Op, Pattern, Escape, Negated}, Rest2}. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1346). -spec parse_additive(list(sqlode@lexer:token()), sqlode@model:engine()) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_additive(Tokens, Engine) -> {Left, Rest} = parse_multiplicative(Tokens, Engine), parse_additive_tail(Left, Rest, Engine). -file("src/sqlode/query_analyzer/expr_parser.gleam", 1354). -spec parse_additive_tail( sqlode@query_ir:expr(), list(sqlode@lexer:token()), sqlode@model:engine() ) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_additive_tail(Left, Tokens, Engine) -> case Tokens of [{operator, Op} | Rest] when ((((((Op =:= <<"+"/utf8>>) orelse (Op =:= <<"-"/utf8>>)) orelse (Op =:= <<"||"/utf8>>)) orelse (Op =:= <<"->"/utf8>>)) orelse (Op =:= <<"->>"/utf8>>)) orelse (Op =:= <<"#>"/utf8>>)) orelse (Op =:= <<"#>>"/utf8>>) -> {Right, After} = parse_multiplicative(Rest, Engine), parse_additive_tail({binary, Op, Left, Right}, After, Engine); _ -> {Left, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1380). -spec parse_multiplicative(list(sqlode@lexer:token()), sqlode@model:engine()) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_multiplicative(Tokens, Engine) -> {Left, Rest} = parse_unary(Tokens, Engine), parse_multiplicative_tail(Left, Rest, Engine). -file("src/sqlode/query_analyzer/expr_parser.gleam", 1388). -spec parse_multiplicative_tail( sqlode@query_ir:expr(), list(sqlode@lexer:token()), sqlode@model:engine() ) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_multiplicative_tail(Left, Tokens, Engine) -> case Tokens of [star | Rest] -> {Right, After} = parse_unary(Rest, Engine), parse_multiplicative_tail( {binary, <<"*"/utf8>>, Left, Right}, After, Engine ); [{operator, Op} | Rest@1] when (Op =:= <<"/"/utf8>>) orelse (Op =:= <<"%"/utf8>>) -> {Right@1, After@1} = parse_unary(Rest@1, Engine), parse_multiplicative_tail( {binary, Op, Left, Right@1}, After@1, Engine ); _ -> {Left, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1414). -spec parse_unary(list(sqlode@lexer:token()), sqlode@model:engine()) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_unary(Tokens, Engine) -> case Tokens of [{operator, <<"-"/utf8>>} | Rest] -> {Inner, After} = parse_unary(Rest, Engine), {{unary, <<"-"/utf8>>, Inner}, After}; [{operator, <<"+"/utf8>>} | Rest@1] -> {Inner@1, After@1} = parse_unary(Rest@1, Engine), {{unary, <<"+"/utf8>>, Inner@1}, After@1}; _ -> parse_cast(Tokens, Engine) end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1431). -spec parse_cast(list(sqlode@lexer:token()), sqlode@model:engine()) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_cast(Tokens, Engine) -> {Atom, Rest} = parse_atom(Tokens, Engine), parse_cast_tail(Atom, Rest). -file("src/sqlode/query_analyzer/expr_parser.gleam", 1500). -spec parse_atom(list(sqlode@lexer:token()), sqlode@model:engine()) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_atom(Tokens, Engine) -> case Tokens of [] -> {{raw_expr, <<"empty expression"/utf8>>, []}, []}; [{keyword, <<"null"/utf8>>} | Rest] -> {null_lit, Rest}; [{keyword, <<"true"/utf8>>} | Rest@1] -> {{bool_lit, true}, Rest@1}; [{keyword, <<"false"/utf8>>} | Rest@2] -> {{bool_lit, false}, Rest@2}; [{string_lit, V} | Rest@3] -> {{string_lit, V}, Rest@3}; [{number_lit, N} | Rest@4] -> {{number_lit, N}, Rest@4}; [{placeholder, Raw} | Rest@5] -> Idx = decode_placeholder(Raw), {{param, Idx, Raw}, Rest@5}; [{keyword, <<"case"/utf8>>} | Rest@6] -> parse_case(Rest@6, Engine); [{keyword, <<"cast"/utf8>>}, l_paren | Rest@7] -> {Inside, After} = collect_parens(Rest@7), {Expr_tokens, As_rest} = split_on_as(Inside), Target = begin _pipe = As_rest, _pipe@1 = gleam@list:map(_pipe, fun(T) -> case T of {ident, N@1} -> N@1; {keyword, K} -> K; _ -> <<""/utf8>> end end), _pipe@2 = gleam@list:filter( _pipe@1, fun(S) -> S /= <<""/utf8>> end ), gleam@string:join(_pipe@2, <<" "/utf8>>) end, {{cast, parse_expr(Expr_tokens, Engine), Target}, After}; [{keyword, <<"not"/utf8>>}, {keyword, <<"exists"/utf8>>}, l_paren | Rest@8] -> {Inside@1, After@1} = collect_parens(Rest@8), {Core, _} = parse_select_core(Inside@1, Engine), {{exists, Core, true}, After@1}; [{keyword, <<"exists"/utf8>>}, l_paren | Rest@9] -> {Inside@2, After@2} = collect_parens(Rest@9), {Core@1, _} = parse_select_core(Inside@2, Engine), {{exists, Core@1, false}, After@2}; [{keyword, <<"array"/utf8>>}, l_paren | Rest@10] -> {Inside@3, After@3} = collect_parens(Rest@10), {Core@2, _} = parse_select_core(Inside@3, Engine), {{scalar_subquery, Core@2}, After@3}; [{keyword, <<"array"/utf8>>}, {operator, <<"["/utf8>>} | Rest@11] -> {Inside@4, After@4} = collect_brackets(Rest@11), {{array_lit, parse_expr_list(Inside@4, Engine)}, After@4}; [l_paren, {keyword, <<"select"/utf8>>} | _] -> {Inside@5, After@5} = collect_parens(drop_first(Tokens)), {Core@3, _} = parse_select_core(Inside@5, Engine), {{scalar_subquery, Core@3}, After@5}; [l_paren, {keyword, <<"with"/utf8>>} | _] -> {Inside@6, After@6} = collect_parens(drop_first(Tokens)), Stmt = parse_stmt(Inside@6, Engine), case Stmt of {select_stmt, _, C} -> {{scalar_subquery, C}, After@6}; _ -> {{raw_expr, <<"non-SELECT subquery in expression"/utf8>>, Tokens}, []} end; [l_paren | Rest@12] -> {Inside@7, After@7} = collect_parens(Rest@12), case split_on_top_commas(Inside@7) of [Single] -> {parse_expr(Single, Engine), After@7}; Many -> {{tuple, gleam@list:map( Many, fun(Group) -> parse_expr(Group, Engine) end )}, After@7} end; [{keyword, <<"interval"/utf8>>}, {string_lit, Val} | Rest@13] -> {{func, <<"interval"/utf8>>, [{func_arg, {string_lit, Val}}], false, none, none}, Rest@13}; [{ident, S@1}, dot, {ident, Name}, l_paren | Rest@14] -> case string:lowercase(S@1) of <<"sqlode"/utf8>> -> {Inside@8, After@8} = collect_parens(Rest@14), {{macro, string:lowercase(Name), Inside@8}, After@8}; _ -> parse_function_call(Name, [l_paren | Rest@14], Engine) end; [{ident, Name@1}, l_paren | Rest@15] -> parse_function_call(Name@1, Rest@15, Engine); [{ident, Table}, dot, star | Rest@16] -> {{star_ref, {some, string:lowercase(Table)}}, Rest@16}; [{ident, Table@1}, dot, {operator, <<"*"/utf8>>} | Rest@17] -> {{star_ref, {some, string:lowercase(Table@1)}}, Rest@17}; [{ident, Table@2}, dot, {ident, Col} | Rest@18] -> {{column_ref, {some, string:lowercase(Table@2)}, string:lowercase(Col)}, Rest@18}; [{ident, Table@3}, dot, {quoted_ident, Col@1} | Rest@19] -> {{column_ref, {some, string:lowercase(Table@3)}, string:lowercase(Col@1)}, Rest@19}; [{ident, Name@2} | Rest@20] -> {{column_ref, none, string:lowercase(Name@2)}, Rest@20}; [{quoted_ident, Name@3} | Rest@21] -> {{column_ref, none, string:lowercase(Name@3)}, Rest@21}; [star | Rest@22] -> {{star_ref, none}, Rest@22}; [{operator, <<"*"/utf8>>} | Rest@23] -> {{star_ref, none}, Rest@23}; _ -> {{raw_expr, <<"unrecognised atom"/utf8>>, Tokens}, []} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1647). -spec parse_function_call( binary(), list(sqlode@lexer:token()), sqlode@model:engine() ) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_function_call(Name, Tokens, Engine) -> {Inside, After} = collect_parens(Tokens), {Distinct, Args_tokens} = case Inside of [{keyword, <<"distinct"/utf8>>} | Rest] -> {true, Rest}; _ -> {false, Inside} end, Args = case Args_tokens of [] -> []; _ -> _pipe = Args_tokens, _pipe@1 = split_on_top_commas(_pipe), gleam@list:map( _pipe@1, fun(Group) -> {func_arg, parse_expr(Group, Engine)} end ) end, {Filter, After_filter} = parse_filter_clause(After, Engine), {Over, After_over} = parse_over_clause(After_filter, Engine), {{func, string:lowercase(Name), Args, Distinct, Filter, Over}, After_over}. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1701). -spec parse_over_clause(list(sqlode@lexer:token()), sqlode@model:engine()) -> {gleam@option:option(sqlode@query_ir:window_spec()), list(sqlode@lexer:token())}. parse_over_clause(Tokens, Engine) -> case Tokens of [{keyword, <<"over"/utf8>>}, l_paren | Rest] -> {Inside, After} = collect_parens(Rest), {{some, parse_window_spec(Inside, Engine)}, After}; [{keyword, <<"over"/utf8>>}, {ident, _} | Rest@1] -> {{some, {window_spec, [], [], none}}, Rest@1}; _ -> {none, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 393). -spec parse_select_item(list(sqlode@lexer:token()), sqlode@model:engine()) -> sqlode@query_ir:select_item_ex(). parse_select_item(Tokens, Engine) -> case Tokens of [{operator, <<"*"/utf8>>}] -> {star_ex, none}; [star] -> {star_ex, none}; [{ident, T}, dot, {operator, <<"*"/utf8>>}] -> {star_ex, {some, string:lowercase(T)}}; [{ident, T@1}, dot, star] -> {star_ex, {some, string:lowercase(T@1)}}; _ -> {Expr_tokens, Alias} = split_trailing_alias(Tokens), {expr_item, parse_expr(Expr_tokens, Engine), Alias} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 384). -spec parse_select_items(list(sqlode@lexer:token()), sqlode@model:engine()) -> list(sqlode@query_ir:select_item_ex()). parse_select_items(Tokens, Engine) -> _pipe = Tokens, _pipe@1 = split_on_top_commas(_pipe), gleam@list:map(_pipe@1, fun(Group) -> parse_select_item(Group, Engine) end). -file("src/sqlode/query_analyzer/expr_parser.gleam", 547). -spec collect_values_rows( list(sqlode@lexer:token()), list(list(sqlode@query_ir:expr())), sqlode@model:engine() ) -> list(list(sqlode@query_ir:expr())). collect_values_rows(Tokens, Acc, Engine) -> case Tokens of [l_paren | Rest] -> {Inner, After} = collect_parens(Rest), Row = gleam@list:map( split_on_top_commas(Inner), fun(Group) -> parse_expr(Group, Engine) end ), case After of [comma | More] -> collect_values_rows(More, [Row | Acc], Engine); _ -> lists:reverse([Row | Acc]) end; _ -> lists:reverse(Acc) end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 537). -spec parse_values_rows(list(sqlode@lexer:token()), sqlode@model:engine()) -> list(list(sqlode@query_ir:expr())). parse_values_rows(Body, Engine) -> case Body of [{keyword, <<"values"/utf8>>} | Rest] -> collect_values_rows(Rest, [], Engine); _ -> [] end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 523). -spec parse_values_from_body( list(sqlode@lexer:token()), list(sqlode@lexer:token()), sqlode@model:engine() ) -> {sqlode@query_ir:from_item_ex(), list(sqlode@lexer:token())}. parse_values_from_body(Body, After, Engine) -> Rows = parse_values_rows(Body, Engine), {Alias_opt, Col_aliases, Rest} = parse_aliased_column_list(After), Alias = gleam@option:unwrap(Alias_opt, <<""/utf8>>), {{from_values, Rows, Alias, Col_aliases}, Rest}. -file("src/sqlode/query_analyzer/expr_parser.gleam", 478). -spec parse_primary_from(list(sqlode@lexer:token()), sqlode@model:engine()) -> {sqlode@query_ir:from_item_ex(), list(sqlode@lexer:token())}. parse_primary_from(Tokens, Engine) -> case Tokens of [l_paren, {keyword, <<"values"/utf8>>} | Rest] -> {_, After} = collect_parens([{keyword, <<"values"/utf8>>} | Rest]), {Body, _} = collect_parens([{keyword, <<"values"/utf8>>} | Rest]), parse_values_from_body(Body, After, Engine); [l_paren, {keyword, <<"select"/utf8>>} | _] -> {Inner, After_rp} = collect_parens(drop_first(Tokens)), {Core, _} = parse_select_core(Inner, Engine), parse_subquery_alias(Core, After_rp, false); [l_paren, {keyword, <<"with"/utf8>>} | _] -> {Inner@1, After_rp@1} = collect_parens(drop_first(Tokens)), Stmt = parse_stmt(Inner@1, Engine), Core@1 = case Stmt of {select_stmt, _, C} -> C; _ -> empty_core() end, parse_subquery_alias(Core@1, After_rp@1, false); [{keyword, <<"lateral"/utf8>>}, l_paren, {keyword, <<"select"/utf8>>} | _] -> {Inner@2, After_rp@2} = collect_parens( drop_first(drop_first(Tokens)) ), {Core@2, _} = parse_select_core(Inner@2, Engine), parse_subquery_alias(Core@2, After_rp@2, true); [{ident, _}, dot, {ident, Name} | Rest@1] -> {Alias, Rest2} = parse_optional_alias(Rest@1), {{from_table, string:lowercase(Name), Alias}, Rest2}; [{ident, Name@1} | Rest@2] -> {Alias@1, Rest2@1} = parse_optional_alias(Rest@2), {{from_table, string:lowercase(Name@1), Alias@1}, Rest2@1}; [{quoted_ident, Name@2} | Rest@3] -> {Alias@2, Rest2@2} = parse_optional_alias(Rest@3), {{from_table, string:lowercase(Name@2), Alias@2}, Rest2@2}; _ -> {{from_table, <<""/utf8>>, none}, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 730). -spec parse_join_on(list(sqlode@lexer:token()), sqlode@model:engine()) -> {sqlode@query_ir:join_on(), list(sqlode@lexer:token())}. parse_join_on(Tokens, Engine) -> case Tokens of [{keyword, <<"on"/utf8>>} | Rest] -> {On_toks, After} = collect_until_keyword( Rest, [<<"join"/utf8>>, <<"inner"/utf8>>, <<"left"/utf8>>, <<"right"/utf8>>, <<"full"/utf8>>, <<"cross"/utf8>>, <<"natural"/utf8>>, <<"where"/utf8>>, <<"group"/utf8>>, <<"having"/utf8>>, <<"order"/utf8>>, <<"limit"/utf8>>, <<"offset"/utf8>>, <<"union"/utf8>>, <<"intersect"/utf8>>, <<"except"/utf8>>, <<"returning"/utf8>>, <<"window"/utf8>>] ), {{join_on_expr, parse_expr(On_toks, Engine)}, After}; [{keyword, <<"using"/utf8>>}, l_paren | Rest@1] -> {Cols, After@1} = collect_parens(Rest@1), {{join_using, parse_ident_list(Cols)}, After@1}; _ -> {join_no_condition, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 652). -spec parse_join_tail( sqlode@query_ir:from_item_ex(), list(sqlode@lexer:token()), sqlode@model:engine() ) -> sqlode@query_ir:from_item_ex(). parse_join_tail(Left, Tokens, Engine) -> case detect_join(Tokens) of {some, {Kind, After_kw, Lateral}} -> {Right_raw, After_right} = parse_primary_from(After_kw, Engine), {On, After_on} = parse_join_on(After_right, Engine), Right = parse_join_tail(Right_raw, After_on, Engine), parse_join_tail( {from_join, Left, Right, Kind, On, Lateral}, [], Engine ); none -> Left end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 470). -spec parse_from_element(list(sqlode@lexer:token()), sqlode@model:engine()) -> sqlode@query_ir:from_item_ex(). parse_from_element(Tokens, Engine) -> {First, After_first} = parse_primary_from(Tokens, Engine), parse_join_tail(First, After_first, Engine). -file("src/sqlode/query_analyzer/expr_parser.gleam", 455). -spec parse_from_clause(list(sqlode@lexer:token()), sqlode@model:engine()) -> {list(sqlode@query_ir:from_item_ex()), list(sqlode@lexer:token())}. parse_from_clause(Tokens, Engine) -> {Items_tokens, Rest} = collect_until_keyword( Tokens, [<<"where"/utf8>>, <<"group"/utf8>>, <<"having"/utf8>>, <<"order"/utf8>>, <<"limit"/utf8>>, <<"offset"/utf8>>, <<"union"/utf8>>, <<"intersect"/utf8>>, <<"except"/utf8>>, <<"window"/utf8>>, <<"returning"/utf8>>] ), Groups = split_on_top_commas(Items_tokens), From_items = gleam@list:map( Groups, fun(Group) -> parse_from_element(Group, Engine) end ), {From_items, Rest}. -file("src/sqlode/query_analyzer/expr_parser.gleam", 834). -spec parse_insert_source(list(sqlode@lexer:token()), sqlode@model:engine()) -> {sqlode@query_ir:insert_source(), list(sqlode@lexer:token())}. parse_insert_source(Tokens, Engine) -> case Tokens of [{keyword, <<"default"/utf8>>}, {keyword, <<"values"/utf8>>} | Rest] -> {insert_default_values, Rest}; [{keyword, <<"values"/utf8>>} | Rest@1] -> Rows = collect_values_rows(Rest@1, [], Engine), After = skip_values_rows(Rest@1), {{insert_values, Rows}, After}; [{keyword, <<"select"/utf8>>} | _] -> {Core, After@1} = parse_select_core(Tokens, Engine), {{insert_select, Core}, After@1}; [{keyword, <<"with"/utf8>>} | _] -> Stmt = parse_stmt(Tokens, Engine), case Stmt of {select_stmt, _, C} -> {{insert_select, C}, []}; _ -> {{insert_values, []}, Tokens} end; _ -> {{insert_values, []}, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 937). -spec parse_assignment(list(sqlode@lexer:token()), sqlode@model:engine()) -> {ok, sqlode@query_ir:assignment()} | {error, nil}. parse_assignment(Tokens, Engine) -> case Tokens of [{ident, Col}, {operator, <<"="/utf8>>} | Rest] -> {ok, {assignment, string:lowercase(Col), parse_expr(Rest, Engine)}}; [{quoted_ident, Col@1}, {operator, <<"="/utf8>>} | Rest@1] -> {ok, {assignment, string:lowercase(Col@1), parse_expr(Rest@1, Engine)}}; _ -> {error, nil} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 928). -spec parse_assignments(list(sqlode@lexer:token()), sqlode@model:engine()) -> list(sqlode@query_ir:assignment()). parse_assignments(Tokens, Engine) -> _pipe = Tokens, _pipe@1 = split_on_top_commas(_pipe), gleam@list:filter_map( _pipe@1, fun(Group) -> parse_assignment(Group, Engine) end ). -file("src/sqlode/query_analyzer/expr_parser.gleam", 805). -spec parse_on_duplicate_key_update( list(sqlode@lexer:token()), sqlode@model:engine() ) -> {list(sqlode@query_ir:assignment()), list(sqlode@lexer:token())}. parse_on_duplicate_key_update(Tokens, Engine) -> case Tokens of [{keyword, <<"on"/utf8>>}, {ident, Duplicate}, {keyword, <<"key"/utf8>>}, {keyword, <<"update"/utf8>>} | Rest] -> case string:lowercase(Duplicate) of <<"duplicate"/utf8>> -> {Assign_toks, After} = collect_until_keyword( Rest, [<<"returning"/utf8>>] ), {parse_assignments(Assign_toks, Engine), After}; _ -> {[], Tokens} end; _ -> {[], Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 994). -spec parse_returning(list(sqlode@lexer:token()), sqlode@model:engine()) -> list(sqlode@query_ir:select_item_ex()). parse_returning(Tokens, Engine) -> case skip_to_returning_or_end(Tokens) of [{keyword, <<"returning"/utf8>>} | Rest] -> Filtered = gleam@list:filter(Rest, fun(T) -> case T of semicolon -> false; _ -> true end end), parse_select_items(Filtered, Engine); _ -> [] end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 772). -spec parse_insert_target( list(sqlode@query_ir:cte_def()), list(sqlode@lexer:token()), sqlode@model:engine() ) -> sqlode@query_ir:stmt(). parse_insert_target(Ctes, Tokens, Engine) -> {Table_name, After_name} = read_qualified_name(Tokens), {Columns, After_cols} = case After_name of [l_paren | After_lp] -> {Inside, After} = collect_parens(After_lp), {parse_ident_list(Inside), After}; _ -> {[], After_name} end, {Source, After_source} = parse_insert_source(After_cols, Engine), {On_duplicate, After_on_dup} = case Engine of my_s_q_l -> parse_on_duplicate_key_update(After_source, Engine); _ -> {[], After_source} end, Returning = parse_returning(After_on_dup, Engine), {insert_stmt, Ctes, Table_name, Columns, Source, On_duplicate, Returning}. -file("src/sqlode/query_analyzer/expr_parser.gleam", 756). -spec parse_insert_body( list(sqlode@query_ir:cte_def()), list(sqlode@lexer:token()), sqlode@model:engine() ) -> sqlode@query_ir:stmt(). parse_insert_body(Ctes, Tokens, Engine) -> case Tokens of [{keyword, <<"into"/utf8>>} | After_into] -> parse_insert_target(Ctes, After_into, Engine); _ -> {unstructured_stmt, <<"INSERT without INTO"/utf8>>, [{keyword, <<"insert"/utf8>>} | Tokens]} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 886). -spec parse_update_body( list(sqlode@query_ir:cte_def()), list(sqlode@lexer:token()), sqlode@model:engine() ) -> sqlode@query_ir:stmt(). parse_update_body(Ctes, Tokens, Engine) -> {Table_name, After_name} = read_qualified_name(Tokens), {Alias, After_alias} = parse_optional_alias(After_name), case After_alias of [{keyword, <<"set"/utf8>>} | After_set] -> {Set_toks, After_set_block} = collect_until_keyword( After_set, [<<"from"/utf8>>, <<"where"/utf8>>, <<"returning"/utf8>>] ), Assignments = parse_assignments(Set_toks, Engine), {From_list, After_from} = case After_set_block of [{keyword, <<"from"/utf8>>} | Rest] -> parse_from_clause(Rest, Engine); _ -> {[], After_set_block} end, {Where_expr, After_where} = case After_from of [{keyword, <<"where"/utf8>>} | Rest@1] -> {Where_toks, After} = collect_until_keyword( Rest@1, [<<"returning"/utf8>>] ), {{some, parse_expr(Where_toks, Engine)}, After}; _ -> {none, After_from} end, Returning = parse_returning(After_where, Engine), {update_stmt, Ctes, Table_name, Alias, Assignments, From_list, Where_expr, Returning}; _ -> {unstructured_stmt, <<"UPDATE without SET"/utf8>>, [{keyword, <<"update"/utf8>>} | Tokens]} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 956). -spec parse_delete_body( list(sqlode@query_ir:cte_def()), list(sqlode@lexer:token()), sqlode@model:engine() ) -> sqlode@query_ir:stmt(). parse_delete_body(Ctes, Tokens, Engine) -> case Tokens of [{keyword, <<"from"/utf8>>} | After_from] -> {Table_name, After_name} = read_qualified_name(After_from), {Alias, After_alias} = parse_optional_alias(After_name), {Using_list, After_using} = case After_alias of [{keyword, <<"using"/utf8>>} | Rest] -> parse_from_clause(Rest, Engine); _ -> {[], After_alias} end, {Where_expr, After_where} = case After_using of [{keyword, <<"where"/utf8>>} | Rest@1] -> {Where_toks, After} = collect_until_keyword( Rest@1, [<<"returning"/utf8>>] ), {{some, parse_expr(Where_toks, Engine)}, After}; _ -> {none, After_using} end, Returning = parse_returning(After_where, Engine), {delete_stmt, Ctes, Table_name, Alias, Using_list, Where_expr, Returning}; _ -> {unstructured_stmt, <<"DELETE without FROM"/utf8>>, [{keyword, <<"delete"/utf8>>} | Tokens]} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1037). -spec parse_expr_list(list(sqlode@lexer:token()), sqlode@model:engine()) -> list(sqlode@query_ir:expr()). parse_expr_list(Tokens, Engine) -> _pipe = Tokens, _pipe@1 = split_on_top_commas(_pipe), gleam@list:map(_pipe@1, fun(Group) -> parse_expr(Group, Engine) end). -file("src/sqlode/query_analyzer/expr_parser.gleam", 1232). -spec parse_in_tail( sqlode@query_ir:expr(), list(sqlode@lexer:token()), boolean(), sqlode@model:engine() ) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_in_tail(Left, Tokens, Negated, Engine) -> case Tokens of [l_paren, {keyword, <<"select"/utf8>>} | _] -> {Inner, After} = collect_parens(drop_first(Tokens)), {Core, _} = parse_select_core(Inner, Engine), {{in_expr, Left, {in_subquery, Core}, Negated}, After}; [l_paren | Rest] -> {Inner@1, After@1} = collect_parens(Rest), case detect_slice_macro(Inner@1) of {some, Name} -> {{in_expr, Left, {in_slice_macro, Name}, Negated}, After@1}; none -> Values = parse_expr_list(Inner@1, Engine), {{in_expr, Left, {in_list, Values}, Negated}, After@1} end; _ -> {{raw_expr, <<"IN without paren list"/utf8>>, Tokens}, []} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1300). -spec parse_between_tail( sqlode@query_ir:expr(), list(sqlode@lexer:token()), boolean(), sqlode@model:engine() ) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_between_tail(Left, Tokens, Negated, Engine) -> {Low, Rest} = parse_additive(Tokens, Engine), case Rest of [{keyword, <<"and"/utf8>>} | After_and] -> {High, After} = parse_additive(After_and, Engine), {{between, Left, Low, High, Negated}, After}; _ -> {{raw_expr, <<"BETWEEN without AND"/utf8>>, Tokens}, []} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1680). -spec parse_filter_clause(list(sqlode@lexer:token()), sqlode@model:engine()) -> {gleam@option:option(sqlode@query_ir:expr()), list(sqlode@lexer:token())}. parse_filter_clause(Tokens, Engine) -> case Tokens of [{keyword, <<"filter"/utf8>>}, l_paren, {keyword, <<"where"/utf8>>} | Rest] -> {Inside, After} = collect_parens_after_where(Rest), {{some, parse_expr(Inside, Engine)}, After}; _ -> {none, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1775). -spec collect_case_branches( list(sqlode@lexer:token()), gleam@option:option(sqlode@query_ir:expr()), list(sqlode@query_ir:case_branch()), sqlode@model:engine() ) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. collect_case_branches(Tokens, Scrutinee, Branches, Engine) -> case Tokens of [{keyword, <<"when"/utf8>>} | Rest] -> {When_toks, After_when} = collect_until_keyword( Rest, [<<"then"/utf8>>] ), After_then = case After_when of [{keyword, <<"then"/utf8>>} | More] -> More; _ -> After_when end, {Then_toks, After_then_block} = collect_until_keyword( After_then, [<<"when"/utf8>>, <<"else"/utf8>>, <<"end"/utf8>>] ), Branch = {case_branch, parse_expr(When_toks, Engine), parse_expr(Then_toks, Engine)}, collect_case_branches( After_then_block, Scrutinee, [Branch | Branches], Engine ); [{keyword, <<"else"/utf8>>} | Rest@1] -> {Else_toks, After_else} = collect_until_keyword( Rest@1, [<<"end"/utf8>>] ), After_end = case After_else of [{keyword, <<"end"/utf8>>} | More@1] -> More@1; _ -> After_else end, {{'case', Scrutinee, lists:reverse(Branches), {some, parse_expr(Else_toks, Engine)}}, After_end}; [{keyword, <<"end"/utf8>>} | Rest@2] -> {{'case', Scrutinee, lists:reverse(Branches), none}, Rest@2}; _ -> {{'case', Scrutinee, lists:reverse(Branches), none}, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1757). -spec parse_case(list(sqlode@lexer:token()), sqlode@model:engine()) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_case(Tokens, Engine) -> {Scrutinee, Rest} = case Tokens of [{keyword, <<"when"/utf8>>} | _] -> {none, Tokens}; _ -> {Scr_toks, After} = collect_until_keyword( Tokens, [<<"when"/utf8>>, <<"else"/utf8>>, <<"end"/utf8>>] ), case Scr_toks of [] -> {none, After}; _ -> {{some, parse_expr(Scr_toks, Engine)}, After} end end, collect_case_branches(Rest, Scrutinee, [], Engine).