From 89c9c7e6ca45d75363f293bebe2b9b8c1c3babbd Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 4 Sep 2026 14:59:37 +0200 Subject: [PATCH] feat(parser): extend Parser with parseTruncate and parseDropMode --- src/main/kotlin/kestrel/sql/parser/Parser.kt | 191 +++++++++++++++++++ 1 file changed, 191 insertions(+) diff --git a/src/main/kotlin/kestrel/sql/parser/Parser.kt b/src/main/kotlin/kestrel/sql/parser/Parser.kt index a93a68a..df871c6 100644 --- a/src/main/kotlin/kestrel/sql/parser/Parser.kt +++ b/src/main/kotlin/kestrel/sql/parser/Parser.kt @@ -1129,6 +1129,197 @@ class Parser(private val tokens: List) { } } + /** + * `TRUNCATE [TABLE] t1 [, t2, ...] [RESTART IDENTITY | CONTINUE IDENTITY] [CASCADE | RESTRICT]`. + * `TABLE` and the identity clause are both optional, matching Postgres (which also defaults to + * CONTINUE IDENTITY when neither RESTART nor CONTINUE is written). + */ + private fun parseTruncate(): TruncateTableStatement { + expect(TokenType.Truncate, "expected TRUNCATE") + match(TokenType.Table) + val tableNames = mutableListOf(expectIdentifier("expected table name")) + while (match(TokenType.Comma)) { + tableNames.add(expectIdentifier("expected table name")) + } + val restartIdentity = when { + match(TokenType.Restart) -> { + expect(TokenType.Identity, "expected IDENTITY after RESTART") + true + } + match(TokenType.Continue) -> { + expect(TokenType.Identity, "expected IDENTITY after CONTINUE") + false + } + else -> false + } + return TruncateTableStatement(tableNames, restartIdentity, parseDropMode()) + } + + private fun parseDropMode(): DropMode = when { + match(TokenType.Cascade) -> DropMode.Cascade + match(TokenType.Restrict) -> DropMode.Restrict + else -> DropMode.Restrict + } + + private fun parseAlter(): Statement { + expect(TokenType.Alter, "expected ALTER") + return when { + check(TokenType.Table) -> parseAlterTable() + check(TokenType.Sequence) -> parseAlterSequence() + else -> throw error("expected TABLE or SEQUENCE after ALTER but found '${describe(peek())}'", peek()) + } + } + + /** + * Every option may appear at most once and in any order, same leniency as + * [parseCreateSequence]. `NO MINVALUE`/`NO MAXVALUE` here are NOT equivalent to leaving the + * option out entirely - see [AlterSequenceStatement]'s own doc. + */ + private fun parseAlterSequence(): AlterSequenceStatement { + expect(TokenType.Sequence, "expected SEQUENCE") + val name = expectIdentifier("expected sequence name") + var incrementBy: Long? = null + var minValue: Long? = null + var clearMinValue = false + var maxValue: Long? = null + var clearMaxValue = false + var restart = false + var restartWith: Long? = null + var cycle: Boolean? = null + while (true) { + when { + match(TokenType.Increment) -> { + match(TokenType.By) + incrementBy = expectLongLiteral("expected a value after INCREMENT BY") + } + match(TokenType.Minvalue) -> minValue = expectLongLiteral("expected a value after MINVALUE") + match(TokenType.Maxvalue) -> maxValue = expectLongLiteral("expected a value after MAXVALUE") + match(TokenType.Cycle) -> cycle = true + match(TokenType.Restart) -> { + restart = true + if (match(TokenType.With)) restartWith = expectLongLiteral("expected a value after RESTART WITH") + } + check(TokenType.No) && peekIsSequenceNoClause() -> { + advance() + when { + match(TokenType.Minvalue) -> clearMinValue = true + match(TokenType.Maxvalue) -> clearMaxValue = true + match(TokenType.Cycle) -> cycle = false + else -> error("unreachable: guarded by peekIsSequenceNoClause") + } + } + else -> break + } + } + return AlterSequenceStatement(name, incrementBy, minValue, clearMinValue, maxValue, clearMaxValue, restart, restartWith, cycle) + } + + private fun parseAlterTable(): AlterTableStatement { + expect(TokenType.Table, "expected TABLE after ALTER") + val tableName = expectIdentifier("expected table name") + val action: AlterTableAction = when { + match(TokenType.Add) -> { + if (match(TokenType.Column)) { + AddColumn(parseAlterAddColumnDefinition()) + } else { + expect(TokenType.Constraint, "expected COLUMN or CONSTRAINT after ADD") + val name = expectIdentifier("expected constraint name after CONSTRAINT") + val constraint: AddedConstraint = when { + check(TokenType.Foreign) -> AddedForeignKey(parseTableForeignKey().copy(name = name)) + check(TokenType.Check) -> AddedCheck(parseTableCheck().copy(name = name)) + check(TokenType.Unique) -> AddedUnique(parseTableUnique().copy(name = name)) + else -> throw error( + "expected FOREIGN KEY, CHECK, or UNIQUE after CONSTRAINT $name but found '${describe(peek())}'", + peek() + ) + } + AddConstraint(constraint) + } + } + match(TokenType.Drop) -> { + if (match(TokenType.Column)) { + val columnName = expectIdentifier("expected column name") + DropColumn(columnName, parseDropMode()) + } else { + expect(TokenType.Constraint, "expected COLUMN or CONSTRAINT after DROP") + val name = expectIdentifier("expected constraint name") + DropConstraint(name, parseDropMode()) + } + } + match(TokenType.Rename) -> { + if (match(TokenType.To)) { + val newName = expectIdentifier("expected new table name") + RenameTable(newName) + } else { + expect(TokenType.Column, "expected COLUMN or TO after RENAME") + val oldName = expectIdentifier("expected column name") + expect(TokenType.To, "expected TO after column name") + val newName = expectIdentifier("expected new column name") + RenameColumn(oldName, newName) + } + } + match(TokenType.Alter) -> { + match(TokenType.Column) + val columnName = expectIdentifier("expected column name") + if (!isContextualKeyword("TYPE")) { + throw error("expected TYPE after column name in ALTER TABLE ... ALTER COLUMN", peek()) + } + advance() + val spec = parseTypeSpec() + val using = if (match(TokenType.Using)) parseExpression() else null + ChangeColumnType(columnName, spec.type, spec.precision, spec.scale, using) + } + else -> throw error( + "expected ADD, DROP, RENAME, or ALTER after table name in ALTER TABLE but found '${describe(peek())}'", + peek() + ) + } + return AlterTableStatement(tableName, action) + } + + /** + * A deliberately restricted column definition: no PRIMARY KEY/REFERENCES/UNIQUE/CHECK, since + * enforcing those against a table that may already hold rows (composite-key interactions, + * FK target validation, CASCADE bookkeeping) is out of scope for a single ADD COLUMN - CREATE + * UNIQUE INDEX already covers UNIQUE on an existing table, and CREATE TABLE covers the rest. + * GENERATED ALWAYS AS is allowed, unlike those - see `Executor.executeAddColumn`/ + * `DiskExecutor.executeAddColumn` for why it does not share that "out of scope" reasoning + * (it backfills by evaluating the expression per existing row, not by writing one constant). + */ + private fun parseAlterAddColumnDefinition(): Column { + val columnName = expectIdentifier("expected column name") + val spec = parseTypeSpec() + var notNull = false + var default: Expression? = null + var generated: Expression? = null + var collation = Collation.Binary + while (true) { + when { + check(TokenType.Not) -> { + advance() + expect(TokenType.Null, "expected NULL after NOT") + notNull = true + } + check(TokenType.Default) -> { + advance() + default = parseExpression() + } + isContextualKeyword("GENERATED") -> generated = parseGeneratedClause() + check(TokenType.Collate) -> collation = parseColumnCollation() + check(TokenType.Primary) -> throw error("ALTER TABLE ADD COLUMN cannot declare PRIMARY KEY", peek()) + check(TokenType.References) -> throw error("ALTER TABLE ADD COLUMN cannot declare REFERENCES", peek()) + check(TokenType.Unique) -> + throw error("ALTER TABLE ADD COLUMN cannot declare UNIQUE; use CREATE UNIQUE INDEX instead", peek()) + check(TokenType.Check) -> throw error("ALTER TABLE ADD COLUMN cannot declare CHECK", peek()) + else -> break + } + } + if (generated != null && default != null) { + throw error("a column cannot declare both DEFAULT and GENERATED ALWAYS AS", peek()) + } + return Column(columnName, spec.type, notNull, primaryKey = false, spec.precision, spec.scale, default, generated, collation) + } + private fun parseExpression(): Expression = parseOr() private fun parseOr(): Expression {