diff --git a/src/test/kotlin/kestrel/ParserTest.kt b/src/test/kotlin/kestrel/ParserTest.kt index 5840ec9..4ffe6c5 100644 --- a/src/test/kotlin/kestrel/ParserTest.kt +++ b/src/test/kotlin/kestrel/ParserTest.kt @@ -1630,4 +1630,177 @@ class ParserTest { val statement = parseOne( "CREATE TABLE t (id INTEGER PRIMARY KEY, dept_id INTEGER, FOREIGN KEY (dept_id) REFERENCES departments(id))" ) as CreateTableStatement + assertEquals(2, statement.columns.size) + val fk = statement.foreignKeys.single() + assertEquals(listOf("dept_id"), fk.columns) + assertEquals("departments", fk.referencedTable) + } + + @Test + fun parsesColumnLevelAndTableLevelCheckConstraints() { + val statement = parseOne( + "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > 0), CHECK (id <> 0))" + ) as CreateTableStatement + assertEquals(2, statement.checks.size) + assertTrue(statement.checks[0].expression is BinaryExpression) + assertTrue(statement.checks[1].expression is BinaryExpression) + } + + @Test + fun parsesColumnLevelUniqueConstraint() { + val statement = parseOne( + "CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT UNIQUE)" + ) as CreateTableStatement + val unique = statement.uniqueConstraints.single() + assertEquals(listOf("email"), unique.columns) + } + + @Test + fun parsesTableLevelCompositeUniqueConstraint() { + val statement = parseOne( + "CREATE TABLE t (a INTEGER, b INTEGER, c INTEGER, UNIQUE (a, b, c))" + ) as CreateTableStatement + val unique = statement.uniqueConstraints.single() + assertEquals(listOf("a", "b", "c"), unique.columns) + } + + @Test + fun parsesMultipleUniqueConstraintsMixingColumnAndTableLevel() { + val statement = parseOne( + "CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT UNIQUE, a INTEGER, b INTEGER, UNIQUE (a, b))" + ) as CreateTableStatement + assertEquals(2, statement.uniqueConstraints.size) + assertEquals(listOf("email"), statement.uniqueConstraints[0].columns) + assertEquals(listOf("a", "b"), statement.uniqueConstraints[1].columns) + } + + @Test + fun parsesAlterTableAddColumnWithNotNullAndDefault() { + val statement = parseOne("ALTER TABLE t ADD COLUMN active BOOLEAN NOT NULL DEFAULT TRUE") as AlterTableStatement + assertEquals("t", statement.tableName) + val action = statement.action as AddColumn + assertEquals("active", action.column.name) + assertEquals(ColumnType.Boolean, action.column.type) + assertTrue(action.column.notNull) + assertEquals(LiteralExpression(Value.BooleanValue(true)), action.column.default) + } + + @Test + fun parsesAlterTableAddColumnWithoutDefault() { + val statement = parseOne("ALTER TABLE t ADD COLUMN note TEXT") as AlterTableStatement + val action = statement.action as AddColumn + assertEquals("note", action.column.name) + assertFalse(action.column.notNull) + assertNull(action.column.default) + } + + @Test + fun parsesAlterTableDropColumnWithAndWithoutCascade() { + val plain = (parseOne("ALTER TABLE t DROP COLUMN note") as AlterTableStatement).action as DropColumn + assertEquals("note", plain.columnName) + assertEquals(DropMode.Restrict, plain.mode) + val cascaded = (parseOne("ALTER TABLE t DROP COLUMN note CASCADE") as AlterTableStatement).action as DropColumn + assertEquals(DropMode.Cascade, cascaded.mode) + } + + @Test + fun parsesAlterTableRenameColumn() { + val action = (parseOne("ALTER TABLE t RENAME COLUMN a TO b") as AlterTableStatement).action as RenameColumn + assertEquals("a", action.oldName) + assertEquals("b", action.newName) + } + + @Test + fun parsesAlterTableRenameTo() { + val statement = parseOne("ALTER TABLE t RENAME TO t2") as AlterTableStatement + assertEquals("t", statement.tableName) + val action = statement.action as RenameTable + assertEquals("t2", action.newName) + } + + @Test + fun parsesAlterTableAlterColumnTypeWithColumnKeywordAndUsing() { + val action = (parseOne("ALTER TABLE t ALTER COLUMN amount TYPE DECIMAL(10, 2) USING amount") as AlterTableStatement) + .action as ChangeColumnType + assertEquals("amount", action.columnName) + assertEquals(ColumnType.Decimal, action.newType) + assertEquals(10, action.precision) + assertEquals(2, action.scale) + assertEquals(ColumnExpression("amount"), action.using) + } + + @Test + fun parsesAlterTableAlterColumnTypeWithoutColumnKeywordOrUsing() { + val action = (parseOne("ALTER TABLE t ALTER amount TYPE REAL") as AlterTableStatement).action as ChangeColumnType + assertEquals("amount", action.columnName) + assertEquals(ColumnType.Real, action.newType) + assertNull(action.precision) + assertNull(action.using) + } + + @Test + fun alterTableAlterColumnRejectsAMissingTypeKeyword() { + assertFailsWith { parse("ALTER TABLE t ALTER COLUMN amount REAL") } + } + + @Test + fun alterTableAddColumnRejectsPrimaryKeyReferencesUniqueAndCheck() { + assertFailsWith { parse("ALTER TABLE t ADD COLUMN id INTEGER PRIMARY KEY") } + assertFailsWith { parse("ALTER TABLE t ADD COLUMN dept_id INTEGER REFERENCES departments(id)") } + assertFailsWith { parse("ALTER TABLE t ADD COLUMN email TEXT UNIQUE") } + assertFailsWith { parse("ALTER TABLE t ADD COLUMN age INTEGER CHECK (age > 0)") } + } + + @Test + fun parsesColumnLevelDefaultInCreateTable() { + val statement = parseOne("CREATE TABLE t (id INTEGER PRIMARY KEY, status TEXT DEFAULT 'new')") as CreateTableStatement + assertEquals(LiteralExpression(Value.TextValue("new")), statement.columns[1].default) + } + + @Test + fun parsesGeneratedAlwaysAsStoredInCreateTable() { + val statement = parseOne("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a * 2) STORED)") as CreateTableStatement + assertEquals(null, statement.columns[0].generated) + assertEquals( + BinaryExpression(BinaryOperator.Multiply, ColumnExpression("a"), LiteralExpression(Value.IntegerValue(2))), + statement.columns[1].generated + ) + } + + @Test + fun generatedColumnRequiresAlwaysKeyword() { + assertFailsWith { parse("CREATE TABLE t (a INTEGER, b INTEGER GENERATED AS (a) STORED)") } + } + + @Test + fun generatedColumnRequiresAsKeyword() { + assertFailsWith { parse("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS (a) STORED)") } + } + + @Test + fun generatedColumnRequiresStoredKeyword() { + assertFailsWith { parse("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a))") } + } + + @Test + fun generatedColumnRejectsCombiningWithDefault() { + assertFailsWith { parse("CREATE TABLE t (a INTEGER, b INTEGER DEFAULT 1 GENERATED ALWAYS AS (a) STORED)") } + } + + @Test + fun generatedColumnRejectsCombiningWithPrimaryKey() { + assertFailsWith { + parse("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a) STORED PRIMARY KEY)") + } + } + + @Test + fun generatedIsAContextualKeywordUsableAsAColumnName() { + val statement = parseOne("CREATE TABLE t (generated INTEGER, always INTEGER, stored INTEGER)") as CreateTableStatement + assertEquals(listOf("generated", "always", "stored"), statement.columns.map { it.name }) + } + + @Test + fun parsesGeneratedAlwaysAsStoredInAlterTableAddColumn() { + val statement = parseOne("ALTER TABLE t ADD COLUMN b INTEGER GENERATED ALWAYS AS (a * 2) STORED") as AlterTableStatement }