From d53bea481ccc6990001068cea97cabe8580cec78 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 8 Sep 2026 16:14:29 +0200 Subject: [PATCH] test: cover dropColumnRestrictsWhenReferencedByAnotherTablesForeignKey --- src/test/kotlin/kestrel/AlterTableTest.kt | 156 ++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/src/test/kotlin/kestrel/AlterTableTest.kt b/src/test/kotlin/kestrel/AlterTableTest.kt index 4576a04..eb58fc9 100644 --- a/src/test/kotlin/kestrel/AlterTableTest.kt +++ b/src/test/kotlin/kestrel/AlterTableTest.kt @@ -157,3 +157,159 @@ class AlterTableTest { assertTrue(error.message!!.contains("PRIMARY KEY")) } + @Test + fun dropColumnRestrictsWhenReferencedByAnotherTablesForeignKey() { + run("CREATE TABLE departments (id INTEGER PRIMARY KEY, code TEXT UNIQUE)") + run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_code TEXT REFERENCES departments(code))") + val error = assertFailsWith { run("ALTER TABLE departments DROP COLUMN code") } + assertTrue(error.message!!.contains("FOREIGN KEY")) + } + + @Test + fun dropColumnCascadeRemovesTheReferencingForeignKey() { + run("CREATE TABLE departments (id INTEGER PRIMARY KEY, code TEXT UNIQUE)") + run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_code TEXT REFERENCES departments(code))") + run("INSERT INTO departments VALUES (1, 'eng')") + run("ALTER TABLE departments DROP COLUMN code CASCADE") + // The FK is gone, so an employee can now reference a nonexistent department code freely. + run("INSERT INTO employees VALUES (1, 'ghost')") + assertEquals(listOf(listOf(text("ghost"))), select("SELECT dept_code FROM employees").rows) + } + + @Test + fun dropColumnRestrictsWhenUsedByItsOwnForeignKey() { + run("CREATE TABLE departments (id INTEGER PRIMARY KEY)") + run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER REFERENCES departments(id))") + val error = assertFailsWith { run("ALTER TABLE employees DROP COLUMN dept_id") } + assertTrue(error.message!!.contains("FOREIGN KEY")) + } + + @Test + fun dropColumnCascadeRemovesItsOwnForeignKey() { + run("CREATE TABLE departments (id INTEGER PRIMARY KEY)") + run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER REFERENCES departments(id))") + run("ALTER TABLE employees DROP COLUMN dept_id CASCADE") + assertEquals(listOf("id"), select("SELECT * FROM employees").columns) + } + + @Test + fun dropColumnRestrictsWhenUsedByAUniqueConstraint() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT UNIQUE)") + val error = assertFailsWith { run("ALTER TABLE t DROP COLUMN email") } + assertTrue(error.message!!.contains("UNIQUE")) + } + + @Test + fun dropColumnCascadeRemovesTheUniqueConstraint() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT UNIQUE)") + run("INSERT INTO t VALUES (1, 'a@x.com')") + run("ALTER TABLE t DROP COLUMN email CASCADE") + run("INSERT INTO t VALUES (2)") + assertEquals(2, select("SELECT * FROM t").rows.size) + } + + @Test + fun dropColumnRestrictsWhenUsedByACheckConstraint() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > 0))") + val error = assertFailsWith { run("ALTER TABLE t DROP COLUMN age") } + assertTrue(error.message!!.contains("CHECK")) + } + + @Test + fun dropColumnCascadeRemovesTheCheckConstraint() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > 0))") + run("ALTER TABLE t DROP COLUMN age CASCADE") + run("INSERT INTO t VALUES (1)") + assertEquals(1, select("SELECT * FROM t").rows.size) + } + + @Test + fun dropColumnRestrictsWhenUsedByAnIndex() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER)") + run("CREATE INDEX idx_age ON t (age)") + val error = assertFailsWith { run("ALTER TABLE t DROP COLUMN age") } + assertTrue(error.message!!.contains("idx_age")) + } + + @Test + fun dropColumnCascadeRemovesTheIndex() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER)") + run("CREATE INDEX idx_age ON t (age)") + run("ALTER TABLE t DROP COLUMN age CASCADE") + assertTrue(catalog.indexNames.isEmpty()) + } + + // ---- RENAME COLUMN ---- + + @Test + fun renameColumnRenamesSchemaAndPreservesData() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, bio TEXT)") + run("INSERT INTO t VALUES (1, 'mathematician')") + run("ALTER TABLE t RENAME COLUMN bio TO biography") + val result = select("SELECT * FROM t") + assertEquals(listOf("id", "biography"), result.columns) + assertEquals(listOf(listOf(int(1), text("mathematician"))), result.rows) + } + + @Test + fun renameColumnRejectsUnknownColumn() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY)") + assertFailsWith { run("ALTER TABLE t RENAME COLUMN missing TO whatever") } + } + + @Test + fun renameColumnRejectsWhenNewNameAlreadyExists() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)") + val error = assertFailsWith { run("ALTER TABLE t RENAME COLUMN name TO id") } + assertTrue(error.message!!.contains("already exists")) + } + + @Test + fun renameColumnUpdatesCompositePrimaryKeyListing() { + run("CREATE TABLE t (region TEXT, seq INTEGER, PRIMARY KEY (region, seq))") + run("INSERT INTO t VALUES ('eu', 1)") + run("ALTER TABLE t RENAME COLUMN region TO area") + assertFailsWith { run("INSERT INTO t VALUES ('eu', 1)") } + run("INSERT INTO t VALUES ('eu', 2)") + assertEquals(2, select("SELECT * FROM t").rows.size) + } + + @Test + fun renameColumnUpdatesItsOwnForeignKeyColumns() { + run("CREATE TABLE departments (id INTEGER PRIMARY KEY)") + run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER REFERENCES departments(id))") + run("INSERT INTO departments VALUES (1)") + run("ALTER TABLE employees RENAME COLUMN dept_id TO department_id") + assertFailsWith { run("INSERT INTO employees VALUES (1, 99)") } + run("INSERT INTO employees VALUES (1, 1)") + assertEquals(1, select("SELECT * FROM employees").rows.size) + } + + @Test + fun renameColumnPropagatesToOtherTablesForeignKeyReferencedColumns() { + run("CREATE TABLE departments (id INTEGER PRIMARY KEY)") + run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER REFERENCES departments(id) ON DELETE CASCADE)") + run("INSERT INTO departments VALUES (1)") + run("INSERT INTO employees VALUES (1, 1)") + run("ALTER TABLE departments RENAME COLUMN id TO dept_pk") + run("DELETE FROM departments WHERE dept_pk = 1") + assertTrue(select("SELECT * FROM employees").rows.isEmpty(), "cascade must still work through the renamed referenced column") + } + + @Test + fun renameColumnUpdatesAUniqueConstraint() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT UNIQUE)") + run("INSERT INTO t VALUES (1, 'a@x.com')") + run("ALTER TABLE t RENAME COLUMN email TO contact_email") + assertFailsWith { run("INSERT INTO t VALUES (2, 'a@x.com')") } + } + + @Test + fun renameColumnUpdatesACheckConstraintAndStillEnforcesIt() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > 0))") + run("ALTER TABLE t RENAME COLUMN age TO years") + assertFailsWith { run("INSERT INTO t VALUES (1, -5)") } + run("INSERT INTO t VALUES (1, 30)") + assertEquals(listOf(listOf(int(30))), select("SELECT years FROM t").rows) + } +