feat(ast): extend Ast with CreateTableStatement and DropMode

This commit is contained in:
dualfroz committed 2026-09-02 20:29:21 +02:00
1 parent 260d649276
commit 3eb5cd7fca
1 file changed
+192 -10
+192 -10
View File
@@ -1,41 +1,223 @@
package kestrel.sql.ast package kestrel.sql.ast
import kestrel.catalog.CheckConstraint
import kestrel.catalog.Column import kestrel.catalog.Column
import kestrel.catalog.ForeignKeyConstraint
import kestrel.catalog.UniqueConstraint
import kestrel.storage.Collation
import kestrel.storage.ColumnType
import kestrel.storage.Value import kestrel.storage.Value
sealed interface Statement sealed interface Statement
data class CreateTableStatement(val tableName: String, val columns: List<Column>) : Statement data class CreateTableStatement(
val tableName: String,
val columns: List<Column>,
val foreignKeys: List<ForeignKeyConstraint> = emptyList(),
val checks: List<CheckConstraint> = emptyList(),
val uniqueConstraints: List<UniqueConstraint> = emptyList(),
val compositePrimaryKey: List<String> = emptyList(),
/** `CREATE TEMP TABLE`/`CREATE TEMPORARY TABLE` - see [kestrel.catalog.TableSchema.temporary]. */
val temporary: Boolean = false
) : Statement
data class DropTableStatement(val tableName: String) : Statement enum class DropMode { Restrict, Cascade }
data class DropTableStatement(val tableName: String, val mode: DropMode = DropMode.Restrict) : Statement
/**
* `TRUNCATE TABLE t1 [, t2, ...] [RESTART IDENTITY | CONTINUE IDENTITY] [CASCADE | RESTRICT]`.
* Unlike a WHERE-less DELETE, TRUNCATE never fires row-level triggers and never runs a FOREIGN KEY's
* own ON DELETE action row by row - see [kestrel.catalog.Catalog.truncateTables]/
* [kestrel.store.sql.DiskCatalog.truncateTables] for why [mode] means something structurally
* different here than it does for [DropTableStatement]/[DropViewStatement].
*/
data class TruncateTableStatement(
val tableNames: List<String>,
val restartIdentity: Boolean = false,
val mode: DropMode = DropMode.Restrict
) : Statement
data class CreateIndexStatement( data class CreateIndexStatement(
val indexName: String, val indexName: String,
val tableName: String, val tableName: String,
val columnName: String val columnNames: List<String>,
val unique: Boolean = false
) : Statement ) : Statement
data class DropIndexStatement(val indexName: String) : Statement data class DropIndexStatement(val indexName: String) : Statement
sealed interface AlterTableAction
data class AddColumn(val column: Column) : AlterTableAction
data class DropColumn(val columnName: String, val mode: DropMode = DropMode.Restrict) : AlterTableAction
data class RenameColumn(val oldName: String, val newName: String) : AlterTableAction
data class RenameTable(val newName: String) : AlterTableAction
/**
* `ALTER TABLE t ALTER [COLUMN] col TYPE newtype [USING expression]` (Postgres syntax). [using],
* when given, is evaluated per existing row against that row's OLD values (exactly like an UPDATE
* SET assignment) and the result coerced into [newType]/[precision]/[scale]; when omitted it
* defaults to a bare reference to [columnName] itself - "recompute this column as its own old
* value, coerced to the new type" - so a widening change (INTEGER -> DECIMAL, say) needs no
* explicit `USING` at all, matching Postgres' own default behavior.
*/
data class ChangeColumnType(
val columnName: String,
val newType: ColumnType,
val precision: Int? = null,
val scale: Int? = null,
val using: Expression? = null
) : AlterTableAction
/**
* `ALTER TABLE t ADD CONSTRAINT name CHECK (...) | UNIQUE (...) | FOREIGN KEY (...) REFERENCES ...` -
* the constraint's own name lives inside [AddedCheck.check]/[AddedUnique.unique]/[AddedForeignKey.foreignKey]
* (each already carries a `name: String?`, shared with the identical table-level `CONSTRAINT name` clause
* a `CREATE TABLE` can declare inline), not as a separate field here.
*/
sealed interface AddedConstraint
data class AddedCheck(val check: CheckConstraint) : AddedConstraint
data class AddedUnique(val unique: UniqueConstraint) : AddedConstraint
data class AddedForeignKey(val foreignKey: ForeignKeyConstraint) : AddedConstraint
data class AddConstraint(val constraint: AddedConstraint) : AlterTableAction
/** `ALTER TABLE t DROP CONSTRAINT name [RESTRICT | CASCADE]` - see [kestrel.catalog.Catalog.dropConstraint]. */
data class DropConstraint(val name: String, val mode: DropMode = DropMode.Restrict) : AlterTableAction
data class AlterTableStatement(val tableName: String, val action: AlterTableAction) : Statement
data class CreateViewStatement(
val viewName: String,
val query: QueryStatement,
val orReplace: Boolean
) : Statement
data class DropViewStatement(val viewName: String, val mode: DropMode = DropMode.Restrict) : Statement
data class InsertStatement( data class InsertStatement(
val tableName: String, val tableName: String,
val columns: List<String>?, val columns: List<String>?,
val rows: List<List<Expression>> val rows: List<List<Expression?>>,
) : Statement val onConflict: OnConflictClause? = null,
val returning: Projection? = null
) : PreparableStatement
/** Empty [targetColumns] means "any unique or primary key violation" (bare `ON CONFLICT DO ...`), matching Postgres. */
data class OnConflictClause(val targetColumns: List<String>, val action: ConflictAction)
sealed interface ConflictAction
data object ConflictDoNothing : ConflictAction
data class ConflictDoUpdate(val assignments: List<Assignment>, val where: Expression?) : ConflictAction
/**
* The statement kinds [PrepareStatement] can wrap: a SELECT/set-operation/CTE ([QueryStatement])
* or a data-mutating [InsertStatement]/[UpdateStatement]/[DeleteStatement]/[MergeStatement] - the
* same "any DML or query, never DDL/transaction control" boundary Postgres' own PREPARE draws.
* Declared as its own sealed interface (rather than checking `is QueryStatement || is InsertStatement
* || ...` by hand at every call site) so the compiler enforces [ParameterSubstitution]'s `when` stays
* exhaustive as new statement kinds are added.
*/
sealed interface PreparableStatement : Statement
sealed interface QueryStatement : Statement, PreparableStatement
data class SelectStatement( data class SelectStatement(
val projection: Projection, val projection: Projection,
val tableName: String, val from: FromItem,
val tableAlias: String? = null,
val joins: List<JoinClause> = emptyList(), val joins: List<JoinClause> = emptyList(),
val where: Expression? = null, val where: Expression? = null,
/** For [RollupSpec]/[CubeSpec]/[GroupingSetsSpec], this is the deduplicated union of every expression across every one of [groupingSpec]'s grouping sets (order = first appearance) - NOT the flat comma list SQL text would suggest. Every existing consumer that only needs "which expressions are legal grouping columns" (validation, dependency analysis, rename propagation, the plain-`GROUP BY a, b` write-back path) reads this field and needs no awareness of [groupingSpec] at all; only [kestrel.engine.GroupAggregation], which needs the actual per-set breakdown, reads [groupingSpec]. */
val groupBy: List<Expression> = emptyList(), val groupBy: List<Expression> = emptyList(),
val groupingSpec: GroupingSpec? = null,
val having: Expression? = null, val having: Expression? = null,
val orderBy: OrderBy? = null, val orderBy: OrderBy? = null,
val limit: Int? = null val limit: Int? = null,
) : Statement /** `LIMIT`/`OFFSET` may be written in either order (matching Postgres) - applied together, after ORDER BY/DISTINCT dedup and before projection, skipping this many rows before [limit] (if any) caps what remains. */
val offset: Int? = null,
/**
* `true` for both `SELECT DISTINCT ...` and `SELECT DISTINCT ON (...) ...` - `false` (the
* default, also written explicitly as `SELECT ALL ...`) means every source row that survives
* WHERE/GROUP BY/HAVING produces its own output row. See [distinctOn] for what distinguishes
* the two `true` forms from each other.
*/
val distinct: Boolean = false,
/**
* Empty for plain `SELECT DISTINCT` (dedup by the *entire* output row, evaluated after
* projection - see [kestrel.engine.SelectRunner]'s `Plan.projectRows`). Non-empty for
* `SELECT DISTINCT ON (expr, ...) ...`: keeps only the first source row per distinct
* combination of these expressions, evaluated the same way `GROUP BY` expressions are
* (pre-projection, against the joined/filtered row) - never resolved against a `SELECT`-list
* alias, matching Postgres' own grammar where `DISTINCT ON (...)` is parsed before the select
* list even exists. "First" means first in `ORDER BY` order when one is given (which is why the
* parser requires `ORDER BY`, if present at all, to start with exactly these expressions - the
* whole point of `DISTINCT ON` is picking a deterministic row per group), otherwise first in
* scan/join order. Deliberately rejected outright when combined with `GROUP BY` or aggregates -
* unlike plain [distinct], there is no unsurprising way to define "first row of a group" once
* the underlying rows have already been collapsed into aggregate groups.
*/
val distinctOn: List<Expression> = emptyList()
) : QueryStatement
data class DeleteStatement(val tableName: String, val where: Expression?) : Statement /**
* Distinguishes `GROUP BY ROLLUP (...)` / `CUBE (...)` / `GROUPING SETS (...)` from an ordinary
* `GROUP BY a, b, ...` (represented by `groupingSpec == null`, the only case before this existed).
* [kestrel.engine.GroupingSetExpansion] turns any of these into the concrete list of grouping sets
* [kestrel.engine.GroupAggregation] actually groups by - one full pass over the source rows per set,
* unioned together, so a single query reports subtotals (and, for [Rollup]/[Cube]/an explicit `()`
* element, a grand total) alongside whatever detail rows survive. Mixing plain columns with
* ROLLUP/CUBE in the same `GROUP BY` list (SQL allows this, producing a cross product of sets) is a
* scope cut - the parser only accepts one of these forms standing alone as the entire clause.
*/
sealed interface GroupingSpec
data class RollupSpec(val expressions: List<Expression>) : GroupingSpec
data class CubeSpec(val expressions: List<Expression>) : GroupingSpec
/** Each element of [sets] is one grouping set as written; an empty inner list is the `()` grand-total set. */
data class GroupingSetsSpec(val sets: List<List<Expression>>) : GroupingSpec
enum class SetOperator { Union, UnionAll, Intersect, IntersectAll, Except, ExceptAll }
data class SetOperationStatement(
val operator: SetOperator,
val left: QueryStatement,
val right: QueryStatement,
val orderBy: OrderBy? = null,
val limit: Int? = null,
val offset: Int? = null
) : QueryStatement
data class CommonTableExpression(
val name: String,
val columns: List<String>?,
val query: QueryStatement
)
data class WithQuery(
val recursive: Boolean,
val ctes: List<CommonTableExpression>,
val body: QueryStatement
) : QueryStatement
sealed interface FromItem
data class TableSource(val name: String, val alias: String? = null) : FromItem
data class SubquerySource(val select: QueryStatement, val alias: String, val lateral: Boolean = false) : FromItem
data class DeleteStatement(
val tableName: String,
val where: Expression?,
val returning: Projection? = null
) : PreparableStatement
data class UpdateStatement( data class UpdateStatement(
val tableName: String, val tableName: String,