diff --git a/src/main/kotlin/kestrel/sql/parser/Parser.kt b/src/main/kotlin/kestrel/sql/parser/Parser.kt index f50996b..404a5a4 100644 --- a/src/main/kotlin/kestrel/sql/parser/Parser.kt +++ b/src/main/kotlin/kestrel/sql/parser/Parser.kt @@ -500,7 +500,153 @@ class Parser(private val tokens: List) { 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() + 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() + 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): ForeignKeyConstraint { + expect(TokenType.References, "expected REFERENCES") + val refTable = expectIdentifier("expected referenced table name") + expect(TokenType.LeftParen, "expected '(' before referenced column list") + val refColumns = mutableListOf() + 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) { columns = names } expect(TokenType.Values, "expected VALUES") - val rows = mutableListOf>() + val rows = mutableListOf>() do { expect(TokenType.LeftParen, "expected '(' before value list") - val values = mutableListOf() + val values = mutableListOf() 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 = emptyList() + if (match(TokenType.LeftParen)) { + val names = mutableListOf() + 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()