feat(parser): extend Parser with parseColumnCollation

This commit is contained in:
dualfroz committed 2026-09-02 22:29:38 +02:00
1 parent 4e101e46eb
commit c2329a388e
1 file changed
+190 -6
+190 -6
View File
@@ -500,7 +500,153 @@ class Parser(private val tokens: List<Token>) {
else -> break
}
}
return Column(columnName, type, notNull, primaryKey)
if (generated != null && default != null) {
throw error("a column cannot declare both DEFAULT and GENERATED ALWAYS AS", peek())
}
if (generated != null && primaryKey) {
throw error("a GENERATED column cannot be a PRIMARY KEY", peek())
}
return Column(columnName, spec.type, notNull, primaryKey, spec.precision, spec.scale, default, generated, collation)
}
/** `COLLATE name` on a column definition (`CREATE TABLE`/`ALTER TABLE ADD COLUMN`) - reuses the same name validation as expression-level `expr COLLATE name` (see [parseCollate]). */
private fun parseColumnCollation(): Collation {
advance()
val nameToken = expect(TokenType.Identifier, "expected a collation name after COLLATE")
return Collation.fromName(nameToken.lexeme) ?: throw error("no such collating sequence: ${nameToken.lexeme}", nameToken)
}
/**
* `GENERATED ALWAYS AS ( expression ) STORED` - Postgres only ever supports `STORED` (unlike
* MySQL/SQLite, which also allow `VIRTUAL`), so this engine matches Postgres exactly rather than
* accepting a keyword it would then have to silently reinterpret. `GENERATED`/`ALWAYS`/`STORED`
* are contextual (same mechanism as `ROLLUP`/`CUBE`/`FILTER`), not reserved keywords, so a table
* can still name a column `generated` without quoting.
*/
private fun parseGeneratedClause(): Expression {
advance()
if (!isContextualKeyword("ALWAYS")) throw error("expected ALWAYS after GENERATED", peek())
advance()
expect(TokenType.As, "expected AS after GENERATED ALWAYS")
expect(TokenType.LeftParen, "expected '(' after GENERATED ALWAYS AS")
val expression = parseExpression()
expect(TokenType.RightParen, "expected ')' to close GENERATED ALWAYS AS expression")
if (!isContextualKeyword("STORED")) throw error("expected STORED after GENERATED ALWAYS AS (...)", peek())
advance()
return expression
}
private fun parseTableUnique(): UniqueConstraint {
expect(TokenType.Unique, "expected UNIQUE")
expect(TokenType.LeftParen, "expected '(' after UNIQUE")
val columns = mutableListOf<String>()
do {
columns.add(expectIdentifier("expected column name"))
} while (match(TokenType.Comma))
expect(TokenType.RightParen, "expected ')' after UNIQUE column list")
return UniqueConstraint(null, columns)
}
private fun parseTableForeignKey(): ForeignKeyConstraint {
expect(TokenType.Foreign, "expected FOREIGN")
expect(TokenType.Key, "expected KEY after FOREIGN")
expect(TokenType.LeftParen, "expected '(' after FOREIGN KEY")
val columnNames = mutableListOf<String>()
do {
columnNames.add(expectIdentifier("expected column name"))
} while (match(TokenType.Comma))
expect(TokenType.RightParen, "expected ')' after foreign key column list")
return parseInlineForeignKey(columnNames)
}
private fun parseInlineForeignKey(columnName: String): ForeignKeyConstraint = parseInlineForeignKey(listOf(columnName))
private fun parseInlineForeignKey(columnNames: List<String>): ForeignKeyConstraint {
expect(TokenType.References, "expected REFERENCES")
val refTable = expectIdentifier("expected referenced table name")
expect(TokenType.LeftParen, "expected '(' before referenced column list")
val refColumns = mutableListOf<String>()
do {
refColumns.add(expectIdentifier("expected referenced column name"))
} while (match(TokenType.Comma))
expect(TokenType.RightParen, "expected ')' after referenced column list")
if (refColumns.size != columnNames.size) {
throw error(
"FOREIGN KEY (${columnNames.joinToString(", ")}) has ${columnNames.size} column(s) but " +
"REFERENCES $refTable(${refColumns.joinToString(", ")}) has ${refColumns.size}",
peek()
)
}
var onDelete = ForeignKeyAction.NoAction
var onUpdate = ForeignKeyAction.NoAction
while (check(TokenType.On)) {
advance()
when {
match(TokenType.Delete) -> onDelete = parseForeignKeyAction()
match(TokenType.Update) -> onUpdate = parseForeignKeyAction()
else -> throw error("expected DELETE or UPDATE after ON", peek())
}
}
return ForeignKeyConstraint(columnNames, refTable, refColumns, onDelete, onUpdate)
}
private fun parseForeignKeyAction(): ForeignKeyAction = when {
match(TokenType.Cascade) -> ForeignKeyAction.Cascade
match(TokenType.Restrict) -> ForeignKeyAction.Restrict
match(TokenType.Set) -> {
expect(TokenType.Null, "expected NULL after SET")
ForeignKeyAction.SetNull
}
match(TokenType.No) -> {
expect(TokenType.Action, "expected ACTION after NO")
ForeignKeyAction.NoAction
}
else -> throw error("expected CASCADE, RESTRICT, SET NULL, or NO ACTION but found '${describe(peek())}'", peek())
}
private fun parseTableCheck(): CheckConstraint = parseCheckConstraint()
private fun parseColumnCheck(): CheckConstraint = parseCheckConstraint()
private fun parseCheckConstraint(): CheckConstraint {
expect(TokenType.Check, "expected CHECK")
expect(TokenType.LeftParen, "expected '(' after CHECK")
val expression = parseExpression()
expect(TokenType.RightParen, "expected ')' after CHECK expression")
return CheckConstraint(null, expression)
}
private data class TypeSpec(val type: ColumnType, val precision: Int?, val scale: Int?)
/** Shared by column definitions (`CREATE TABLE`) and `CAST(... AS type)`. */
private fun parseTypeSpec(): TypeSpec {
val typeToken = advance()
val type = when (typeToken.type) {
TokenType.Integer -> ColumnType.Integer
TokenType.Text -> ColumnType.Text
TokenType.Boolean -> ColumnType.Boolean
TokenType.Real -> ColumnType.Real
TokenType.Decimal -> ColumnType.Decimal
TokenType.Date -> ColumnType.Date
else -> throw error("expected a type but found '${describe(typeToken)}'", typeToken)
}
var precision: Int? = null
var scale: Int? = null
if (type == ColumnType.Decimal && match(TokenType.LeftParen)) {
precision = expectIntLiteral("expected precision in DECIMAL(...)")
scale = if (match(TokenType.Comma)) expectIntLiteral("expected scale in DECIMAL(precision, scale)") else 0
expect(TokenType.RightParen, "expected ')' after DECIMAL precision/scale")
}
return TypeSpec(type, precision, scale)
}
private fun expectIntLiteral(message: String): Int = expect(TokenType.IntLiteral, message).intValue!!.toInt()
/** A sequence option's bound (MINVALUE/MAXVALUE/START WITH/RESTART WITH) needs the full 64-bit range, unlike [expectIntLiteral]'s callers - and, unlike those, may be negative for a descending sequence. */
private fun expectLongLiteral(message: String): Long {
val negative = match(TokenType.Minus)
val value = expect(TokenType.IntLiteral, message).intValue!!
return if (negative) -value else value
}
private fun parseInsert(): InsertStatement {
@@ -517,24 +663,62 @@ class Parser(private val tokens: List<Token>) {
columns = names
}
expect(TokenType.Values, "expected VALUES")
val rows = mutableListOf<List<Expression>>()
val rows = mutableListOf<List<Expression?>>()
do {
expect(TokenType.LeftParen, "expected '(' before value list")
val values = mutableListOf<Expression>()
val values = mutableListOf<Expression?>()
do {
values.add(parseExpression())
values.add(if (match(TokenType.Default)) null else parseExpression())
} while (match(TokenType.Comma))
expect(TokenType.RightParen, "expected ')' after value list")
rows.add(values)
} while (match(TokenType.Comma))
return InsertStatement(tableName, columns, rows)
val onConflict = if (check(TokenType.On)) parseOnConflict() else null
val returning = parseReturning()
return InsertStatement(tableName, columns, rows, onConflict, returning)
}
private fun parseReturning(): Projection? = if (match(TokenType.Returning)) parseProjection() else null
private fun parseOnConflict(): OnConflictClause {
expect(TokenType.On, "expected ON")
expect(TokenType.Conflict, "expected CONFLICT after ON")
var targetColumns: List<String> = emptyList()
if (match(TokenType.LeftParen)) {
val names = mutableListOf<String>()
do {
names.add(expectIdentifier("expected column name"))
} while (match(TokenType.Comma))
expect(TokenType.RightParen, "expected ')' after ON CONFLICT column list")
targetColumns = names
}
expect(TokenType.Do, "expected DO after ON CONFLICT")
val action: ConflictAction = when {
match(TokenType.Nothing) -> ConflictDoNothing
match(TokenType.Update) -> {
expect(TokenType.Set, "expected SET after DO UPDATE")
val assignments = parseAssignmentList()
val where = if (match(TokenType.Where)) parseExpression() else null
ConflictDoUpdate(assignments, where)
}
else -> throw error("expected NOTHING or UPDATE after DO but found '${describe(peek())}'", peek())
}
return OnConflictClause(targetColumns, action)
}
private fun parseSelect(): SelectStatement {
val core = parseSelectCore()
val orderBy = parseOrderBy()
val (limit, offset) = parseLimitOffset()
return core.copy(orderBy = orderBy, limit = limit, offset = offset)
}
private fun parseSelectCore(): SelectStatement {
expect(TokenType.Select, "expected SELECT")
val (distinct, distinctOn) = parseDistinctClause()
val projection = parseProjection()
expect(TokenType.From, "expected FROM")
val (tableName, tableAlias) = parseTableRef()
val from = parseFromItem()
val joins = parseJoins()
val where = if (match(TokenType.Where)) parseExpression() else null
val groupBy = parseGroupBy()