From 4e101e46eb429f7de00bca59ca6e12a96ba56b2f Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 2 Sep 2026 21:57:10 +0200 Subject: [PATCH] feat(parser): extend Parser with parseTablePrimaryKey --- src/main/kotlin/kestrel/sql/parser/Parser.kt | 70 +++++++++++++++++--- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/kestrel/sql/parser/Parser.kt b/src/main/kotlin/kestrel/sql/parser/Parser.kt index cd0ff0f..f50996b 100644 --- a/src/main/kotlin/kestrel/sql/parser/Parser.kt +++ b/src/main/kotlin/kestrel/sql/parser/Parser.kt @@ -420,18 +420,58 @@ class Parser(private val tokens: List) { return CreateTableStatement(tableName, columns, foreignKeys, checks, uniqueConstraints, compositePrimaryKey, temporary) } - private fun parseColumnDefinition(): Column { - val columnName = expectIdentifier("expected column name") - val typeToken = advance() - val type = when (typeToken.type) { - TokenType.Integer -> ColumnType.Integer - TokenType.Text -> ColumnType.Text - TokenType.Boolean -> ColumnType.Boolean - TokenType.Real -> ColumnType.Real - else -> throw error("expected a column type but found '${describe(typeToken)}'", typeToken) + private fun parseTablePrimaryKey(): List { + expect(TokenType.Primary, "expected PRIMARY") + expect(TokenType.Key, "expected KEY after PRIMARY") + expect(TokenType.LeftParen, "expected '(' after PRIMARY KEY") + val columns = mutableListOf() + do { + columns.add(expectIdentifier("expected column name")) + } while (match(TokenType.Comma)) + expect(TokenType.RightParen, "expected ')' after PRIMARY KEY column list") + return columns + } + + /** + * `CONSTRAINT name FOREIGN KEY (...) | CHECK (...) | UNIQUE (...)` - a named table-level + * constraint, standard SQL syntax this engine accepts both inline in `CREATE TABLE` (here) and + * via `ALTER TABLE ... ADD` (see [parseAlterTable]'s identical dispatch). `PRIMARY KEY` is + * deliberately not accepted after `CONSTRAINT name`: [kestrel.catalog.TableSchema] has no name + * field for a table's PRIMARY KEY at all, so there would be nowhere to put it, and + * `ALTER TABLE ... DROP CONSTRAINT` never needs to target a PRIMARY KEY (see + * [kestrel.catalog.Catalog.dropColumn]'s own doc on why dropping a PRIMARY KEY is out of scope + * entirely - redesign it or drop the table instead). + */ + private fun parseNamedTableConstraint( + foreignKeys: MutableList, + checks: MutableList, + uniqueConstraints: MutableList + ) { + advance() + val name = expectIdentifier("expected constraint name after CONSTRAINT") + when { + check(TokenType.Foreign) -> foreignKeys.add(parseTableForeignKey().copy(name = name)) + check(TokenType.Check) -> checks.add(parseTableCheck().copy(name = name)) + check(TokenType.Unique) -> uniqueConstraints.add(parseTableUnique().copy(name = name)) + else -> throw error( + "expected FOREIGN KEY, CHECK, or UNIQUE after CONSTRAINT $name but found '${describe(peek())}'", + peek() + ) } + } + + private fun parseColumnDefinition( + foreignKeys: MutableList, + checks: MutableList, + uniqueConstraints: MutableList + ): Column { + val columnName = expectIdentifier("expected column name") + val spec = parseTypeSpec() var notNull = false var primaryKey = false + var default: Expression? = null + var generated: Expression? = null + var collation = Collation.Binary while (true) { when { check(TokenType.Not) -> { @@ -445,6 +485,18 @@ class Parser(private val tokens: List) { primaryKey = true notNull = true } + check(TokenType.Unique) -> { + advance() + uniqueConstraints.add(UniqueConstraint(null, listOf(columnName))) + } + check(TokenType.References) -> foreignKeys.add(parseInlineForeignKey(columnName)) + check(TokenType.Check) -> checks.add(parseColumnCheck()) + check(TokenType.Default) -> { + advance() + default = parseExpression() + } + isContextualKeyword("GENERATED") -> generated = parseGeneratedClause() + check(TokenType.Collate) -> collation = parseColumnCollation() else -> break } }