diff --git a/src/main/kotlin/kestrel/sql/ast/Ast.kt b/src/main/kotlin/kestrel/sql/ast/Ast.kt index e4633d2..a2a9400 100644 --- a/src/main/kotlin/kestrel/sql/ast/Ast.kt +++ b/src/main/kotlin/kestrel/sql/ast/Ast.kt @@ -222,10 +222,138 @@ data class DeleteStatement( data class UpdateStatement( val tableName: String, val assignments: List, - 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, + 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) : 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?, val values: List) : 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 -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 ` - 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 = 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 @@ -233,15 +361,22 @@ data object CommitStatement : 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( val type: JoinType, - val tableName: String, - val alias: String?, - val on: Expression + val from: FromItem, + val on: Expression? ) sealed interface Projection @@ -252,7 +387,16 @@ data class ExpressionProjection(val items: List) : Projection 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) 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 +/** + * [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( val name: String, val argument: Expression?,