From bff836d5b9107d78c6dc65404d5296ba7f000a37 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 2 Sep 2026 20:44:20 +0200 Subject: [PATCH] feat(ast): extend Ast with CallExpression and NextValueExpression --- src/main/kotlin/kestrel/sql/ast/Ast.kt | 166 ++++++++++++++++++++++++- 1 file changed, 165 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/kestrel/sql/ast/Ast.kt b/src/main/kotlin/kestrel/sql/ast/Ast.kt index a2a9400..2490118 100644 --- a/src/main/kotlin/kestrel/sql/ast/Ast.kt +++ b/src/main/kotlin/kestrel/sql/ast/Ast.kt @@ -427,7 +427,152 @@ data class ColumnExpression(val name: String, val qualifier: String? = null) : E data class FunctionExpression( val name: String, val argument: Expression?, - val star: Boolean + val star: Boolean, + val separator: Expression? = null, + val orderBy: List = emptyList(), + val distinct: Boolean = false, + val filter: Expression? = null +) : Expression + +data class CallExpression(val name: String, val arguments: List) : Expression + +/** + * `NEXTVAL('seq')`/`CURRVAL('seq')`. Deliberately their own [Expression] variants rather than + * folding into [CallExpression] like every other function call - [sequenceName] is a name resolved + * once at parse time (the parser requires the argument to literally be a string literal, not an + * arbitrary expression), never a per-row value, which keeps every existing [CallExpression]-shaped + * validator (`Executor.validateConstantDefault`, `CheckExpressionColumns`, `GroupAggregation`, ...) + * untouched by NEXTVAL's side effect instead of needing a special case bolted onto each one. See + * [kestrel.engine.Executor.executeInsertInner]/`executeUpdateInner` for where these two are actually + * allowed to evaluate - CHECK constraints, trigger `WHEN` clauses, and SELECT/WHERE all reject them. + */ +data class NextValueExpression(val sequenceName: String) : Expression + +data class CurrentValueExpression(val sequenceName: String) : Expression + +/** + * `MERGE_ACTION()` - takes no arguments (the parser rejects any), so unlike [CallExpression] it + * carries no per-call state and is a `data object` rather than a `data class`. Only meaningful inside + * a [MergeStatement]'s own [MergeStatement.returning] projection, where it evaluates to `'INSERT'`, + * `'UPDATE'`, or `'DELETE'` depending on which action a given RETURNING row came from - see + * [kestrel.engine.Executor.evaluateMergeReturning] for where that per-row action is actually bound. + * Every other [Expression] consumer (CHECK constraints, WHERE, trigger bodies, plain INSERT/UPDATE/ + * DELETE RETURNING, ...) treats it as structurally harmless - no column reference to check, nothing + * to rename, nothing to depend on - exactly like [NextValueExpression]/[CurrentValueExpression] do, + * and instead rejects it the same way they reject NEXTVAL/CURRVAL: by never handing the + * [kestrel.engine.Evaluator] that runs it the per-row action string it needs, so evaluation itself + * throws. + */ +data object MergeActionExpression : Expression + +/** + * `$1`, `$2`, ... - a positional parameter placeholder, 1-based to match the SQL text. Only ever + * produced by the parser inside a [PrepareStatement]'s [PrepareStatement.target] (or, harmlessly, + * anywhere else an [Expression] is grammatically legal, since the parser has no context at parse + * time to reject it elsewhere) - [kestrel.engine.ParameterSubstitution] replaces every occurrence + * with a [LiteralExpression] before the surrounding statement actually runs, so by evaluation time + * ([kestrel.engine.Evaluator]) one reaching [kestrel.engine.Evaluator.evaluate] unsubstituted is + * always a usage error (typed outside of PREPARE/EXECUTE entirely) and throws there, exactly like + * [MergeActionExpression] throws when evaluated outside a MERGE's own RETURNING. + */ +data class ParameterExpression(val index: Int) : Expression + +data class IsNullExpression(val operand: Expression, val negated: Boolean) : Expression + +data class InExpression(val operand: Expression, val values: List, val negated: Boolean) : Expression + +data class BetweenExpression( + val operand: Expression, + val low: Expression, + val high: Expression, + val negated: Boolean +) : Expression + +data class LikeExpression(val operand: Expression, val pattern: Expression, val negated: Boolean) : Expression + +data class CaseExpression( + val operand: Expression?, + val branches: List, + val elseExpression: Expression? +) : Expression + +data class CaseBranch(val condition: Expression, val result: Expression) + +data class ScalarSubqueryExpression(val select: SelectStatement) : Expression + +data class ExistsExpression(val select: SelectStatement) : Expression + +data class InSubqueryExpression( + val operand: Expression, + val select: SelectStatement, + val negated: Boolean +) : Expression + +data class CastExpression( + val operand: Expression, + val targetType: ColumnType, + val precision: Int? = null, + val scale: Int? = null +) : Expression + +enum class WindowFunctionKind { RowNumber, Rank, DenseRank, Aggregate, Lag, Lead, FirstValue, LastValue, Ntile } + +/** Where a [FrameBound] sits relative to the current row; ordered exactly as SQL requires a frame's start to precede its end. */ +enum class FrameBoundType { UnboundedPreceding, Preceding, CurrentRow, Following, UnboundedFollowing } + +/** `UNBOUNDED PRECEDING`/`CURRENT ROW`/`UNBOUNDED FOLLOWING` carry no [offset]; `N PRECEDING`/`N FOLLOWING` evaluate it per row. */ +data class FrameBound(val type: FrameBoundType, val offset: Expression? = null) + +/** + * [Rows] counts physical row positions (`ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING` = the row before + * and after, regardless of ties). [Range] measures distance in the value of a single ORDER BY column + * (numeric or DATE) - `RANGE BETWEEN 5 PRECEDING AND 5 FOLLOWING` on `ORDER BY amount` includes every + * row whose `amount` is within 5 of the current row's, ties included, however many physical rows that + * is. [Groups] counts whole peer groups (ties on the OVER's ORDER BY) instead of rows or values - + * `GROUPS BETWEEN 1 PRECEDING AND 1 FOLLOWING` includes every row in the peer group before the + * current one, the current peer group, and the one after, however many rows each group has. Unlike + * [Range] it works with any ORDER BY (zero, one, or many columns, any type), not just a single + * numeric/DATE column. + */ +enum class FrameUnit { Rows, Range, Groups } + +/** + * `EXCLUDE` narrows a frame after its [FrameBound]s are resolved, by removing rows tied to the + * current row on the OVER's ORDER BY. [NoOthers] (the default - no `EXCLUDE` clause at all) removes + * nothing. [CurrentRow] removes only the current row itself. [Group] removes the current row's + * entire peer group, including the current row. [Ties] removes every *other* row in the peer group + * but keeps the current row - the one exclusion mode where the current row's own value always still + * counts. With no ORDER BY the whole partition is one peer group, matching the same convention + * already used for the frame's own `CURRENT ROW` bound and for the implicit default frame. + */ +enum class FrameExclude { NoOthers, CurrentRow, Group, Ties } + +/** `ROWS|RANGE BETWEEN AND [EXCLUDE ...]`. */ +data class WindowFrame(val unit: FrameUnit, val start: FrameBound, val end: FrameBound, val exclude: FrameExclude = FrameExclude.NoOthers) + +/** + * `name(...) OVER (PARTITION BY ... ORDER BY ... [ROWS BETWEEN ... AND ...])`. [RowNumber]/[Rank]/ + * [DenseRank] never take an argument (always `argument == null && star == false`); [Aggregate] + * reuses the same COUNT/SUM/AVG/MIN/MAX vocabulary as [FunctionExpression] (including COUNT(*) via + * [star]) but computes per row over a partition/frame instead of collapsing the whole group into + * one row. [Lag]/[Lead] read [argument] from a row [offset] rows before/after the current one + * (default 1), falling back to [defaultValue] (or NULL) past the partition's edge - both are `null` + * for every other kind. [FirstValue]/[LastValue] read [argument] from the first/last row of the + * current frame; [Ntile] treats [argument] as the bucket count and ignores [offset]/[defaultValue]. + * [frame] is only meaningful for [Aggregate]/[FirstValue]/[LastValue] - `null` means the default SQL + * frame (`RANGE UNBOUNDED PRECEDING AND CURRENT ROW` with an ORDER BY, the whole partition without + * one); every other kind rejects an explicit [frame] outright since it would not change their result. + */ +data class WindowExpression( + val name: String, + val kind: WindowFunctionKind, + val argument: Expression?, + val star: Boolean, + val partitionBy: List, + val orderBy: List, + val offset: Expression? = null, + val defaultValue: Expression? = null, + val frame: WindowFrame? = null ) : Expression data class BinaryExpression( @@ -438,6 +583,25 @@ data class BinaryExpression( data class UnaryExpression(val operator: UnaryOperator, val operand: Expression) : Expression +/** + * `expr COLLATE name` (SQL:1999) - overrides the text-comparison sequence used for [operand] + * wherever it participates in a comparison (`=`/`<>`/`<`/`<=`/`>`/`>=`, `IN`, `BETWEEN`, a + * searched-`CASE`'s operand match) - see [kestrel.engine.Evaluator]'s own comparison helpers, the + * only place a [Collation] other than [Collation.Binary] changes behavior this way. Evaluating + * [operand] itself is unaffected ([kestrel.engine.Evaluator.evaluate] just recurses into it) - + * COLLATE only ever changes how two already-evaluated values are ordered against each other, never + * what value an expression itself produces. Always wins over a column's own declared COLLATE (a + * `col TYPE COLLATE name` on its [kestrel.catalog.Column], resolved automatically for a bare + * [ColumnExpression] with no [CollateExpression] of its own via + * [kestrel.engine.ColumnResolver.declaredCollation]) - explicit beats declared, exactly like + * SQLite. `ORDER BY`/window `OVER (... ORDER BY ...)` (see [kestrel.engine.compareOrderKeyValues]) + * only ever honor an explicit [CollateExpression] on the sort key, never a bare column's declared + * COLLATE - a documented, narrower scope cut than before: sorting a `COLLATE NOCASE` column still + * needs `ORDER BY col COLLATE NOCASE` spelled out, only comparisons pick the declared one up for + * free. + */ +data class CollateExpression(val operand: Expression, val collation: Collation) : Expression + enum class BinaryOperator(val symbol: String) { Add("+"), Subtract("-"), Multiply("*"), Divide("/"), Equal("="), NotEqual("<>"), Less("<"), LessEqual("<="),