feat(ast): extend Ast with CallExpression and NextValueExpression

This commit is contained in:
dualfroz committed 2026-09-02 20:44:20 +02:00
1 parent e816d4edf1
commit bff836d5b9
1 file changed
+165 -1
+165 -1
View File
@@ -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<OrderKey> = emptyList(),
val distinct: Boolean = false,
val filter: Expression? = null
) : Expression
data class CallExpression(val name: String, val arguments: List<Expression>) : 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<Expression>, 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<CaseBranch>,
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 <start> AND <end> [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<Expression>,
val orderBy: List<OrderKey>,
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("<="),