Current section

Files

Jump to
sqlode src sqlode internal query_analyzer expr_parser.gleam
Raw

src/sqlode/internal/query_analyzer/expr_parser.gleam

//// Recursive-descent parser that turns a list of SQL tokens into the
//// expression-aware IR (`query_ir.Expr` / `query_ir.SelectCore` /
//// `query_ir.Stmt`).
////
//// The parser is deliberately permissive: it models the SQL subset
//// sqlode needs to reason about (the constructs exercised by the
//// fixtures in `test/fixtures/complex_sql/`) and falls back to
//// `query_ir.RawExpr` / `query_ir.UnstructuredStmt` with an explicit
//// `reason` string when it hits a construct it does not understand.
//// The downstream analyzer surfaces these as
//// `AnalysisError.UnsupportedExpression`, so "silent fallback to
//// StringType" never happens — every gap is tied to a concrete IR
//// node an operator can point at.
////
//// Every public parsing entry takes an `engine: model.Engine` parameter
//// so MySQL-only constructs (`ON DUPLICATE KEY UPDATE`,
//// `LIMIT offset, count`) can be recognised without polluting the
//// PostgreSQL / SQLite paths. The engine is threaded through every
//// internal helper that (directly or transitively) parses another
//// expression, select core, or statement. Pure token-shape helpers
//// (paren collection, comma splitting, keyword scanning, etc.) do not
//// receive it — they are dialect-agnostic.
////
//// Precedence roughly follows PostgreSQL's operator table:
////
//// 1. OR
//// 2. AND
//// 3. NOT
//// 4. IS [NOT] NULL/TRUE/FALSE, IS [NOT] DISTINCT FROM
//// 5. =, <>, !=, <, >, <=, >=, LIKE, ILIKE, IN, BETWEEN, SIMILAR TO,
//// @>, <@, ?|, ?&, &&
//// 6. +, -, ||, JSON ops (->, ->>, #>, #>>)
//// 7. *, /, %
//// 8. unary -, +
//// 9. ::type cast
//// 10. function calls / atoms
import gleam/int
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/string
import sqlode/internal/lexer
import sqlode/internal/model
import sqlode/internal/naming
import sqlode/internal/query_analyzer/token_utils
import sqlode/internal/query_ir
/// Parse a full statement from its token list. Never fails; unknown
/// constructs surface as `UnstructuredStmt(reason, tokens)` with the
/// raw tokens preserved for legacy passes and the reason string
/// bubbled up to analyzer diagnostics.
pub fn parse_stmt(
tokens: List(lexer.Token),
engine: model.Engine,
) -> query_ir.Stmt {
let #(ctes, recursive, after_ctes) = parse_with_clause(tokens, engine)
case after_ctes {
[lexer.Keyword("select"), ..] -> {
let #(core, _rest) = parse_select_core(after_ctes, engine)
query_ir.SelectStmt(ctes: attach_recursive(ctes, recursive), core: core)
}
[lexer.Keyword("insert"), ..rest] ->
parse_insert_body(attach_recursive(ctes, recursive), rest, engine)
[lexer.Keyword("update"), ..rest] ->
parse_update_body(attach_recursive(ctes, recursive), rest, engine)
[lexer.Keyword("delete"), ..rest] ->
parse_delete_body(attach_recursive(ctes, recursive), rest, engine)
_ ->
query_ir.UnstructuredStmt(
reason: "unrecognised top-level statement",
tokens: tokens,
)
}
}
fn attach_recursive(
ctes: List(query_ir.CteDef),
recursive: Bool,
) -> List(query_ir.CteDef) {
case recursive {
False -> ctes
True ->
list.map(ctes, fn(cte) {
query_ir.CteDef(
name: cte.name,
columns: cte.columns,
body: cte.body,
recursive: True,
)
})
}
}
// ============================================================
// WITH-clause parsing
// ============================================================
fn parse_with_clause(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(List(query_ir.CteDef), Bool, List(lexer.Token)) {
case tokens {
[lexer.Keyword("with"), lexer.Keyword("recursive"), ..rest] -> {
let #(ctes, remaining) = parse_cte_list(rest, [], engine)
#(ctes, True, remaining)
}
[lexer.Keyword("with"), ..rest] -> {
let #(ctes, remaining) = parse_cte_list(rest, [], engine)
#(ctes, False, remaining)
}
_ -> #([], False, tokens)
}
}
fn parse_cte_list(
tokens: List(lexer.Token),
acc: List(query_ir.CteDef),
engine: model.Engine,
) -> #(List(query_ir.CteDef), List(lexer.Token)) {
case parse_single_cte(tokens, engine) {
Some(#(cte, after)) ->
case after {
[lexer.Comma, ..more] -> parse_cte_list(more, [cte, ..acc], engine)
_ -> #(list.reverse([cte, ..acc]), after)
}
None -> #(list.reverse(acc), tokens)
}
}
fn parse_single_cte(
tokens: List(lexer.Token),
engine: model.Engine,
) -> Option(#(query_ir.CteDef, List(lexer.Token))) {
case tokens {
[lexer.Ident(name), ..after_name] ->
parse_cte_after_name(name, after_name, engine)
[lexer.QuotedIdent(name), ..after_name] ->
parse_cte_after_name(name, after_name, engine)
_ -> None
}
}
fn parse_cte_after_name(
name: String,
tokens: List(lexer.Token),
engine: model.Engine,
) -> Option(#(query_ir.CteDef, List(lexer.Token))) {
let #(columns, after_cols) = case tokens {
[lexer.LParen, ..after_lp] -> {
let #(inside, after) = collect_parens(after_lp)
#(parse_ident_list(inside), after)
}
_ -> #([], tokens)
}
case after_cols {
[lexer.Keyword("as"), lexer.LParen, ..after_as_lp] -> {
let #(inner, after) = collect_parens(after_as_lp)
let body = parse_stmt(inner, engine)
Some(#(
query_ir.CteDef(
name: string.lowercase(name),
columns: columns,
body: body,
recursive: False,
),
after,
))
}
_ -> None
}
}
fn parse_ident_list(tokens: List(lexer.Token)) -> List(String) {
tokens
|> split_on_top_commas()
|> list.filter_map(fn(group) {
case group {
[lexer.Ident(n)] -> Ok(string.lowercase(n))
[lexer.QuotedIdent(n)] -> Ok(string.lowercase(n))
_ -> Error(Nil)
}
})
}
// ============================================================
// SELECT parsing
// ============================================================
pub fn parse_select_core(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.SelectCore, List(lexer.Token)) {
let #(distinct, after_distinct) = case tokens {
[lexer.Keyword("select"), lexer.Keyword("distinct"), ..rest] -> #(
True,
rest,
)
[lexer.Keyword("select"), ..rest] -> #(False, rest)
_ -> #(False, tokens)
}
let #(select_tokens, rest_after_select) =
collect_until_keyword(after_distinct, [
"from", "where", "group", "having", "order", "limit", "offset", "union",
"intersect", "except", "window",
])
let items = parse_select_items(select_tokens, engine)
let #(from_list, rest_after_from) = case rest_after_select {
[lexer.Keyword("from"), ..rest] -> parse_from_clause(rest, engine)
_ -> #([], rest_after_select)
}
let #(where_expr, rest_after_where) = case rest_after_from {
[lexer.Keyword("where"), ..rest] -> {
let #(where_toks, after) =
collect_until_keyword(rest, [
"group", "having", "order", "limit", "offset", "union", "intersect",
"except", "window",
])
#(Some(parse_expr(where_toks, engine)), after)
}
_ -> #(None, rest_after_from)
}
let #(group_by, rest_after_group) = case rest_after_where {
[lexer.Keyword("group"), lexer.Keyword("by"), ..rest] -> {
let #(gb_toks, after) =
collect_until_keyword(rest, [
"having", "order", "limit", "offset", "union", "intersect", "except",
"window",
])
#(parse_expr_list(gb_toks, engine), after)
}
_ -> #([], rest_after_where)
}
let #(having_expr, rest_after_having) = case rest_after_group {
[lexer.Keyword("having"), ..rest] -> {
let #(h_toks, after) =
collect_until_keyword(rest, [
"order", "limit", "offset", "union", "intersect", "except", "window",
])
#(Some(parse_expr(h_toks, engine)), after)
}
_ -> #(None, rest_after_group)
}
let rest_after_window = case rest_after_having {
[lexer.Keyword("window"), ..rest] -> {
let #(_, after) =
collect_until_keyword(rest, [
"order", "limit", "offset", "union", "intersect", "except",
])
after
}
_ -> rest_after_having
}
let #(order_by, rest_after_order) = case rest_after_window {
[lexer.Keyword("order"), lexer.Keyword("by"), ..rest] -> {
let #(o_toks, after) =
collect_until_keyword(rest, [
"limit", "offset", "union", "intersect", "except",
])
#(parse_order_keys(o_toks, engine), after)
}
_ -> #([], rest_after_window)
}
// LIMIT handling. MySQL additionally supports the two-argument form
// `LIMIT offset, count` which assigns in the opposite order from
// PostgreSQL's `LIMIT count OFFSET offset`. For MySQL we split the
// collected LIMIT tokens on a top-level comma and populate
// `offset` / `limit` accordingly; if no comma is present the single
// expression is a plain count, matching the other dialects.
let #(limit, offset_from_limit, rest_after_limit) = case rest_after_order {
[lexer.Keyword("limit"), ..rest] -> {
let #(l_toks, after) =
collect_until_keyword(rest, ["offset", "union", "intersect", "except"])
case engine {
model.MySQL -> {
case split_on_top_commas(l_toks) {
[offset_toks, count_toks] -> {
// MySQL `LIMIT a, b` means offset=a, count=b — the
// OPPOSITE assignment from `LIMIT a OFFSET b`.
#(
Some(parse_expr(count_toks, engine)),
Some(parse_expr(offset_toks, engine)),
after,
)
}
_ -> #(Some(parse_expr(l_toks, engine)), None, after)
}
}
_ -> #(Some(parse_expr(l_toks, engine)), None, after)
}
}
_ -> #(None, None, rest_after_order)
}
let #(offset, rest_after_offset) = case rest_after_limit {
[lexer.Keyword("offset"), ..rest] -> {
let #(o_toks, after) =
collect_until_keyword(rest, ["union", "intersect", "except"])
#(Some(parse_expr(o_toks, engine)), after)
}
_ -> #(offset_from_limit, rest_after_limit)
}
let #(set_op, remaining) = parse_set_op(rest_after_offset, engine)
#(
query_ir.SelectCore(
distinct: distinct,
select_items: items,
from: from_list,
where_: where_expr,
group_by: group_by,
having: having_expr,
order_by: order_by,
limit: limit,
offset: offset,
set_op: set_op,
),
remaining,
)
}
fn parse_set_op(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(Option(query_ir.SetOp), List(lexer.Token)) {
case tokens {
[lexer.Keyword(kw), ..rest]
if kw == "union" || kw == "intersect" || kw == "except"
-> {
let #(all, rest2) = case rest {
[lexer.Keyword("all"), ..more] -> #(True, more)
_ -> #(False, rest)
}
let #(core, after) = parse_select_core(rest2, engine)
let kind = case kw {
"union" -> query_ir.Union
"intersect" -> query_ir.Intersect
_ -> query_ir.Except
}
#(Some(query_ir.SetOp(kind: kind, all: all, right: core)), after)
}
_ -> #(None, tokens)
}
}
fn parse_order_keys(
tokens: List(lexer.Token),
engine: model.Engine,
) -> List(query_ir.OrderKey) {
tokens
|> split_on_top_commas()
|> list.map(fn(group) { parse_order_key(group, engine) })
}
fn parse_order_key(
tokens: List(lexer.Token),
engine: model.Engine,
) -> query_ir.OrderKey {
let reversed = list.reverse(tokens)
let #(nulls, reversed2) = case reversed {
[lexer.Keyword("first"), lexer.Keyword("nulls"), ..rest] -> #(
Some(query_ir.NullsFirst),
rest,
)
[lexer.Keyword("last"), lexer.Keyword("nulls"), ..rest] -> #(
Some(query_ir.NullsLast),
rest,
)
_ -> #(None, reversed)
}
let #(descending, reversed3) = case reversed2 {
[lexer.Keyword("desc"), ..rest] -> #(True, rest)
[lexer.Keyword("asc"), ..rest] -> #(False, rest)
_ -> #(False, reversed2)
}
let expr_tokens = list.reverse(reversed3)
query_ir.OrderKey(
expr: parse_expr(expr_tokens, engine),
descending: descending,
nulls: nulls,
)
}
// ============================================================
// SELECT items
// ============================================================
fn parse_select_items(
tokens: List(lexer.Token),
engine: model.Engine,
) -> List(query_ir.SelectItemEx) {
tokens
|> split_on_top_commas()
|> list.map(fn(group) { parse_select_item(group, engine) })
}
fn parse_select_item(
tokens: List(lexer.Token),
engine: model.Engine,
) -> query_ir.SelectItemEx {
case tokens {
[lexer.Operator("*")] -> query_ir.StarEx(table_prefix: None)
[lexer.Star] -> query_ir.StarEx(table_prefix: None)
[lexer.Ident(t), lexer.Dot, lexer.Operator("*")] ->
query_ir.StarEx(table_prefix: Some(string.lowercase(t)))
[lexer.Ident(t), lexer.Dot, lexer.Star] ->
query_ir.StarEx(table_prefix: Some(string.lowercase(t)))
_ -> {
let #(expr_tokens, alias) = split_trailing_alias(tokens)
query_ir.ExprItem(expr: parse_expr(expr_tokens, engine), alias: alias)
}
}
}
fn split_trailing_alias(
tokens: List(lexer.Token),
) -> #(List(lexer.Token), Option(String)) {
case list.reverse(tokens) {
[lexer.Ident(name), lexer.Keyword("as"), ..rest] -> #(
list.reverse(rest),
Some(naming.normalize_identifier(name)),
)
[lexer.QuotedIdent(name), lexer.Keyword("as"), ..rest] -> #(
list.reverse(rest),
Some(naming.normalize_identifier(name)),
)
// `expr alias` (no AS) — only accept this when the prior token
// clearly ends an expression (ident / paren / literal), to avoid
// misreading `SELECT tier` as `SELECT <no-expr> tier`.
[lexer.Ident(name), prev, ..rest] ->
case is_expr_end(prev) {
True -> #(
list.reverse([prev, ..rest]),
Some(naming.normalize_identifier(name)),
)
False -> #(tokens, None)
}
_ -> #(tokens, None)
}
}
fn is_expr_end(token: lexer.Token) -> Bool {
case token {
lexer.RParen -> True
lexer.Ident(_) -> True
lexer.QuotedIdent(_) -> True
lexer.StringLit(_) -> True
lexer.NumberLit(_) -> True
lexer.Placeholder(_) -> True
lexer.Star -> True
_ -> False
}
}
// ============================================================
// FROM / JOIN
// ============================================================
fn parse_from_clause(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(List(query_ir.FromItemEx), List(lexer.Token)) {
let #(items_tokens, rest) =
collect_until_keyword(tokens, [
"where", "group", "having", "order", "limit", "offset", "union",
"intersect", "except", "window", "returning",
])
let groups = split_on_top_commas(items_tokens)
let from_items =
list.map(groups, fn(group) { parse_from_element(group, engine) })
#(from_items, rest)
}
fn parse_from_element(
tokens: List(lexer.Token),
engine: model.Engine,
) -> query_ir.FromItemEx {
let #(first, after_first) = parse_primary_from(tokens, engine)
parse_join_tail(first, after_first, engine)
}
fn parse_primary_from(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.FromItemEx, List(lexer.Token)) {
case tokens {
[lexer.LParen, lexer.Keyword("values"), ..rest] -> {
let #(_, after) = collect_parens([lexer.Keyword("values"), ..rest])
let #(body, _) = collect_parens([lexer.Keyword("values"), ..rest])
parse_values_from_body(body, after, engine)
}
[lexer.LParen, lexer.Keyword("select"), ..] -> {
let #(inner, after_rp) = collect_parens(drop_first(tokens))
let #(core, _) = parse_select_core(inner, engine)
parse_subquery_alias(core, after_rp, False)
}
[lexer.LParen, lexer.Keyword("with"), ..] -> {
let #(inner, after_rp) = collect_parens(drop_first(tokens))
let stmt = parse_stmt(inner, engine)
let core = case stmt {
query_ir.SelectStmt(core: c, ..) -> c
_ -> empty_core()
}
parse_subquery_alias(core, after_rp, False)
}
[lexer.Keyword("lateral"), lexer.LParen, lexer.Keyword("select"), ..] -> {
let #(inner, after_rp) = collect_parens(drop_first(drop_first(tokens)))
let #(core, _) = parse_select_core(inner, engine)
parse_subquery_alias(core, after_rp, True)
}
[lexer.Ident(_), lexer.Dot, lexer.Ident(name), ..rest] -> {
let #(alias, rest2) = parse_optional_alias(rest)
#(query_ir.FromTable(name: string.lowercase(name), alias: alias), rest2)
}
[lexer.Ident(name), ..rest] -> {
let #(alias, rest2) = parse_optional_alias(rest)
#(query_ir.FromTable(name: string.lowercase(name), alias: alias), rest2)
}
[lexer.QuotedIdent(name), ..rest] -> {
let #(alias, rest2) = parse_optional_alias(rest)
#(query_ir.FromTable(name: string.lowercase(name), alias: alias), rest2)
}
_ -> #(query_ir.FromTable(name: "", alias: None), tokens)
}
}
fn parse_values_from_body(
body: List(lexer.Token),
after: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.FromItemEx, List(lexer.Token)) {
let rows = parse_values_rows(body, engine)
let #(alias_opt, col_aliases, rest) = parse_aliased_column_list(after)
let alias = option.unwrap(alias_opt, "")
#(
query_ir.FromValues(rows: rows, alias: alias, column_aliases: col_aliases),
rest,
)
}
fn parse_values_rows(
body: List(lexer.Token),
engine: model.Engine,
) -> List(List(query_ir.Expr)) {
case body {
[lexer.Keyword("values"), ..rest] -> collect_values_rows(rest, [], engine)
_ -> []
}
}
fn collect_values_rows(
tokens: List(lexer.Token),
acc: List(List(query_ir.Expr)),
engine: model.Engine,
) -> List(List(query_ir.Expr)) {
case tokens {
[lexer.LParen, ..rest] -> {
let #(inner, after) = collect_parens(rest)
let row =
list.map(split_on_top_commas(inner), fn(group) {
parse_expr(group, engine)
})
case after {
[lexer.Comma, ..more] -> collect_values_rows(more, [row, ..acc], engine)
_ -> list.reverse([row, ..acc])
}
}
_ -> list.reverse(acc)
}
}
fn parse_subquery_alias(
core: query_ir.SelectCore,
after: List(lexer.Token),
lateral: Bool,
) -> #(query_ir.FromItemEx, List(lexer.Token)) {
let #(alias_opt, col_aliases, rest) = parse_aliased_column_list(after)
let alias = option.unwrap(alias_opt, "")
let item =
query_ir.FromSubquery(core: core, alias: alias, column_aliases: col_aliases)
case lateral {
True -> #(
query_ir.FromJoin(
left: query_ir.FromTable(name: "", alias: None),
right: item,
kind: query_ir.CrossJoin,
on: query_ir.JoinNoCondition,
lateral: True,
),
rest,
)
False -> #(item, rest)
}
}
fn parse_aliased_column_list(
tokens: List(lexer.Token),
) -> #(Option(String), List(String), List(lexer.Token)) {
let after_as = case tokens {
[lexer.Keyword("as"), ..rest] -> rest
_ -> tokens
}
case after_as {
[lexer.Ident(alias), lexer.LParen, ..after_lp] -> {
let #(cols, after) = collect_parens(after_lp)
#(Some(string.lowercase(alias)), parse_ident_list(cols), after)
}
[lexer.QuotedIdent(alias), lexer.LParen, ..after_lp] -> {
let #(cols, after) = collect_parens(after_lp)
#(Some(string.lowercase(alias)), parse_ident_list(cols), after)
}
[lexer.Ident(alias), ..rest] -> #(Some(string.lowercase(alias)), [], rest)
[lexer.QuotedIdent(alias), ..rest] -> #(
Some(string.lowercase(alias)),
[],
rest,
)
_ -> #(None, [], after_as)
}
}
fn parse_optional_alias(
tokens: List(lexer.Token),
) -> #(Option(String), List(lexer.Token)) {
case tokens {
[lexer.Keyword("as"), lexer.Ident(name), ..rest] -> #(
Some(string.lowercase(name)),
rest,
)
[lexer.Keyword("as"), lexer.QuotedIdent(name), ..rest] -> #(
Some(string.lowercase(name)),
rest,
)
[lexer.Ident(name), ..rest] ->
case is_reserved_after_table(name) {
True -> #(None, tokens)
False -> #(Some(string.lowercase(name)), rest)
}
[lexer.QuotedIdent(name), ..rest] -> #(Some(string.lowercase(name)), rest)
_ -> #(None, tokens)
}
}
fn is_reserved_after_table(name: String) -> Bool {
let lowered = string.lowercase(name)
list.contains(
[
"on", "where", "group", "having", "order", "limit", "offset", "union",
"intersect", "except", "returning", "using", "lateral", "left", "right",
"inner", "outer", "cross", "full", "natural", "join", "window",
],
lowered,
)
}
fn parse_join_tail(
left: query_ir.FromItemEx,
tokens: List(lexer.Token),
engine: model.Engine,
) -> query_ir.FromItemEx {
case detect_join(tokens) {
Some(#(kind, after_kw, lateral)) -> {
let #(right_raw, after_right) = parse_primary_from(after_kw, engine)
let #(on, after_on) = parse_join_on(after_right, engine)
let right = parse_join_tail(right_raw, after_on, engine)
parse_join_tail(
query_ir.FromJoin(
left: left,
right: right,
kind: kind,
on: on,
lateral: lateral,
),
[],
engine,
)
}
None -> left
}
}
fn detect_join(
tokens: List(lexer.Token),
) -> Option(#(query_ir.JoinKind, List(lexer.Token), Bool)) {
case tokens {
[lexer.Keyword("join"), lexer.Keyword("lateral"), ..rest] ->
Some(#(query_ir.InnerJoin, rest, True))
[lexer.Keyword("join"), ..rest] -> Some(#(query_ir.InnerJoin, rest, False))
[lexer.Keyword("inner"), lexer.Keyword("join"), ..rest] ->
Some(#(query_ir.InnerJoin, rest, False))
[lexer.Keyword("cross"), lexer.Keyword("join"), ..rest] ->
Some(#(query_ir.CrossJoin, rest, False))
[
lexer.Keyword("left"),
lexer.Keyword("join"),
lexer.Keyword("lateral"),
..rest
] -> Some(#(query_ir.LeftJoin, rest, True))
[
lexer.Keyword("left"),
lexer.Keyword("outer"),
lexer.Keyword("join"),
lexer.Keyword("lateral"),
..rest
] -> Some(#(query_ir.LeftJoin, rest, True))
[
lexer.Keyword("left"),
lexer.Keyword("outer"),
lexer.Keyword("join"),
..rest
] -> Some(#(query_ir.LeftJoin, rest, False))
[lexer.Keyword("left"), lexer.Keyword("join"), ..rest] ->
Some(#(query_ir.LeftJoin, rest, False))
[
lexer.Keyword("right"),
lexer.Keyword("outer"),
lexer.Keyword("join"),
..rest
] -> Some(#(query_ir.RightJoin, rest, False))
[lexer.Keyword("right"), lexer.Keyword("join"), ..rest] ->
Some(#(query_ir.RightJoin, rest, False))
[
lexer.Keyword("full"),
lexer.Keyword("outer"),
lexer.Keyword("join"),
..rest
] -> Some(#(query_ir.FullJoin, rest, False))
[lexer.Keyword("full"), lexer.Keyword("join"), ..rest] ->
Some(#(query_ir.FullJoin, rest, False))
_ -> None
}
}
fn parse_join_on(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.JoinOn, List(lexer.Token)) {
case tokens {
[lexer.Keyword("on"), ..rest] -> {
let #(on_toks, after) =
collect_until_keyword(rest, [
"join", "inner", "left", "right", "full", "cross", "natural", "where",
"group", "having", "order", "limit", "offset", "union", "intersect",
"except", "returning", "window",
])
#(query_ir.JoinOnExpr(expr: parse_expr(on_toks, engine)), after)
}
[lexer.Keyword("using"), lexer.LParen, ..rest] -> {
let #(cols, after) = collect_parens(rest)
#(query_ir.JoinUsing(columns: parse_ident_list(cols)), after)
}
_ -> #(query_ir.JoinNoCondition, tokens)
}
}
// ============================================================
// INSERT / UPDATE / DELETE bodies
// ============================================================
fn parse_insert_body(
ctes: List(query_ir.CteDef),
tokens: List(lexer.Token),
engine: model.Engine,
) -> query_ir.Stmt {
case token_utils.strip_insert_or_action(tokens) {
[lexer.Keyword("into"), ..after_into] ->
parse_insert_target(ctes, after_into, engine)
_ ->
query_ir.UnstructuredStmt(reason: "INSERT without INTO", tokens: [
lexer.Keyword("insert"),
..tokens
])
}
}
fn parse_insert_target(
ctes: List(query_ir.CteDef),
tokens: List(lexer.Token),
engine: model.Engine,
) -> query_ir.Stmt {
let #(table_name, after_name) = read_qualified_name(tokens)
let #(columns, after_cols) = case after_name {
[lexer.LParen, ..after_lp] -> {
let #(inside, after) = collect_parens(after_lp)
#(parse_ident_list(inside), after)
}
_ -> #([], after_name)
}
let #(source, after_source) = parse_insert_source(after_cols, engine)
// MySQL-only: consume an `ON DUPLICATE KEY UPDATE ...` tail if
// present. For PostgreSQL and SQLite we leave the tail untouched —
// any unexpected tokens will simply be ignored by `parse_returning`
// (which scans for RETURNING or yields []).
let #(on_duplicate, after_on_dup) = case engine {
model.MySQL -> parse_on_duplicate_key_update(after_source, engine)
_ -> #([], after_source)
}
let returning = parse_returning(after_on_dup, engine)
query_ir.InsertStmt(
ctes: ctes,
table: table_name,
columns: columns,
source: source,
on_duplicate_key_update: on_duplicate,
returning: returning,
)
}
fn parse_on_duplicate_key_update(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(List(query_ir.Assignment), List(lexer.Token)) {
// `DUPLICATE` is not in the lexer's keyword list, so it arrives as an
// `Ident`. Compare case-insensitively so users can write either case.
case tokens {
[
lexer.Keyword("on"),
lexer.Ident(duplicate),
lexer.Keyword("key"),
lexer.Keyword("update"),
..rest
] ->
case string.lowercase(duplicate) {
"duplicate" -> {
// Collect assignments up to RETURNING or end-of-stream. Other
// tail keywords shouldn't appear here (INSERT has no WHERE /
// ORDER BY in MySQL's upsert form), but we stop at RETURNING
// so the later parse_returning can still see it.
let #(assign_toks, after) = collect_until_keyword(rest, ["returning"])
#(parse_assignments(assign_toks, engine), after)
}
_ -> #([], tokens)
}
_ -> #([], tokens)
}
}
fn parse_insert_source(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.InsertSource, List(lexer.Token)) {
case tokens {
[lexer.Keyword("default"), lexer.Keyword("values"), ..rest] -> #(
query_ir.InsertDefaultValues,
rest,
)
[lexer.Keyword("values"), ..rest] -> {
let rows = collect_values_rows(rest, [], engine)
// Return the tokens that sit after the last VALUES row intact.
// For MySQL, the tail may start with ON DUPLICATE KEY UPDATE,
// which parse_insert_target handles. For PostgreSQL / SQLite,
// anything non-RETURNING is simply ignored downstream.
let after = skip_values_rows(rest)
#(query_ir.InsertValues(rows: rows), after)
}
[lexer.Keyword("select"), ..] -> {
let #(core, after) = parse_select_core(tokens, engine)
#(query_ir.InsertSelect(core: core), after)
}
[lexer.Keyword("with"), ..] -> {
let stmt = parse_stmt(tokens, engine)
case stmt {
query_ir.SelectStmt(core: c, ..) -> #(
query_ir.InsertSelect(core: c),
[],
)
_ -> #(query_ir.InsertValues(rows: []), tokens)
}
}
_ -> #(query_ir.InsertValues(rows: []), tokens)
}
}
/// Walk past VALUES rows without consuming the tokens that follow the
/// final row. Mirrors `collect_values_rows` but returns the leftover
/// tokens instead of the parsed expression lists.
fn skip_values_rows(tokens: List(lexer.Token)) -> List(lexer.Token) {
case tokens {
[lexer.LParen, ..rest] -> {
let #(_inner, after) = collect_parens(rest)
case after {
[lexer.Comma, ..more] -> skip_values_rows(more)
_ -> after
}
}
_ -> tokens
}
}
fn parse_update_body(
ctes: List(query_ir.CteDef),
tokens: List(lexer.Token),
engine: model.Engine,
) -> query_ir.Stmt {
let #(table_name, after_name) = read_qualified_name(tokens)
let #(alias, after_alias) = parse_optional_alias(after_name)
case after_alias {
[lexer.Keyword("set"), ..after_set] -> {
let #(set_toks, after_set_block) =
collect_until_keyword(after_set, ["from", "where", "returning"])
let assignments = parse_assignments(set_toks, engine)
let #(from_list, after_from) = case after_set_block {
[lexer.Keyword("from"), ..rest] -> parse_from_clause(rest, engine)
_ -> #([], after_set_block)
}
let #(where_expr, after_where) = case after_from {
[lexer.Keyword("where"), ..rest] -> {
let #(where_toks, after) = collect_until_keyword(rest, ["returning"])
#(Some(parse_expr(where_toks, engine)), after)
}
_ -> #(None, after_from)
}
let returning = parse_returning(after_where, engine)
query_ir.UpdateStmt(
ctes: ctes,
table: table_name,
alias: alias,
assignments: assignments,
from: from_list,
where_: where_expr,
returning: returning,
)
}
_ ->
query_ir.UnstructuredStmt(reason: "UPDATE without SET", tokens: [
lexer.Keyword("update"),
..tokens
])
}
}
fn parse_assignments(
tokens: List(lexer.Token),
engine: model.Engine,
) -> List(query_ir.Assignment) {
tokens
|> split_on_top_commas()
|> list.filter_map(fn(group) { parse_assignment(group, engine) })
}
fn parse_assignment(
tokens: List(lexer.Token),
engine: model.Engine,
) -> Result(query_ir.Assignment, Nil) {
case tokens {
[lexer.Ident(col), lexer.Operator("="), ..rest] ->
Ok(query_ir.Assignment(
column: string.lowercase(col),
value: parse_expr(rest, engine),
))
[lexer.QuotedIdent(col), lexer.Operator("="), ..rest] ->
Ok(query_ir.Assignment(
column: string.lowercase(col),
value: parse_expr(rest, engine),
))
_ -> Error(Nil)
}
}
fn parse_delete_body(
ctes: List(query_ir.CteDef),
tokens: List(lexer.Token),
engine: model.Engine,
) -> query_ir.Stmt {
case tokens {
[lexer.Keyword("from"), ..after_from] -> {
let #(table_name, after_name) = read_qualified_name(after_from)
let #(alias, after_alias) = parse_optional_alias(after_name)
let #(using_list, after_using) = case after_alias {
[lexer.Keyword("using"), ..rest] -> parse_from_clause(rest, engine)
_ -> #([], after_alias)
}
let #(where_expr, after_where) = case after_using {
[lexer.Keyword("where"), ..rest] -> {
let #(where_toks, after) = collect_until_keyword(rest, ["returning"])
#(Some(parse_expr(where_toks, engine)), after)
}
_ -> #(None, after_using)
}
let returning = parse_returning(after_where, engine)
query_ir.DeleteStmt(
ctes: ctes,
table: table_name,
alias: alias,
using: using_list,
where_: where_expr,
returning: returning,
)
}
_ ->
query_ir.UnstructuredStmt(reason: "DELETE without FROM", tokens: [
lexer.Keyword("delete"),
..tokens
])
}
}
fn parse_returning(
tokens: List(lexer.Token),
engine: model.Engine,
) -> List(query_ir.SelectItemEx) {
// Scan the remaining tokens for a RETURNING keyword at top level.
// This is deliberately permissive so any leading garbage left by
// previous parsing steps (e.g. unconsumed MySQL-only tails on a
// PostgreSQL engine) is tolerated without losing the RETURNING
// projection when it does appear.
case skip_to_returning_or_end(tokens) {
[lexer.Keyword("returning"), ..rest] -> {
let filtered =
list.filter(rest, fn(t) {
case t {
lexer.Semicolon -> False
_ -> True
}
})
parse_select_items(filtered, engine)
}
_ -> []
}
}
// ============================================================
// Expressions
// ============================================================
pub fn parse_expr(
tokens: List(lexer.Token),
engine: model.Engine,
) -> query_ir.Expr {
let #(expr, rest) = parse_or(trim_noise(tokens), engine)
case rest {
[] -> expr
_ ->
query_ir.RawExpr(
reason: "trailing tokens after expression",
tokens: tokens,
)
}
}
fn parse_expr_list(
tokens: List(lexer.Token),
engine: model.Engine,
) -> List(query_ir.Expr) {
tokens
|> split_on_top_commas()
|> list.map(fn(group) { parse_expr(group, engine) })
}
fn parse_or(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
let #(left, rest) = parse_and(tokens, engine)
parse_or_tail(left, rest, engine)
}
fn parse_or_tail(
left: query_ir.Expr,
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
case tokens {
[lexer.Keyword("or"), ..rest] -> {
let #(right, after) = parse_and(rest, engine)
parse_or_tail(
query_ir.Binary(op: "or", left: left, right: right),
after,
engine,
)
}
_ -> #(left, tokens)
}
}
fn parse_and(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
let #(left, rest) = parse_not(tokens, engine)
parse_and_tail(left, rest, engine)
}
fn parse_and_tail(
left: query_ir.Expr,
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
case tokens {
[lexer.Keyword("and"), ..rest] -> {
let #(right, after) = parse_not(rest, engine)
parse_and_tail(
query_ir.Binary(op: "and", left: left, right: right),
after,
engine,
)
}
_ -> #(left, tokens)
}
}
fn parse_not(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
case tokens {
[lexer.Keyword("not"), ..rest] -> {
let #(inner, after) = parse_not(rest, engine)
#(query_ir.Unary(op: "not", arg: inner), after)
}
_ -> parse_comparison(tokens, engine)
}
}
fn parse_comparison(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
let #(left, rest) = parse_additive(tokens, engine)
parse_comparison_tail(left, rest, engine)
}
fn parse_comparison_tail(
left: query_ir.Expr,
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
case tokens {
[lexer.Operator(op), ..rest]
if op == "="
|| op == "<>"
|| op == "!="
|| op == "<"
|| op == ">"
|| op == "<="
|| op == ">="
|| op == "@>"
|| op == "<@"
|| op == "?|"
|| op == "?&"
|| op == "&&"
-> {
case rest {
[lexer.Keyword("any"), lexer.LParen, ..after_lp] -> {
let #(inner, after) = collect_parens(after_lp)
let right = parse_expr(inner, engine)
#(
query_ir.Quantified(
op: op,
left: left,
quantifier: query_ir.QAny,
right: right,
),
after,
)
}
[lexer.Keyword("all"), lexer.LParen, ..after_lp] -> {
let #(inner, after) = collect_parens(after_lp)
let right = parse_expr(inner, engine)
#(
query_ir.Quantified(
op: op,
left: left,
quantifier: query_ir.QAll,
right: right,
),
after,
)
}
_ -> {
let #(right, after) = parse_additive(rest, engine)
#(query_ir.Binary(op: op, left: left, right: right), after)
}
}
}
[lexer.Keyword("is"), lexer.Keyword("not"), lexer.Keyword("null"), ..rest] -> #(
query_ir.IsCheck(expr: left, predicate: query_ir.IsNull, negated: True),
rest,
)
[lexer.Keyword("is"), lexer.Keyword("null"), ..rest] -> #(
query_ir.IsCheck(expr: left, predicate: query_ir.IsNull, negated: False),
rest,
)
[lexer.Keyword("is"), lexer.Keyword("not"), lexer.Keyword("true"), ..rest] -> #(
query_ir.IsCheck(expr: left, predicate: query_ir.IsTrue, negated: True),
rest,
)
[lexer.Keyword("is"), lexer.Keyword("true"), ..rest] -> #(
query_ir.IsCheck(expr: left, predicate: query_ir.IsTrue, negated: False),
rest,
)
[lexer.Keyword("is"), lexer.Keyword("not"), lexer.Keyword("false"), ..rest] -> #(
query_ir.IsCheck(expr: left, predicate: query_ir.IsFalse, negated: True),
rest,
)
[lexer.Keyword("is"), lexer.Keyword("false"), ..rest] -> #(
query_ir.IsCheck(expr: left, predicate: query_ir.IsFalse, negated: False),
rest,
)
[
lexer.Keyword("is"),
lexer.Keyword("not"),
lexer.Keyword("unknown"),
..rest
] -> #(
query_ir.IsCheck(expr: left, predicate: query_ir.IsUnknown, negated: True),
rest,
)
[lexer.Keyword("is"), lexer.Keyword("unknown"), ..rest] -> #(
query_ir.IsCheck(
expr: left,
predicate: query_ir.IsUnknown,
negated: False,
),
rest,
)
[lexer.Keyword("not"), lexer.Keyword("in"), ..rest] ->
parse_in_tail(left, rest, True, engine)
[lexer.Keyword("in"), ..rest] -> parse_in_tail(left, rest, False, engine)
[lexer.Keyword("not"), lexer.Keyword("between"), ..rest] ->
parse_between_tail(left, rest, True, engine)
[lexer.Keyword("between"), ..rest] ->
parse_between_tail(left, rest, False, engine)
[lexer.Keyword("not"), lexer.Keyword("like"), ..rest] ->
parse_like_tail(left, rest, query_ir.Like, True, engine)
[lexer.Keyword("like"), ..rest] ->
parse_like_tail(left, rest, query_ir.Like, False, engine)
[lexer.Keyword("not"), lexer.Keyword("ilike"), ..rest] ->
parse_like_tail(left, rest, query_ir.Ilike, True, engine)
[lexer.Keyword("ilike"), ..rest] ->
parse_like_tail(left, rest, query_ir.Ilike, False, engine)
_ -> #(left, tokens)
}
}
fn parse_in_tail(
left: query_ir.Expr,
tokens: List(lexer.Token),
negated: Bool,
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
case tokens {
[lexer.LParen, lexer.Keyword("select"), ..] -> {
let #(inner, after) = collect_parens(drop_first(tokens))
let #(core, _) = parse_select_core(inner, engine)
#(
query_ir.InExpr(
expr: left,
source: query_ir.InSubquery(core: core),
negated: negated,
),
after,
)
}
[lexer.LParen, ..rest] -> {
let #(inner, after) = collect_parens(rest)
case detect_slice_macro(inner) {
Some(name) -> #(
query_ir.InExpr(
expr: left,
source: query_ir.InSliceMacro(name: name),
negated: negated,
),
after,
)
None -> {
let values = parse_expr_list(inner, engine)
#(
query_ir.InExpr(
expr: left,
source: query_ir.InList(values: values),
negated: negated,
),
after,
)
}
}
}
_ -> #(
query_ir.RawExpr(reason: "IN without paren list", tokens: tokens),
[],
)
}
}
fn detect_slice_macro(tokens: List(lexer.Token)) -> Option(String) {
case tokens {
[
lexer.Ident(s),
lexer.Dot,
lexer.Ident(sl),
lexer.LParen,
lexer.Ident(name),
lexer.RParen,
] ->
case string.lowercase(s), string.lowercase(sl) {
"sqlode", "slice" -> Some(string.lowercase(name))
_, _ -> None
}
_ -> None
}
}
fn parse_between_tail(
left: query_ir.Expr,
tokens: List(lexer.Token),
negated: Bool,
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
let #(low, rest) = parse_additive(tokens, engine)
case rest {
[lexer.Keyword("and"), ..after_and] -> {
let #(high, after) = parse_additive(after_and, engine)
#(
query_ir.Between(expr: left, low: low, high: high, negated: negated),
after,
)
}
_ -> #(query_ir.RawExpr(reason: "BETWEEN without AND", tokens: tokens), [])
}
}
fn parse_like_tail(
left: query_ir.Expr,
tokens: List(lexer.Token),
op: query_ir.LikeOp,
negated: Bool,
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
let #(pattern, rest) = parse_additive(tokens, engine)
let #(escape, rest2) = case rest {
[lexer.Keyword("escape"), ..more] -> {
let #(esc, after) = parse_additive(more, engine)
#(Some(esc), after)
}
_ -> #(None, rest)
}
#(
query_ir.LikeExpr(
expr: left,
op: op,
pattern: pattern,
escape: escape,
negated: negated,
),
rest2,
)
}
fn parse_additive(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
let #(left, rest) = parse_multiplicative(tokens, engine)
parse_additive_tail(left, rest, engine)
}
fn parse_additive_tail(
left: query_ir.Expr,
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
case tokens {
[lexer.Operator(op), ..rest]
if op == "+"
|| op == "-"
|| op == "||"
|| op == "->"
|| op == "->>"
|| op == "#>"
|| op == "#>>"
-> {
let #(right, after) = parse_multiplicative(rest, engine)
parse_additive_tail(
query_ir.Binary(op: op, left: left, right: right),
after,
engine,
)
}
_ -> #(left, tokens)
}
}
fn parse_multiplicative(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
let #(left, rest) = parse_unary(tokens, engine)
parse_multiplicative_tail(left, rest, engine)
}
fn parse_multiplicative_tail(
left: query_ir.Expr,
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
case tokens {
[lexer.Star, ..rest] -> {
let #(right, after) = parse_unary(rest, engine)
parse_multiplicative_tail(
query_ir.Binary(op: "*", left: left, right: right),
after,
engine,
)
}
[lexer.Operator(op), ..rest] if op == "/" || op == "%" -> {
let #(right, after) = parse_unary(rest, engine)
parse_multiplicative_tail(
query_ir.Binary(op: op, left: left, right: right),
after,
engine,
)
}
_ -> #(left, tokens)
}
}
fn parse_unary(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
case tokens {
[lexer.Operator("-"), ..rest] -> {
let #(inner, after) = parse_unary(rest, engine)
#(query_ir.Unary(op: "-", arg: inner), after)
}
[lexer.Operator("+"), ..rest] -> {
let #(inner, after) = parse_unary(rest, engine)
#(query_ir.Unary(op: "+", arg: inner), after)
}
_ -> parse_cast(tokens, engine)
}
}
fn parse_cast(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
let #(atom, rest) = parse_atom(tokens, engine)
parse_cast_tail(atom, rest)
}
// `parse_cast_tail` does not take `engine` because the `::type` suffix
// parses only a type name via `read_type_tokens`, which is a pure
// token-shape helper and not dialect-sensitive.
fn parse_cast_tail(
atom: query_ir.Expr,
tokens: List(lexer.Token),
) -> #(query_ir.Expr, List(lexer.Token)) {
case tokens {
[lexer.Operator("::"), ..rest] -> {
let #(type_name, after) = read_type_tokens(rest)
parse_cast_tail(query_ir.Cast(expr: atom, target_type: type_name), after)
}
_ -> #(atom, tokens)
}
}
fn read_type_tokens(tokens: List(lexer.Token)) -> #(String, List(lexer.Token)) {
collect_type_tokens(tokens, [])
}
fn collect_type_tokens(
tokens: List(lexer.Token),
acc: List(String),
) -> #(String, List(lexer.Token)) {
case tokens {
[lexer.Ident(n), ..rest] ->
collect_type_tokens(rest, [string.lowercase(n), ..acc])
[lexer.Keyword(k), ..rest] -> collect_type_tokens(rest, [k, ..acc])
[lexer.LParen, ..rest] -> {
let #(inside, after) = collect_parens(rest)
let inside_text = render_type_parens(inside)
let last_acc = case acc {
[first, ..rest_acc] -> [first <> "(" <> inside_text <> ")", ..rest_acc]
[] -> ["(" <> inside_text <> ")"]
}
collect_type_tokens(after, last_acc)
}
[lexer.Operator("[]"), ..rest] ->
case acc {
[first, ..rest_acc] ->
collect_type_tokens(rest, [first <> "[]", ..rest_acc])
[] -> collect_type_tokens(rest, ["[]"])
}
_ -> #(list.reverse(acc) |> string.join(" ") |> string.trim, tokens)
}
}
fn render_type_parens(tokens: List(lexer.Token)) -> String {
tokens
|> list.map(fn(t) {
case t {
lexer.Ident(n) -> n
lexer.Keyword(k) -> k
lexer.NumberLit(n) -> n
lexer.Comma -> ","
_ -> ""
}
})
|> string.join("")
}
fn parse_atom(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
case tokens {
[] -> #(query_ir.RawExpr(reason: "empty expression", tokens: []), [])
[lexer.Keyword("null"), ..rest] -> #(query_ir.NullLit, rest)
[lexer.Keyword("true"), ..rest] -> #(query_ir.BoolLit(value: True), rest)
[lexer.Keyword("false"), ..rest] -> #(query_ir.BoolLit(value: False), rest)
[lexer.StringLit(v), ..rest] -> #(query_ir.StringLit(value: v), rest)
[lexer.NumberLit(n), ..rest] -> #(query_ir.NumberLit(value: n), rest)
[lexer.Placeholder(raw), ..rest] -> {
let idx = decode_placeholder(raw)
#(query_ir.Param(index: idx, raw: raw), rest)
}
[lexer.Keyword("case"), ..rest] -> parse_case(rest, engine)
[lexer.Keyword("cast"), lexer.LParen, ..rest] -> {
let #(inside, after) = collect_parens(rest)
let #(expr_tokens, as_rest) = split_on_as(inside)
let target =
as_rest
|> list.map(fn(t) {
case t {
lexer.Ident(n) -> n
lexer.Keyword(k) -> k
_ -> ""
}
})
|> list.filter(fn(s) { s != "" })
|> string.join(" ")
#(
query_ir.Cast(
expr: parse_expr(expr_tokens, engine),
target_type: target,
),
after,
)
}
[lexer.Keyword("not"), lexer.Keyword("exists"), lexer.LParen, ..rest] -> {
let #(inside, after) = collect_parens(rest)
let #(core, _) = parse_select_core(inside, engine)
#(query_ir.Exists(core: core, negated: True), after)
}
[lexer.Keyword("exists"), lexer.LParen, ..rest] -> {
let #(inside, after) = collect_parens(rest)
let #(core, _) = parse_select_core(inside, engine)
#(query_ir.Exists(core: core, negated: False), after)
}
[lexer.Keyword("array"), lexer.LParen, ..rest] -> {
let #(inside, after) = collect_parens(rest)
let #(core, _) = parse_select_core(inside, engine)
#(query_ir.ScalarSubquery(core: core), after)
}
[lexer.Keyword("array"), lexer.Operator("["), ..rest] -> {
let #(inside, after) = collect_brackets(rest)
#(query_ir.ArrayLit(elements: parse_expr_list(inside, engine)), after)
}
[lexer.LParen, lexer.Keyword("select"), ..] -> {
let #(inside, after) = collect_parens(drop_first(tokens))
let #(core, _) = parse_select_core(inside, engine)
#(query_ir.ScalarSubquery(core: core), after)
}
[lexer.LParen, lexer.Keyword("with"), ..] -> {
let #(inside, after) = collect_parens(drop_first(tokens))
let stmt = parse_stmt(inside, engine)
case stmt {
query_ir.SelectStmt(core: c, ..) -> #(
query_ir.ScalarSubquery(core: c),
after,
)
_ -> #(
query_ir.RawExpr(
reason: "non-SELECT subquery in expression",
tokens: tokens,
),
[],
)
}
}
[lexer.LParen, ..rest] -> {
let #(inside, after) = collect_parens(rest)
case split_on_top_commas(inside) {
[single] -> #(parse_expr(single, engine), after)
many -> #(
query_ir.Tuple(
elements: list.map(many, fn(group) { parse_expr(group, engine) }),
),
after,
)
}
}
[lexer.Keyword("interval"), lexer.StringLit(val), ..rest] -> #(
query_ir.Func(
name: "interval",
args: [query_ir.FuncArg(expr: query_ir.StringLit(value: val))],
distinct: False,
filter: None,
over: None,
),
rest,
)
[lexer.Ident(s), lexer.Dot, lexer.Ident(name), lexer.LParen, ..rest] ->
case string.lowercase(s) {
"sqlode" -> {
let #(inside, after) = collect_parens(rest)
#(query_ir.Macro(name: string.lowercase(name), body: inside), after)
}
_ -> parse_function_call(name, [lexer.LParen, ..rest], engine)
}
[lexer.Ident(name), lexer.LParen, ..rest] ->
parse_function_call(name, rest, engine)
[lexer.Ident(table), lexer.Dot, lexer.Star, ..rest] -> #(
query_ir.StarRef(table: Some(string.lowercase(table))),
rest,
)
[lexer.Ident(table), lexer.Dot, lexer.Operator("*"), ..rest] -> #(
query_ir.StarRef(table: Some(string.lowercase(table))),
rest,
)
[lexer.Ident(table), lexer.Dot, lexer.Ident(col), ..rest] -> #(
query_ir.ColumnRef(
table: Some(string.lowercase(table)),
name: string.lowercase(col),
),
rest,
)
[lexer.Ident(table), lexer.Dot, lexer.QuotedIdent(col), ..rest] -> #(
query_ir.ColumnRef(
table: Some(string.lowercase(table)),
name: string.lowercase(col),
),
rest,
)
[lexer.Ident(name), ..rest] -> #(
query_ir.ColumnRef(table: None, name: string.lowercase(name)),
rest,
)
[lexer.QuotedIdent(name), ..rest] -> #(
query_ir.ColumnRef(table: None, name: string.lowercase(name)),
rest,
)
[lexer.Star, ..rest] -> #(query_ir.StarRef(table: None), rest)
[lexer.Operator("*"), ..rest] -> #(query_ir.StarRef(table: None), rest)
_ -> #(query_ir.RawExpr(reason: "unrecognised atom", tokens: tokens), [])
}
}
fn parse_function_call(
name: String,
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
let #(inside, after) = collect_parens(tokens)
let #(distinct, args_tokens) = case inside {
[lexer.Keyword("distinct"), ..rest] -> #(True, rest)
_ -> #(False, inside)
}
let args = case args_tokens {
[] -> []
_ ->
args_tokens
|> split_on_top_commas()
|> list.map(fn(group) {
query_ir.FuncArg(expr: parse_expr(group, engine))
})
}
let #(filter, after_filter) = parse_filter_clause(after, engine)
let #(over, after_over) = parse_over_clause(after_filter, engine)
#(
query_ir.Func(
name: string.lowercase(name),
args: args,
distinct: distinct,
filter: filter,
over: over,
),
after_over,
)
}
fn parse_filter_clause(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(Option(query_ir.Expr), List(lexer.Token)) {
case tokens {
[lexer.Keyword("filter"), lexer.LParen, lexer.Keyword("where"), ..rest] -> {
let #(inside, after) = collect_parens_after_where(rest)
#(Some(parse_expr(inside, engine)), after)
}
_ -> #(None, tokens)
}
}
fn collect_parens_after_where(
tokens: List(lexer.Token),
) -> #(List(lexer.Token), List(lexer.Token)) {
// `rest` is already past `LParen` `where`, so depth is 1 and current
// contents are the WHERE predicate tokens until the matching RParen.
collect_parens(tokens)
}
fn parse_over_clause(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(Option(query_ir.WindowSpec), List(lexer.Token)) {
case tokens {
[lexer.Keyword("over"), lexer.LParen, ..rest] -> {
let #(inside, after) = collect_parens(rest)
#(Some(parse_window_spec(inside, engine)), after)
}
[lexer.Keyword("over"), lexer.Ident(_), ..rest] -> {
// Named window reference — treat as empty spec; the named
// window definition is not followed into right now.
#(
Some(query_ir.WindowSpec(partition_by: [], order_by: [], frame: None)),
rest,
)
}
_ -> #(None, tokens)
}
}
fn parse_window_spec(
tokens: List(lexer.Token),
engine: model.Engine,
) -> query_ir.WindowSpec {
let #(_name_skipped, rest) = case tokens {
[lexer.Ident(_), ..rest] -> #(True, rest)
_ -> #(False, tokens)
}
let #(partition_by, rest2) = case rest {
[lexer.Keyword("partition"), lexer.Keyword("by"), ..more] -> {
let #(p_toks, after) =
collect_until_keyword(more, ["order", "range", "rows", "groups"])
#(parse_expr_list(p_toks, engine), after)
}
_ -> #([], rest)
}
let #(order_by, rest3) = case rest2 {
[lexer.Keyword("order"), lexer.Keyword("by"), ..more] -> {
let #(o_toks, after) =
collect_until_keyword(more, ["range", "rows", "groups"])
#(parse_order_keys(o_toks, engine), after)
}
_ -> #([], rest2)
}
let frame = case rest3 {
[] -> None
other -> Some(other)
}
query_ir.WindowSpec(
partition_by: partition_by,
order_by: order_by,
frame: frame,
)
}
fn parse_case(
tokens: List(lexer.Token),
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
let #(scrutinee, rest) = case tokens {
[lexer.Keyword("when"), ..] -> #(None, tokens)
_ -> {
let #(scr_toks, after) =
collect_until_keyword(tokens, ["when", "else", "end"])
case scr_toks {
[] -> #(None, after)
_ -> #(Some(parse_expr(scr_toks, engine)), after)
}
}
}
collect_case_branches(rest, scrutinee, [], engine)
}
fn collect_case_branches(
tokens: List(lexer.Token),
scrutinee: Option(query_ir.Expr),
branches: List(query_ir.CaseBranch),
engine: model.Engine,
) -> #(query_ir.Expr, List(lexer.Token)) {
case tokens {
[lexer.Keyword("when"), ..rest] -> {
let #(when_toks, after_when) = collect_until_keyword(rest, ["then"])
let after_then = case after_when {
[lexer.Keyword("then"), ..more] -> more
_ -> after_when
}
let #(then_toks, after_then_block) =
collect_until_keyword(after_then, ["when", "else", "end"])
let branch =
query_ir.CaseBranch(
when_: parse_expr(when_toks, engine),
then: parse_expr(then_toks, engine),
)
collect_case_branches(
after_then_block,
scrutinee,
[branch, ..branches],
engine,
)
}
[lexer.Keyword("else"), ..rest] -> {
let #(else_toks, after_else) = collect_until_keyword(rest, ["end"])
let after_end = case after_else {
[lexer.Keyword("end"), ..more] -> more
_ -> after_else
}
#(
query_ir.Case(
scrutinee: scrutinee,
branches: list.reverse(branches),
else_: Some(parse_expr(else_toks, engine)),
),
after_end,
)
}
[lexer.Keyword("end"), ..rest] -> #(
query_ir.Case(
scrutinee: scrutinee,
branches: list.reverse(branches),
else_: None,
),
rest,
)
_ -> #(
query_ir.Case(
scrutinee: scrutinee,
branches: list.reverse(branches),
else_: None,
),
tokens,
)
}
}
// ============================================================
// Token helpers
// ============================================================
fn split_on_top_commas(tokens: List(lexer.Token)) -> List(List(lexer.Token)) {
split_commas_loop(tokens, 0, [], [])
}
fn split_commas_loop(
tokens: List(lexer.Token),
depth: Int,
current: List(lexer.Token),
acc: List(List(lexer.Token)),
) -> List(List(lexer.Token)) {
case tokens {
[] ->
case current {
[] -> list.reverse(acc)
_ -> list.reverse([list.reverse(current), ..acc])
}
[lexer.Comma, ..rest] if depth == 0 ->
case current {
[] -> split_commas_loop(rest, 0, [], acc)
_ -> split_commas_loop(rest, 0, [], [list.reverse(current), ..acc])
}
[lexer.LParen, ..rest] ->
split_commas_loop(rest, depth + 1, [lexer.LParen, ..current], acc)
[lexer.RParen, ..rest] ->
split_commas_loop(rest, depth - 1, [lexer.RParen, ..current], acc)
[t, ..rest] -> split_commas_loop(rest, depth, [t, ..current], acc)
}
}
fn collect_parens(
tokens: List(lexer.Token),
) -> #(List(lexer.Token), List(lexer.Token)) {
collect_paren_loop(tokens, 1, [])
}
fn collect_paren_loop(
tokens: List(lexer.Token),
depth: Int,
acc: List(lexer.Token),
) -> #(List(lexer.Token), List(lexer.Token)) {
case depth <= 0 {
True -> #(list.reverse(acc), tokens)
False ->
case tokens {
[] -> #(list.reverse(acc), [])
[lexer.LParen, ..rest] ->
collect_paren_loop(rest, depth + 1, [lexer.LParen, ..acc])
[lexer.RParen, ..rest] ->
case depth == 1 {
True -> #(list.reverse(acc), rest)
False -> collect_paren_loop(rest, depth - 1, [lexer.RParen, ..acc])
}
[t, ..rest] -> collect_paren_loop(rest, depth, [t, ..acc])
}
}
}
fn collect_brackets(
tokens: List(lexer.Token),
) -> #(List(lexer.Token), List(lexer.Token)) {
collect_bracket_loop(tokens, 1, [])
}
fn collect_bracket_loop(
tokens: List(lexer.Token),
depth: Int,
acc: List(lexer.Token),
) -> #(List(lexer.Token), List(lexer.Token)) {
case depth <= 0 {
True -> #(list.reverse(acc), tokens)
False ->
case tokens {
[] -> #(list.reverse(acc), [])
[lexer.Operator("["), ..rest] ->
collect_bracket_loop(rest, depth + 1, [lexer.Operator("["), ..acc])
[lexer.Operator("]"), ..rest] ->
case depth == 1 {
True -> #(list.reverse(acc), rest)
False ->
collect_bracket_loop(rest, depth - 1, [lexer.Operator("]"), ..acc])
}
[t, ..rest] -> collect_bracket_loop(rest, depth, [t, ..acc])
}
}
}
fn collect_until_keyword(
tokens: List(lexer.Token),
stop: List(String),
) -> #(List(lexer.Token), List(lexer.Token)) {
collect_until_loop(tokens, stop, 0, [])
}
fn collect_until_loop(
tokens: List(lexer.Token),
stop: List(String),
depth: Int,
acc: List(lexer.Token),
) -> #(List(lexer.Token), List(lexer.Token)) {
case tokens {
[] -> #(list.reverse(acc), [])
[lexer.LParen, ..rest] ->
collect_until_loop(rest, stop, depth + 1, [lexer.LParen, ..acc])
[lexer.RParen, ..rest] ->
collect_until_loop(rest, stop, depth - 1, [lexer.RParen, ..acc])
[lexer.Keyword(kw), ..rest] as t if depth == 0 -> {
case list.contains(stop, kw) {
True -> #(list.reverse(acc), t)
False ->
collect_until_loop(rest, stop, depth, [lexer.Keyword(kw), ..acc])
}
}
[t, ..rest] -> collect_until_loop(rest, stop, depth, [t, ..acc])
}
}
fn read_qualified_name(
tokens: List(lexer.Token),
) -> #(String, List(lexer.Token)) {
case tokens {
[lexer.Ident(_), lexer.Dot, lexer.Ident(name), ..rest] -> #(
string.lowercase(name),
rest,
)
[lexer.Ident(name), ..rest] -> #(string.lowercase(name), rest)
[lexer.QuotedIdent(name), ..rest] -> #(string.lowercase(name), rest)
_ -> #("", tokens)
}
}
fn drop_first(tokens: List(lexer.Token)) -> List(lexer.Token) {
case tokens {
[_, ..rest] -> rest
[] -> []
}
}
fn empty_core() -> query_ir.SelectCore {
query_ir.SelectCore(
distinct: False,
select_items: [],
from: [],
where_: None,
group_by: [],
having: None,
order_by: [],
limit: None,
offset: None,
set_op: None,
)
}
fn trim_noise(tokens: List(lexer.Token)) -> List(lexer.Token) {
list.filter(tokens, fn(t) {
case t {
lexer.Semicolon -> False
_ -> True
}
})
}
fn split_on_as(
tokens: List(lexer.Token),
) -> #(List(lexer.Token), List(lexer.Token)) {
split_on_as_loop(tokens, 0, [])
}
fn split_on_as_loop(
tokens: List(lexer.Token),
depth: Int,
acc: List(lexer.Token),
) -> #(List(lexer.Token), List(lexer.Token)) {
case tokens {
[] -> #(list.reverse(acc), [])
[lexer.LParen, ..rest] ->
split_on_as_loop(rest, depth + 1, [lexer.LParen, ..acc])
[lexer.RParen, ..rest] ->
split_on_as_loop(rest, depth - 1, [lexer.RParen, ..acc])
[lexer.Keyword("as"), ..rest] if depth == 0 -> #(list.reverse(acc), rest)
[t, ..rest] -> split_on_as_loop(rest, depth, [t, ..acc])
}
}
fn skip_to_returning_or_end(tokens: List(lexer.Token)) -> List(lexer.Token) {
case tokens {
[] -> []
[lexer.Keyword("returning"), ..] as t -> t
[_, ..rest] -> skip_to_returning_or_end(rest)
}
}
fn decode_placeholder(raw: String) -> Int {
// Accept $N, ?N, :N, @N; when missing, default to 0 so the analyzer
// can surface ParameterTypeNotInferred on it.
let digits = case string.to_graphemes(raw) {
[] -> ""
[_first, ..rest] -> string.concat(rest)
}
case int.parse(digits) {
Ok(n) -> n
Error(_) -> 0
}
}