From 3eb5cd7fcae9229f0d15644b37a56453e2fac07c Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 2 Sep 2026 20:29:21 +0200 Subject: [PATCH] feat(ast): extend Ast with CreateTableStatement and DropMode --- src/main/kotlin/kestrel/sql/ast/Ast.kt | 202 +++++++++++++++++++++++-- 1 file changed, 192 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/kestrel/sql/ast/Ast.kt b/src/main/kotlin/kestrel/sql/ast/Ast.kt index 3c9ec59..e4633d2 100644 --- a/src/main/kotlin/kestrel/sql/ast/Ast.kt +++ b/src/main/kotlin/kestrel/sql/ast/Ast.kt @@ -1,41 +1,223 @@ package kestrel.sql.ast +import kestrel.catalog.CheckConstraint import kestrel.catalog.Column +import kestrel.catalog.ForeignKeyConstraint +import kestrel.catalog.UniqueConstraint +import kestrel.storage.Collation +import kestrel.storage.ColumnType import kestrel.storage.Value sealed interface Statement -data class CreateTableStatement(val tableName: String, val columns: List) : Statement +data class CreateTableStatement( + val tableName: String, + val columns: List, + val foreignKeys: List = emptyList(), + val checks: List = emptyList(), + val uniqueConstraints: List = emptyList(), + val compositePrimaryKey: List = 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, + val restartIdentity: Boolean = false, + val mode: DropMode = DropMode.Restrict +) : Statement data class CreateIndexStatement( val indexName: String, val tableName: String, - val columnName: String + val columnNames: List, + val unique: Boolean = false ) : 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( val tableName: String, val columns: List?, - val rows: List> -) : Statement + val rows: List>, + 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, val action: ConflictAction) + +sealed interface ConflictAction + +data object ConflictDoNothing : ConflictAction + +data class ConflictDoUpdate(val assignments: List, 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( val projection: Projection, - val tableName: String, - val tableAlias: String? = null, + val from: FromItem, val joins: List = emptyList(), 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 = emptyList(), + val groupingSpec: GroupingSpec? = null, val having: Expression? = null, val orderBy: OrderBy? = null, - val limit: Int? = null -) : Statement + val limit: Int? = null, + /** `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 = 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) : GroupingSpec + +data class CubeSpec(val expressions: List) : 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>) : 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?, + val query: QueryStatement +) + +data class WithQuery( + val recursive: Boolean, + val ctes: List, + 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( val tableName: String,