-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/1, parse_select_core/1, parse_expr/1]). -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" " 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", 62). -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", 384). -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", 357). -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", 572). -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", 550). -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", 608). -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", 756). -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", 1120). -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", 1306). -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", 1635). -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", 1631). -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", 154). -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", 1666). -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", 1660). -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", 524). -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", 500). -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", 1279). -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", 1275). -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", 1262). -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", 1497). -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", 1694). -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", 1688). -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", 1724). -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", 1717). -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", 1747). -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", 1761). -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", 1768). -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", 1783). -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", 1798). -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", 1792). -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", 1814). -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", 125). -spec parse_cte_after_name(binary(), list(sqlode@lexer:token())) -> gleam@option:option({sqlode@query_ir:cte_def(), list(sqlode@lexer:token())}). parse_cte_after_name(Name, Tokens) -> {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), {some, {{cte_def, string:lowercase(Name), Columns, Body, false}, After@1}}; _ -> none end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 41). ?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@query_ir:stmt(). parse_stmt(Tokens) -> {Ctes, Recursive, After_ctes} = parse_with_clause(Tokens), case After_ctes of [{keyword, <<"select"/utf8>>} | _] -> {Core, _} = parse_select_core(After_ctes), {select_stmt, attach_recursive(Ctes, Recursive), Core}; [{keyword, <<"insert"/utf8>>} | Rest] -> parse_insert_body(attach_recursive(Ctes, Recursive), Rest); [{keyword, <<"update"/utf8>>} | Rest@1] -> parse_update_body(attach_recursive(Ctes, Recursive), Rest@1); [{keyword, <<"delete"/utf8>>} | Rest@2] -> parse_delete_body(attach_recursive(Ctes, Recursive), Rest@2); _ -> {unstructured_stmt, <<"unrecognised top-level statement"/utf8>>, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 84). -spec parse_with_clause(list(sqlode@lexer:token())) -> {list(sqlode@query_ir:cte_def()), boolean(), list(sqlode@lexer:token())}. parse_with_clause(Tokens) -> case Tokens of [{keyword, <<"with"/utf8>>}, {keyword, <<"recursive"/utf8>>} | Rest] -> {Ctes, Remaining} = parse_cte_list(Rest, []), {Ctes, true, Remaining}; [{keyword, <<"with"/utf8>>} | Rest@1] -> {Ctes@1, Remaining@1} = parse_cte_list(Rest@1, []), {Ctes@1, false, Remaining@1}; _ -> {[], false, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 100). -spec parse_cte_list( list(sqlode@lexer:token()), list(sqlode@query_ir:cte_def()) ) -> {list(sqlode@query_ir:cte_def()), list(sqlode@lexer:token())}. parse_cte_list(Tokens, Acc) -> case parse_single_cte(Tokens) of {some, {Cte, After}} -> case After of [comma | More] -> parse_cte_list(More, [Cte | Acc]); _ -> {lists:reverse([Cte | Acc]), After} end; none -> {lists:reverse(Acc), Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 114). -spec parse_single_cte(list(sqlode@lexer:token())) -> gleam@option:option({sqlode@query_ir:cte_def(), list(sqlode@lexer:token())}). parse_single_cte(Tokens) -> case Tokens of [{ident, Name} | After_name] -> parse_cte_after_name(Name, After_name); [{quoted_ident, Name@1} | After_name@1] -> parse_cte_after_name(Name@1, After_name@1); _ -> none end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 277). -spec parse_set_op(list(sqlode@lexer:token())) -> {gleam@option:option(sqlode@query_ir:set_op()), list(sqlode@lexer:token())}. parse_set_op(Tokens) -> 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), 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", 170). -spec parse_select_core(list(sqlode@lexer:token())) -> {sqlode@query_ir:select_core(), list(sqlode@lexer:token())}. parse_select_core(Tokens) -> {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), {From_list, Rest_after_from} = case Rest_after_select of [{keyword, <<"from"/utf8>>} | Rest@2] -> parse_from_clause(Rest@2); _ -> {[], 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)}, 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), 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)}, 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), After@4}; _ -> {[], Rest_after_window} end, {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>>] ), {{some, parse_expr(L_toks)}, After@5}; _ -> {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)}, After@6}; _ -> {none, Rest_after_limit} end, {Set_op, Remaining} = parse_set_op(Rest_after_offset), {{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", 1525). -spec parse_window_spec(list(sqlode@lexer:token())) -> sqlode@query_ir:window_spec(). parse_window_spec(Tokens) -> {_, 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), 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), 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", 300). -spec parse_order_keys(list(sqlode@lexer:token())) -> list(sqlode@query_ir:order_key()). parse_order_keys(Tokens) -> _pipe = Tokens, _pipe@1 = split_on_top_commas(_pipe), gleam@list:map(_pipe@1, fun parse_order_key/1). -file("src/sqlode/query_analyzer/expr_parser.gleam", 306). -spec parse_order_key(list(sqlode@lexer:token())) -> sqlode@query_ir:order_key(). parse_order_key(Tokens) -> 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), Descending, Nulls}. -file("src/sqlode/query_analyzer/expr_parser.gleam", 886). -spec parse_expr(list(sqlode@lexer:token())) -> sqlode@query_ir:expr(). parse_expr(Tokens) -> {Expr, Rest} = parse_or(trim_noise(Tokens)), case Rest of [] -> Expr; _ -> {raw_expr, <<"trailing tokens after expression"/utf8>>, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 904). -spec parse_or(list(sqlode@lexer:token())) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_or(Tokens) -> {Left, Rest} = parse_and(Tokens), parse_or_tail(Left, Rest). -file("src/sqlode/query_analyzer/expr_parser.gleam", 909). -spec parse_or_tail(sqlode@query_ir:expr(), list(sqlode@lexer:token())) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_or_tail(Left, Tokens) -> case Tokens of [{keyword, <<"or"/utf8>>} | Rest] -> {Right, After} = parse_and(Rest), parse_or_tail({binary, <<"or"/utf8>>, Left, Right}, After); _ -> {Left, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 922). -spec parse_and(list(sqlode@lexer:token())) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_and(Tokens) -> {Left, Rest} = parse_not(Tokens), parse_and_tail(Left, Rest). -file("src/sqlode/query_analyzer/expr_parser.gleam", 927). -spec parse_and_tail(sqlode@query_ir:expr(), list(sqlode@lexer:token())) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_and_tail(Left, Tokens) -> case Tokens of [{keyword, <<"and"/utf8>>} | Rest] -> {Right, After} = parse_not(Rest), parse_and_tail({binary, <<"and"/utf8>>, Left, Right}, After); _ -> {Left, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 943). -spec parse_not(list(sqlode@lexer:token())) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_not(Tokens) -> case Tokens of [{keyword, <<"not"/utf8>>} | Rest] -> {Inner, After} = parse_not(Rest), {{unary, <<"not"/utf8>>, Inner}, After}; _ -> parse_comparison(Tokens) end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 953). -spec parse_comparison(list(sqlode@lexer:token())) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_comparison(Tokens) -> {Left, Rest} = parse_additive(Tokens), parse_comparison_tail(Left, Rest). -file("src/sqlode/query_analyzer/expr_parser.gleam", 960). -spec parse_comparison_tail(sqlode@query_ir:expr(), list(sqlode@lexer:token())) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_comparison_tail(Left, Tokens) -> 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), {{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), {{quantified, Op, Left, q_all, Right@1}, After@1}; _ -> {Right@2, After@2} = parse_additive(Rest), {{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); [{keyword, <<"in"/utf8>>} | Rest@10] -> parse_in_tail(Left, Rest@10, false); [{keyword, <<"not"/utf8>>}, {keyword, <<"between"/utf8>>} | Rest@11] -> parse_between_tail(Left, Rest@11, true); [{keyword, <<"between"/utf8>>} | Rest@12] -> parse_between_tail(Left, Rest@12, false); [{keyword, <<"not"/utf8>>}, {keyword, <<"like"/utf8>>} | Rest@13] -> parse_like_tail(Left, Rest@13, like, true); [{keyword, <<"like"/utf8>>} | Rest@14] -> parse_like_tail(Left, Rest@14, like, false); [{keyword, <<"not"/utf8>>}, {keyword, <<"ilike"/utf8>>} | Rest@15] -> parse_like_tail(Left, Rest@15, ilike, true); [{keyword, <<"ilike"/utf8>>} | Rest@16] -> parse_like_tail(Left, Rest@16, ilike, false); _ -> {Left, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1156). -spec parse_like_tail( sqlode@query_ir:expr(), list(sqlode@lexer:token()), sqlode@query_ir:like_op(), boolean() ) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_like_tail(Left, Tokens, Op, Negated) -> {Pattern, Rest} = parse_additive(Tokens), {Escape, Rest2} = case Rest of [{keyword, <<"escape"/utf8>>} | More] -> {Esc, After} = parse_additive(More), {{some, Esc}, After}; _ -> {none, Rest} end, {{like_expr, Left, Op, Pattern, Escape, Negated}, Rest2}. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1182). -spec parse_additive(list(sqlode@lexer:token())) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_additive(Tokens) -> {Left, Rest} = parse_multiplicative(Tokens), parse_additive_tail(Left, Rest). -file("src/sqlode/query_analyzer/expr_parser.gleam", 1189). -spec parse_additive_tail(sqlode@query_ir:expr(), list(sqlode@lexer:token())) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_additive_tail(Left, Tokens) -> 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), parse_additive_tail({binary, Op, Left, Right}, After); _ -> {Left, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1213). -spec parse_multiplicative(list(sqlode@lexer:token())) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_multiplicative(Tokens) -> {Left, Rest} = parse_unary(Tokens), parse_multiplicative_tail(Left, Rest). -file("src/sqlode/query_analyzer/expr_parser.gleam", 1220). -spec parse_multiplicative_tail( sqlode@query_ir:expr(), list(sqlode@lexer:token()) ) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_multiplicative_tail(Left, Tokens) -> case Tokens of [star | Rest] -> {Right, After} = parse_unary(Rest), parse_multiplicative_tail( {binary, <<"*"/utf8>>, Left, Right}, After ); [{operator, Op} | Rest@1] when (Op =:= <<"/"/utf8>>) orelse (Op =:= <<"%"/utf8>>) -> {Right@1, After@1} = parse_unary(Rest@1), parse_multiplicative_tail({binary, Op, Left, Right@1}, After@1); _ -> {Left, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1243). -spec parse_unary(list(sqlode@lexer:token())) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_unary(Tokens) -> case Tokens of [{operator, <<"-"/utf8>>} | Rest] -> {Inner, After} = parse_unary(Rest), {{unary, <<"-"/utf8>>, Inner}, After}; [{operator, <<"+"/utf8>>} | Rest@1] -> {Inner@1, After@1} = parse_unary(Rest@1), {{unary, <<"+"/utf8>>, Inner@1}, After@1}; _ -> parse_cast(Tokens) end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1257). -spec parse_cast(list(sqlode@lexer:token())) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_cast(Tokens) -> {Atom, Rest} = parse_atom(Tokens), parse_cast_tail(Atom, Rest). -file("src/sqlode/query_analyzer/expr_parser.gleam", 1320). -spec parse_atom(list(sqlode@lexer:token())) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_atom(Tokens) -> 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); [{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), 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), {{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), {{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), {{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)}, After@4}; [l_paren, {keyword, <<"select"/utf8>>} | _] -> {Inside@5, After@5} = collect_parens(drop_first(Tokens)), {Core@3, _} = parse_select_core(Inside@5), {{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), 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), After@7}; Many -> {{tuple, gleam@list:map(Many, fun parse_expr/1)}, 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]) end; [{ident, Name@1}, l_paren | Rest@15] -> parse_function_call(Name@1, Rest@15); [{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", 1455). -spec parse_function_call(binary(), list(sqlode@lexer:token())) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_function_call(Name, Tokens) -> {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)} end ) end, {Filter, After_filter} = parse_filter_clause(After), {Over, After_over} = parse_over_clause(After_filter), {{func, string:lowercase(Name), Args, Distinct, Filter, Over}, After_over}. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1505). -spec parse_over_clause(list(sqlode@lexer:token())) -> {gleam@option:option(sqlode@query_ir:window_spec()), list(sqlode@lexer:token())}. parse_over_clause(Tokens) -> case Tokens of [{keyword, <<"over"/utf8>>}, l_paren | Rest] -> {Inside, After} = collect_parens(Rest), {{some, parse_window_spec(Inside)}, After}; [{keyword, <<"over"/utf8>>}, {ident, _} | Rest@1] -> {{some, {window_spec, [], [], none}}, Rest@1}; _ -> {none, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 342). -spec parse_select_item(list(sqlode@lexer:token())) -> sqlode@query_ir:select_item_ex(). parse_select_item(Tokens) -> 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), Alias} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 336). -spec parse_select_items(list(sqlode@lexer:token())) -> list(sqlode@query_ir:select_item_ex()). parse_select_items(Tokens) -> _pipe = Tokens, _pipe@1 = split_on_top_commas(_pipe), gleam@list:map(_pipe@1, fun parse_select_item/1). -file("src/sqlode/query_analyzer/expr_parser.gleam", 483). -spec collect_values_rows( list(sqlode@lexer:token()), list(list(sqlode@query_ir:expr())) ) -> list(list(sqlode@query_ir:expr())). collect_values_rows(Tokens, Acc) -> case Tokens of [l_paren | Rest] -> {Inner, After} = collect_parens(Rest), Row = gleam@list:map(split_on_top_commas(Inner), fun parse_expr/1), case After of [comma | More] -> collect_values_rows(More, [Row | Acc]); _ -> lists:reverse([Row | Acc]) end; _ -> lists:reverse(Acc) end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 476). -spec parse_values_rows(list(sqlode@lexer:token())) -> list(list(sqlode@query_ir:expr())). parse_values_rows(Body) -> case Body of [{keyword, <<"values"/utf8>>} | Rest] -> collect_values_rows(Rest, []); _ -> [] end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 463). -spec parse_values_from_body( list(sqlode@lexer:token()), list(sqlode@lexer:token()) ) -> {sqlode@query_ir:from_item_ex(), list(sqlode@lexer:token())}. parse_values_from_body(Body, After) -> Rows = parse_values_rows(Body), {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", 419). -spec parse_primary_from(list(sqlode@lexer:token())) -> {sqlode@query_ir:from_item_ex(), list(sqlode@lexer:token())}. parse_primary_from(Tokens) -> 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); [l_paren, {keyword, <<"select"/utf8>>} | _] -> {Inner, After_rp} = collect_parens(drop_first(Tokens)), {Core, _} = parse_select_core(Inner), 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), 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), 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", 660). -spec parse_join_on(list(sqlode@lexer:token())) -> {sqlode@query_ir:join_on(), list(sqlode@lexer:token())}. parse_join_on(Tokens) -> 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)}, 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", 584). -spec parse_join_tail( sqlode@query_ir:from_item_ex(), list(sqlode@lexer:token()) ) -> sqlode@query_ir:from_item_ex(). parse_join_tail(Left, Tokens) -> case detect_join(Tokens) of {some, {Kind, After_kw, Lateral}} -> {Right_raw, After_right} = parse_primary_from(After_kw), {On, After_on} = parse_join_on(After_right), Right = parse_join_tail(Right_raw, After_on), parse_join_tail({from_join, Left, Right, Kind, On, Lateral}, []); none -> Left end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 414). -spec parse_from_element(list(sqlode@lexer:token())) -> sqlode@query_ir:from_item_ex(). parse_from_element(Tokens) -> {First, After_first} = parse_primary_from(Tokens), parse_join_tail(First, After_first). -file("src/sqlode/query_analyzer/expr_parser.gleam", 401). -spec parse_from_clause(list(sqlode@lexer:token())) -> {list(sqlode@query_ir:from_item_ex()), list(sqlode@lexer:token())}. parse_from_clause(Tokens) -> {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 parse_from_element/1), {From_items, Rest}. -file("src/sqlode/query_analyzer/expr_parser.gleam", 723). -spec parse_insert_source(list(sqlode@lexer:token())) -> {sqlode@query_ir:insert_source(), list(sqlode@lexer:token())}. parse_insert_source(Tokens) -> 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, []), After = skip_to_returning_or_end(Rest@1), {{insert_values, Rows}, After}; [{keyword, <<"select"/utf8>>} | _] -> {Core, After@1} = parse_select_core(Tokens), {{insert_select, Core}, After@1}; [{keyword, <<"with"/utf8>>} | _] -> Stmt = parse_stmt(Tokens), 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", 811). -spec parse_assignment(list(sqlode@lexer:token())) -> {ok, sqlode@query_ir:assignment()} | {error, nil}. parse_assignment(Tokens) -> case Tokens of [{ident, Col}, {operator, <<"="/utf8>>} | Rest] -> {ok, {assignment, string:lowercase(Col), parse_expr(Rest)}}; [{quoted_ident, Col@1}, {operator, <<"="/utf8>>} | Rest@1] -> {ok, {assignment, string:lowercase(Col@1), parse_expr(Rest@1)}}; _ -> {error, nil} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 805). -spec parse_assignments(list(sqlode@lexer:token())) -> list(sqlode@query_ir:assignment()). parse_assignments(Tokens) -> _pipe = Tokens, _pipe@1 = split_on_top_commas(_pipe), gleam@list:filter_map(_pipe@1, fun parse_assignment/1). -file("src/sqlode/query_analyzer/expr_parser.gleam", 866). -spec parse_returning(list(sqlode@lexer:token())) -> list(sqlode@query_ir:select_item_ex()). parse_returning(Tokens) -> case 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); _ -> [] end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 700). -spec parse_insert_target( list(sqlode@query_ir:cte_def()), list(sqlode@lexer:token()) ) -> sqlode@query_ir:stmt(). parse_insert_target(Ctes, Tokens) -> {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), Returning = parse_returning(After_source), {insert_stmt, Ctes, Table_name, Columns, Source, Returning}. -file("src/sqlode/query_analyzer/expr_parser.gleam", 685). -spec parse_insert_body( list(sqlode@query_ir:cte_def()), list(sqlode@lexer:token()) ) -> sqlode@query_ir:stmt(). parse_insert_body(Ctes, Tokens) -> case Tokens of [{keyword, <<"into"/utf8>>} | After_into] -> parse_insert_target(Ctes, After_into); _ -> {unstructured_stmt, <<"INSERT without INTO"/utf8>>, [{keyword, <<"insert"/utf8>>} | Tokens]} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 764). -spec parse_update_body( list(sqlode@query_ir:cte_def()), list(sqlode@lexer:token()) ) -> sqlode@query_ir:stmt(). parse_update_body(Ctes, Tokens) -> {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), {From_list, After_from} = case After_set_block of [{keyword, <<"from"/utf8>>} | Rest] -> parse_from_clause(Rest); _ -> {[], 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)}, After}; _ -> {none, After_from} end, Returning = parse_returning(After_where), {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", 829). -spec parse_delete_body( list(sqlode@query_ir:cte_def()), list(sqlode@lexer:token()) ) -> sqlode@query_ir:stmt(). parse_delete_body(Ctes, Tokens) -> 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); _ -> {[], 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)}, After}; _ -> {none, After_using} end, Returning = parse_returning(After_where), {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", 898). -spec parse_expr_list(list(sqlode@lexer:token())) -> list(sqlode@query_ir:expr()). parse_expr_list(Tokens) -> _pipe = Tokens, _pipe@1 = split_on_top_commas(_pipe), gleam@list:map(_pipe@1, fun parse_expr/1). -file("src/sqlode/query_analyzer/expr_parser.gleam", 1071). -spec parse_in_tail( sqlode@query_ir:expr(), list(sqlode@lexer:token()), boolean() ) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_in_tail(Left, Tokens, Negated) -> case Tokens of [l_paren, {keyword, <<"select"/utf8>>} | _] -> {Inner, After} = collect_parens(drop_first(Tokens)), {Core, _} = parse_select_core(Inner), {{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), {{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", 1138). -spec parse_between_tail( sqlode@query_ir:expr(), list(sqlode@lexer:token()), boolean() ) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_between_tail(Left, Tokens, Negated) -> {Low, Rest} = parse_additive(Tokens), case Rest of [{keyword, <<"and"/utf8>>} | After_and] -> {High, After} = parse_additive(After_and), {{between, Left, Low, High, Negated}, After}; _ -> {{raw_expr, <<"BETWEEN without AND"/utf8>>, Tokens}, []} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1485). -spec parse_filter_clause(list(sqlode@lexer:token())) -> {gleam@option:option(sqlode@query_ir:expr()), list(sqlode@lexer:token())}. parse_filter_clause(Tokens) -> case Tokens of [{keyword, <<"filter"/utf8>>}, l_paren, {keyword, <<"where"/utf8>>} | Rest] -> {Inside, After} = collect_parens_after_where(Rest), {{some, parse_expr(Inside)}, After}; _ -> {none, Tokens} end. -file("src/sqlode/query_analyzer/expr_parser.gleam", 1572). -spec collect_case_branches( list(sqlode@lexer:token()), gleam@option:option(sqlode@query_ir:expr()), list(sqlode@query_ir:case_branch()) ) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. collect_case_branches(Tokens, Scrutinee, Branches) -> 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), parse_expr(Then_toks)}, collect_case_branches( After_then_block, Scrutinee, [Branch | Branches] ); [{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)}}, 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", 1557). -spec parse_case(list(sqlode@lexer:token())) -> {sqlode@query_ir:expr(), list(sqlode@lexer:token())}. parse_case(Tokens) -> {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)}, After} end end, collect_case_branches(Rest, Scrutinee, []).