feat(parser): extend Parser with parseTruncate and parseDropMode

This commit is contained in:
dualfroz committed 2026-09-04 14:59:37 +02:00
1 parent 81653b5d5a
commit 89c9c7e6ca
1 file changed
+191
@@ -1129,6 +1129,197 @@ class Parser(private val tokens: List<Token>) {
}
}
/**
* `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 {