From 48fa9202a4e4cf8a4f3f358799e5e08af48a1d21 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 2 Sep 2026 21:41:00 +0200 Subject: [PATCH] feat(parser): extend Parser with parseCreateSequence --- src/main/kotlin/kestrel/sql/parser/Parser.kt | 202 ++++++++++++++++++- 1 file changed, 195 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/kestrel/sql/parser/Parser.kt b/src/main/kotlin/kestrel/sql/parser/Parser.kt index 90c8c4c..cd0ff0f 100644 --- a/src/main/kotlin/kestrel/sql/parser/Parser.kt +++ b/src/main/kotlin/kestrel/sql/parser/Parser.kt @@ -186,20 +186,133 @@ class Parser(private val tokens: List) { } } - private fun parseCreateIndex(): CreateIndexStatement { + /** + * Every numeric option (`INCREMENT [BY]`/`MINVALUE`/`MAXVALUE`/`START [WITH]`) may appear at + * most once and in any order, matching Postgres; a repeated option silently keeps the last one + * written rather than erroring, the same "last one wins" leniency [parseAlterTable] already + * gives `ADD`/`DROP`/`RENAME` if written more than once in spirit (though that one can only ever + * carry a single action). `NO MINVALUE`/`NO MAXVALUE` here are never distinguished from "not + * written at all" - see [CreateSequenceStatement]'s own doc for why that's fine for CREATE + * specifically (unlike [AlterSequenceStatement], there is no earlier bound to preserve). + */ + private fun parseCreateSequence(): CreateSequenceStatement { + expect(TokenType.Sequence, "expected SEQUENCE") + val name = expectIdentifier("expected sequence name") + var incrementBy: Long? = null + var minValue: Long? = null + var maxValue: Long? = null + var startValue: Long? = null + var cycle = false + 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.Start) -> { + match(TokenType.With) + startValue = expectLongLiteral("expected a value after START WITH") + } + match(TokenType.Cycle) -> cycle = true + check(TokenType.No) && peekIsSequenceNoClause() -> { + advance() + when { + match(TokenType.Minvalue) -> minValue = null + match(TokenType.Maxvalue) -> maxValue = null + match(TokenType.Cycle) -> cycle = false + else -> error("unreachable: guarded by peekIsSequenceNoClause") + } + } + else -> break + } + } + return CreateSequenceStatement(name, incrementBy, minValue, maxValue, startValue, cycle) + } + + /** Distinguishes a sequence option's `NO MINVALUE|MAXVALUE|CYCLE` from `ON DELETE NO ACTION`'s unrelated `NO` - both start with the same token. */ + private fun peekIsSequenceNoClause(): Boolean = + peekAt(1)?.type in setOf(TokenType.Minvalue, TokenType.Maxvalue, TokenType.Cycle) + + private fun peekAt(offset: Int): Token? = tokens.getOrNull(position + offset) + + /** + * `body` is deliberately restricted to INSERT/UPDATE/DELETE ([parseTriggerBodyStatement]) - + * the only statement kinds with a side effect worth firing, matching SQLite's own trigger-body + * grammar. The closing `END` here is the trigger body's own terminator, distinct from the `;` + * `parseProgram` still expects after the whole CREATE TRIGGER statement. + */ + private fun parseCreateTrigger(): CreateTriggerStatement { + expect(TokenType.Trigger, "expected TRIGGER") + val triggerName = expectIdentifier("expected trigger name") + val timing = when { + match(TokenType.Before) -> TriggerTiming.Before + match(TokenType.After) -> TriggerTiming.After + else -> throw error("expected BEFORE or AFTER after trigger name but found '${describe(peek())}'", peek()) + } + val event = when { + match(TokenType.Insert) -> TriggerEvent.Insert + match(TokenType.Update) -> TriggerEvent.Update + match(TokenType.Delete) -> TriggerEvent.Delete + else -> throw error("expected INSERT, UPDATE, or DELETE after ${timing.name.uppercase()} but found '${describe(peek())}'", peek()) + } + expect(TokenType.On, "expected ON after trigger event") + val tableName = expectIdentifier("expected table name") + expect(TokenType.For, "expected FOR after table name") + expect(TokenType.Each, "expected EACH after FOR") + expect(TokenType.Row, "expected ROW after EACH") + val whenClause = if (match(TokenType.When)) parseExpression() else null + expect(TokenType.Begin, "expected BEGIN to start trigger body") + val body = mutableListOf() + while (!check(TokenType.End)) { + body.add(parseTriggerBodyStatement()) + expect(TokenType.Semicolon, "expected ';' after trigger body statement") + } + expect(TokenType.End, "expected END to close trigger body") + return CreateTriggerStatement(triggerName, timing, event, tableName, whenClause, body) + } + + private fun parseTriggerBodyStatement(): Statement = when (peek().type) { + TokenType.Insert -> parseInsert() + TokenType.Update -> parseUpdate() + TokenType.Delete -> parseDelete() + else -> throw error( + "trigger body only supports INSERT, UPDATE, or DELETE statements but found '${describe(peek())}'", + peek() + ) + } + + private fun parseCreateView(orReplace: Boolean): CreateViewStatement { + expect(TokenType.View, "expected VIEW") + val viewName = expectIdentifier("expected view name") + expect(TokenType.As, "expected AS after view name") + val query = parseQueryStatement() + return CreateViewStatement(viewName, query, orReplace) + } + + private fun parseCreateUniqueIndex(): CreateIndexStatement { + expect(TokenType.Unique, "expected UNIQUE") + return parseCreateIndex(unique = true) + } + + private fun parseCreateIndex(unique: Boolean): CreateIndexStatement { expect(TokenType.Index, "expected INDEX") val indexName = expectIdentifier("expected index name") expect(TokenType.On, "expected ON after index name") val tableName = expectIdentifier("expected table name") expect(TokenType.LeftParen, "expected '(' before indexed column") - val columnName = expectIdentifier("expected column name") + val columnNames = mutableListOf(expectIdentifier("expected column name")) + while (match(TokenType.Comma)) { + columnNames.add(expectIdentifier("expected column name")) + } expect(TokenType.RightParen, "expected ')' after indexed column") - return CreateIndexStatement(indexName, tableName, columnName) + return CreateIndexStatement(indexName, tableName, columnNames, unique) } private fun parseExplain(): ExplainStatement { expect(TokenType.Explain, "expected EXPLAIN") - return ExplainStatement(parseSelect()) + return ExplainStatement(parseQueryStatement()) } private fun parseBegin(): Statement { @@ -216,20 +329,95 @@ class Parser(private val tokens: List) { private fun parseRollback(): Statement { expect(TokenType.Rollback, "expected ROLLBACK") + if (match(TokenType.To)) { + match(TokenType.Savepoint) + val name = expectIdentifier("expected savepoint name after ROLLBACK TO") + return RollbackToSavepointStatement(name) + } match(TokenType.Transaction) return RollbackStatement } - private fun parseCreateTable(): CreateTableStatement { + private fun parseSavepoint(): Statement { + expect(TokenType.Savepoint, "expected SAVEPOINT") + val name = expectIdentifier("expected savepoint name after SAVEPOINT") + return SavepointStatement(name) + } + + private fun parseReleaseSavepoint(): Statement { + expect(TokenType.Release, "expected RELEASE") + match(TokenType.Savepoint) + val name = expectIdentifier("expected savepoint name after RELEASE") + return ReleaseSavepointStatement(name) + } + + private fun parsePrepare(): Statement { + expect(TokenType.Prepare, "expected PREPARE") + val name = expectIdentifier("expected prepared statement name") + expect(TokenType.As, "expected AS after prepared statement name") + return PrepareStatement(name, parsePreparableStatement()) + } + + private fun parsePreparableStatement(): PreparableStatement = when (peek().type) { + TokenType.Select, TokenType.With -> parseQueryStatement() + TokenType.Insert -> parseInsert() + TokenType.Update -> parseUpdate() + TokenType.Delete -> parseDelete() + TokenType.Merge -> parseMerge() + else -> throw error( + "PREPARE only supports SELECT, INSERT, UPDATE, DELETE, or MERGE but found '${describe(peek())}'", + peek() + ) + } + + private fun parseExecute(): Statement { + expect(TokenType.Execute, "expected EXECUTE") + val name = expectIdentifier("expected prepared statement name") + val arguments = mutableListOf() + if (match(TokenType.LeftParen)) { + if (!check(TokenType.RightParen)) { + arguments.add(parseExpression()) + while (match(TokenType.Comma)) arguments.add(parseExpression()) + } + expect(TokenType.RightParen, "expected ')' after EXECUTE arguments") + } + return ExecuteStatement(name, arguments) + } + + private fun parseDeallocate(): Statement { + expect(TokenType.Deallocate, "expected DEALLOCATE") + match(TokenType.Prepare) + if (match(TokenType.All)) return DeallocateStatement(null, all = true) + val name = expectIdentifier("expected prepared statement name or ALL after DEALLOCATE") + return DeallocateStatement(name) + } + + private fun parseCreateTable(temporary: Boolean = false): CreateTableStatement { expect(TokenType.Table, "expected TABLE after CREATE") val tableName = expectIdentifier("expected table name") expect(TokenType.LeftParen, "expected '(' after table name") val columns = mutableListOf() + val foreignKeys = mutableListOf() + val checks = mutableListOf() + val uniqueConstraints = mutableListOf() + var compositePrimaryKey: List = emptyList() do { - columns.add(parseColumnDefinition()) + when { + check(TokenType.Constraint) -> parseNamedTableConstraint(foreignKeys, checks, uniqueConstraints) + check(TokenType.Foreign) -> foreignKeys.add(parseTableForeignKey()) + check(TokenType.Check) -> checks.add(parseTableCheck()) + check(TokenType.Unique) -> uniqueConstraints.add(parseTableUnique()) + check(TokenType.Primary) -> { + if (compositePrimaryKey.isNotEmpty()) { + throw error("table declares more than one PRIMARY KEY clause", peek()) + } + compositePrimaryKey = parseTablePrimaryKey() + } + else -> columns.add(parseColumnDefinition(foreignKeys, checks, uniqueConstraints)) + } } while (match(TokenType.Comma)) expect(TokenType.RightParen, "expected ')' after column definitions") - return CreateTableStatement(tableName, columns) + return CreateTableStatement(tableName, columns, foreignKeys, checks, uniqueConstraints, compositePrimaryKey, temporary) } private fun parseColumnDefinition(): Column {