feat(ast): extend Ast with MergeStatement and MergeWhenClause
This commit is contained in:
1 parent
3eb5cd7fca
commit
e816d4edf1
1 file changed
+172
-8
@@ -222,10 +222,138 @@ data class DeleteStatement(
|
|||||||
data class UpdateStatement(
|
data class UpdateStatement(
|
||||||
val tableName: String,
|
val tableName: String,
|
||||||
val assignments: List<Assignment>,
|
val assignments: List<Assignment>,
|
||||||
val where: Expression?
|
val where: Expression?,
|
||||||
|
val returning: Projection? = null
|
||||||
|
) : PreparableStatement
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `MERGE INTO target [AS targetAlias] USING source ON onCondition whenClauses...`. [source] is the
|
||||||
|
* same [FromItem] a `FROM`/`USING`-shaped clause anywhere else in this grammar produces (a bare
|
||||||
|
* table name or a derived-table subquery, carrying its own alias), reused rather than inventing a
|
||||||
|
* MERGE-specific source type. [whenClauses] preserves the exact order they were written in - see
|
||||||
|
* [kestrel.engine.Executor]'s executeMerge for why that order (not a MATCHED-then-NOT-MATCHED split)
|
||||||
|
* is what standard MERGE semantics actually require: for a given source row, at most one category
|
||||||
|
* (MATCHED or NOT MATCHED) can ever apply, so evaluating the whole list in written order and
|
||||||
|
* skipping clauses of the other category naturally reproduces "first matching WHEN clause wins".
|
||||||
|
* Use from inside a trigger body is deliberately unsupported (the parser's trigger-body grammar only
|
||||||
|
* ever accepts INSERT/UPDATE/DELETE, a structural scope cut, not a runtime check). [returning], when
|
||||||
|
* present, projects one result row per action MERGE actually took (insert/update/delete), in the
|
||||||
|
* order those actions ran - see [kestrel.engine.Executor.evaluateMergeReturning].
|
||||||
|
*/
|
||||||
|
data class MergeStatement(
|
||||||
|
val targetTable: String,
|
||||||
|
val targetAlias: String?,
|
||||||
|
val source: FromItem,
|
||||||
|
val onCondition: Expression,
|
||||||
|
val whenClauses: List<MergeWhenClause>,
|
||||||
|
val returning: Projection? = null
|
||||||
|
) : PreparableStatement
|
||||||
|
|
||||||
|
sealed interface MergeWhenClause {
|
||||||
|
val condition: Expression?
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WHEN MATCHED [AND condition] THEN UPDATE SET assignments... - `assignments` targets are always unqualified target columns, matching plain UPDATE. */
|
||||||
|
data class MergeMatchedUpdate(override val condition: Expression?, val assignments: List<Assignment>) : MergeWhenClause
|
||||||
|
|
||||||
|
/** WHEN MATCHED [AND condition] THEN DELETE */
|
||||||
|
data class MergeMatchedDelete(override val condition: Expression?) : MergeWhenClause
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WHEN NOT MATCHED [AND condition] THEN INSERT [(columns)] VALUES (values). `columns == null` means
|
||||||
|
* every target column in schema order, exactly like a column-list-less plain INSERT. Unlike an
|
||||||
|
* ordinary INSERT, [values] is always exactly one row (MERGE inserts at most one row per
|
||||||
|
* unmatched source row) and can only reference the source side - there is no target row yet.
|
||||||
|
*/
|
||||||
|
data class MergeNotMatchedInsert(override val condition: Expression?, val columns: List<String>?, val values: List<Expression?>) : MergeWhenClause
|
||||||
|
|
||||||
|
data class ExplainStatement(val select: QueryStatement) : Statement
|
||||||
|
|
||||||
|
enum class TriggerTiming { Before, After }
|
||||||
|
|
||||||
|
enum class TriggerEvent { Insert, Update, Delete }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `body` is restricted by the parser to INSERT/UPDATE/DELETE statements (matching SQLite's own
|
||||||
|
* trigger-body grammar) - those are the only statement kinds with a side effect worth firing, and
|
||||||
|
* the only ones [kestrel.engine.Executor] knows how to run with a [kestrel.engine.TriggerRowContext]
|
||||||
|
* bound. `whenClause`, when present, is evaluated once per candidate row before [body] runs at all;
|
||||||
|
* a row for which it is false or NULL never fires this trigger.
|
||||||
|
*/
|
||||||
|
data class CreateTriggerStatement(
|
||||||
|
val triggerName: String,
|
||||||
|
val timing: TriggerTiming,
|
||||||
|
val event: TriggerEvent,
|
||||||
|
val tableName: String,
|
||||||
|
val whenClause: Expression?,
|
||||||
|
val body: List<Statement>
|
||||||
) : Statement
|
) : Statement
|
||||||
|
|
||||||
data class ExplainStatement(val select: SelectStatement) : Statement
|
data class DropTriggerStatement(val triggerName: String) : Statement
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `incrementBy`/`minValue`/`maxValue`/`startValue` are already fully resolved by the parser's
|
||||||
|
* defaulting rules (see `Parser.parseSequenceOptions`) - `null` only ever means "not written by the
|
||||||
|
* user", never "no bound"; [kestrel.engine.Executor.executeCreateSequence] is what turns a `null`
|
||||||
|
* into Postgres' own ascending/descending default (MINVALUE 1 / MAXVALUE Long.MAX_VALUE and the
|
||||||
|
* reverse for a negative [incrementBy]) and validates the combination.
|
||||||
|
*/
|
||||||
|
data class CreateSequenceStatement(
|
||||||
|
val sequenceName: String,
|
||||||
|
val incrementBy: Long? = null,
|
||||||
|
val minValue: Long? = null,
|
||||||
|
val maxValue: Long? = null,
|
||||||
|
val startValue: Long? = null,
|
||||||
|
val cycle: Boolean = false
|
||||||
|
) : Statement
|
||||||
|
|
||||||
|
data class DropSequenceStatement(val sequenceName: String) : Statement
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every field besides [sequenceName] is "not mentioned in this ALTER" when `null`/`false` -
|
||||||
|
* [clearMinValue]/[clearMaxValue] (Postgres' `NO MINVALUE`/`NO MAXVALUE`) are tracked separately
|
||||||
|
* from [minValue]/[maxValue] because, unlike CREATE SEQUENCE, "not mentioned" and "explicitly
|
||||||
|
* cleared back to the type default" are different instructions on an ALTER: the former leaves
|
||||||
|
* today's bound untouched, the latter recomputes it from [incrementBy]'s sign exactly like a fresh
|
||||||
|
* CREATE SEQUENCE with no bound written at all would. [restart] with a `null` [restartWith] is a
|
||||||
|
* bare `RESTART` (reset to the sequence's original `START WITH`, unaffected by any earlier ALTER);
|
||||||
|
* [restart] `false` means no RESTART clause at all, leaving the counter wherever NEXTVAL last left it.
|
||||||
|
*/
|
||||||
|
data class AlterSequenceStatement(
|
||||||
|
val sequenceName: String,
|
||||||
|
val incrementBy: Long? = null,
|
||||||
|
val minValue: Long? = null,
|
||||||
|
val clearMinValue: Boolean = false,
|
||||||
|
val maxValue: Long? = null,
|
||||||
|
val clearMaxValue: Boolean = false,
|
||||||
|
val restart: Boolean = false,
|
||||||
|
val restartWith: Long? = null,
|
||||||
|
val cycle: Boolean? = null
|
||||||
|
) : Statement
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `PREPARE name AS <target>` - parses and stores [target] under [name] without running it.
|
||||||
|
* [kestrel.engine.Executor] keeps prepared statements in a session-scoped registry OUTSIDE
|
||||||
|
* [kestrel.catalog.Catalog] (unlike CREATE VIEW/TRIGGER/SEQUENCE) - a deliberate choice, not an
|
||||||
|
* oversight: they carry no data of their own (only an AST plus, at EXECUTE time, caller-supplied
|
||||||
|
* argument values), so BEGIN/ROLLBACK's catalog snapshot/restore has nothing meaningful to undo for
|
||||||
|
* them, and a name PREPARE-d before a transaction started should not vanish just because that
|
||||||
|
* transaction later rolled back. See [kestrel.engine.ParameterSubstitution] for how `$1`, `$2`, ...
|
||||||
|
* inside [target] are resolved once an EXECUTE actually supplies values.
|
||||||
|
*/
|
||||||
|
data class PrepareStatement(val name: String, val target: PreparableStatement) : Statement
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `EXECUTE name(arg, arg, ...)` (or `EXECUTE name` for a statement with no parameters) -
|
||||||
|
* [kestrel.engine.Executor.resolveExecute] looks [name] up, evaluates each [arguments] expression
|
||||||
|
* as a constant (no column/row context exists to resolve against), and substitutes the results for
|
||||||
|
* every [ParameterExpression] in the prepared statement before it actually runs - so whatever this
|
||||||
|
* resolves to is always one of [PreparableStatement]'s own concrete kinds, never `EXECUTE` itself.
|
||||||
|
*/
|
||||||
|
data class ExecuteStatement(val name: String, val arguments: List<Expression> = emptyList()) : Statement
|
||||||
|
|
||||||
|
/** `DEALLOCATE name` removes one prepared statement; `DEALLOCATE ALL` ([all] `true`, [name] `null`) removes every one. */
|
||||||
|
data class DeallocateStatement(val name: String?, val all: Boolean = false) : Statement
|
||||||
|
|
||||||
data object BeginStatement : Statement
|
data object BeginStatement : Statement
|
||||||
|
|
||||||
@@ -233,15 +361,22 @@ data object CommitStatement : Statement
|
|||||||
|
|
||||||
data object RollbackStatement : Statement
|
data object RollbackStatement : Statement
|
||||||
|
|
||||||
data class Assignment(val columnName: String, val value: Expression)
|
data class SavepointStatement(val name: String) : Statement
|
||||||
|
|
||||||
enum class JoinType { Inner, Left }
|
data class ReleaseSavepointStatement(val name: String) : Statement
|
||||||
|
|
||||||
|
data class RollbackToSavepointStatement(val name: String) : Statement
|
||||||
|
|
||||||
|
/** `value == null` means the assignment was written as `col = DEFAULT` - the column's declared DEFAULT (or NULL without one), evaluated fresh per row rather than carrying a literal at parse time. */
|
||||||
|
data class Assignment(val columnName: String, val value: Expression?)
|
||||||
|
|
||||||
|
enum class JoinType { Inner, Left, Right, Full, Cross }
|
||||||
|
|
||||||
|
/** [on] is null exactly for [JoinType.Cross] - a CROSS JOIN has no condition, matching every pairing. */
|
||||||
data class JoinClause(
|
data class JoinClause(
|
||||||
val type: JoinType,
|
val type: JoinType,
|
||||||
val tableName: String,
|
val from: FromItem,
|
||||||
val alias: String?,
|
val on: Expression?
|
||||||
val on: Expression
|
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed interface Projection
|
sealed interface Projection
|
||||||
@@ -252,7 +387,16 @@ data class ExpressionProjection(val items: List<SelectItem>) : Projection
|
|||||||
|
|
||||||
data class SelectItem(val expression: Expression, val alias: String?)
|
data class SelectItem(val expression: Expression, val alias: String?)
|
||||||
|
|
||||||
data class OrderBy(val expression: Expression, val descending: Boolean)
|
/**
|
||||||
|
* [nullsFirst] is `null` when the SQL text had no explicit `NULLS FIRST`/`NULLS LAST` - every
|
||||||
|
* comparator consuming this (see [kestrel.engine.compareOrderKeyValues]) then falls back to
|
||||||
|
* `!descending`, preserving this engine's original convention (NULL as the smallest value
|
||||||
|
* regardless of direction) from before this syntax existed. `true`/`false` here always means an
|
||||||
|
* explicit clause was present, independent of [descending].
|
||||||
|
*/
|
||||||
|
data class OrderKey(val expression: Expression, val descending: Boolean, val nullsFirst: Boolean? = null)
|
||||||
|
|
||||||
|
data class OrderBy(val keys: List<OrderKey>)
|
||||||
|
|
||||||
sealed interface Expression
|
sealed interface Expression
|
||||||
|
|
||||||
@@ -260,6 +404,26 @@ data class LiteralExpression(val value: Value) : Expression
|
|||||||
|
|
||||||
data class ColumnExpression(val name: String, val qualifier: String? = null) : Expression
|
data class ColumnExpression(val name: String, val qualifier: String? = null) : Expression
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [separator] and [orderBy] exist only for `STRING_AGG(value, separator [ORDER BY ...])` - every
|
||||||
|
* other aggregate (COUNT/SUM/AVG/MIN/MAX) leaves both null/empty. Kept on the shared node rather than
|
||||||
|
* a dedicated `StringAggExpression` variant so STRING_AGG rides every existing [FunctionExpression]
|
||||||
|
* validator (GROUP BY membership, CHECK/DEFAULT aggregate bans, subquery bans, ...) for free instead
|
||||||
|
* of needing a twelfth near-parallel `when` branch across the codebase.
|
||||||
|
*
|
||||||
|
* [distinct] is `COUNT(DISTINCT x)`/`SUM(DISTINCT x)`/`AVG(DISTINCT x)`/`MIN(DISTINCT x)`/
|
||||||
|
* `MAX(DISTINCT x)` - never true together with [star] or alongside [separator] (the parser rejects
|
||||||
|
* `DISTINCT` on STRING_AGG's own call shape and on `*`), and never true on a windowed aggregate (the
|
||||||
|
* parser rejects `DISTINCT ... OVER (...)` outright, matching Postgres - there is no per-row windowed
|
||||||
|
* notion of "distinct so far").
|
||||||
|
*
|
||||||
|
* [filter] is `aggregate_name(args) FILTER (WHERE condition)` - a per-row predicate evaluated against
|
||||||
|
* the same group the aggregate would otherwise see in full, restricting which rows reach the aggregate
|
||||||
|
* without needing a separate `CASE WHEN ... END` inside every argument. Never set on STRING_AGG (its
|
||||||
|
* own call shape returns before this field would ever be populated) or on a windowed aggregate (the
|
||||||
|
* parser rejects `FILTER` together with `OVER`, matching how [distinct] is rejected there too - there
|
||||||
|
* is no per-row windowed notion of a group to filter).
|
||||||
|
*/
|
||||||
data class FunctionExpression(
|
data class FunctionExpression(
|
||||||
val name: String,
|
val name: String,
|
||||||
val argument: Expression?,
|
val argument: Expression?,
|
||||||
|
|||||||
Reference in new issue
Block a user