test: cover addConstraintCheckSucceedsWhenEveryExistingRowSatisfiesIt

This commit is contained in:
dualfroz committed 2026-09-08 17:00:33 +02:00
1 parent 4216c1c5eb
commit 68e2600e76
1 file changed
+157
+157
View File
@@ -633,3 +633,160 @@ class AlterTableTest {
assertEquals(listOf(listOf(text("pending"))), select("SELECT status FROM t").rows)
}
// ---- ADD CONSTRAINT / DROP CONSTRAINT ----
@Test
fun addConstraintCheckSucceedsWhenEveryExistingRowSatisfiesIt() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER)")
run("INSERT INTO t VALUES (1, 30), (2, 40)")
val result = exec("ALTER TABLE t ADD CONSTRAINT age_positive CHECK (age > 0)")
assertEquals(AlterTableResult("t"), result)
val error = assertFailsWith<KestrelException> { run("INSERT INTO t VALUES (3, -1)") }
assertTrue(error.message!!.contains("age_positive"), error.message!!)
}
@Test
fun addConstraintCheckRejectsWhenAnExistingRowAlreadyViolatesIt() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER)")
run("INSERT INTO t VALUES (1, 30), (2, -5)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t ADD CONSTRAINT age_positive CHECK (age > 0)") }
assertTrue(error.message!!.contains("age_positive"), error.message!!)
// Rejected, so never installed: a fresh negative row must still be free to insert.
run("INSERT INTO t VALUES (3, -1)")
assertEquals(2, select("SELECT id FROM t WHERE age < 0").rows.size)
}
@Test
fun addConstraintCheckRejectsAReferenceToAnUnknownColumn() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t ADD CONSTRAINT c1 CHECK (bogus > 0)") }
assertTrue(error.message!!.contains("bogus"), error.message!!)
}
@Test
fun addConstraintCheckNameRoundTripsThroughSqlWriterDumpAndReplay() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER)")
run("ALTER TABLE t ADD CONSTRAINT age_positive CHECK (age > 0)")
val dumped = SqlWriter.dump(catalog)
assertTrue(dumped.contains("CONSTRAINT age_positive CHECK"), dumped)
val freshCatalog = Catalog()
val freshExecutor = Executor(freshCatalog)
for (statement in Parser(Lexer(dumped).tokenize()).parseProgram()) freshExecutor.execute(statement)
val error = assertFailsWith<KestrelException> {
freshExecutor.execute(Parser(Lexer("INSERT INTO t VALUES (5, -1)").tokenize()).parseProgram().single())
}
assertTrue(error.message!!.contains("age_positive"), error.message!!)
}
@Test
fun addConstraintUniqueSucceedsWhenNoExistingRowsAreDuplicates() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT)")
run("INSERT INTO t VALUES (1, 'a@x.com'), (2, 'b@x.com')")
exec("ALTER TABLE t ADD CONSTRAINT uq_email UNIQUE (email)")
run("INSERT INTO t VALUES (3, 'c@x.com')")
val error = assertFailsWith<KestrelException> { run("INSERT INTO t VALUES (4, 'c@x.com')") }
assertTrue(error.message!!.contains("uq_email"), error.message!!)
}
@Test
fun addConstraintUniqueRejectsWhenExistingRowsAreAlreadyDuplicated() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT)")
run("INSERT INTO t VALUES (1, 'a@x.com'), (2, 'a@x.com')")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t ADD CONSTRAINT uq_email UNIQUE (email)") }
assertTrue(error.message!!.contains("uq_email"), error.message!!)
}
@Test
fun addConstraintUniqueIgnoresNullValuesAmongExistingRows() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT)")
run("INSERT INTO t VALUES (1, NULL), (2, NULL)")
exec("ALTER TABLE t ADD CONSTRAINT uq_email UNIQUE (email)")
run("INSERT INTO t VALUES (3, NULL)")
assertEquals(3, select("SELECT id FROM t").rows.size)
}
@Test
fun addConstraintUniqueRejectsAReferenceToAnUnknownColumn() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t ADD CONSTRAINT uq_bogus UNIQUE (bogus)") }
assertTrue(error.message!!.contains("bogus"), error.message!!)
}
@Test
fun addConstraintForeignKeySucceedsWhenEveryExistingRowHasAMatchOrIsNull() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT)")
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER)")
run("INSERT INTO departments VALUES (1, 'eng')")
run("INSERT INTO employees VALUES (1, 1), (2, NULL)")
exec("ALTER TABLE employees ADD CONSTRAINT fk_dept FOREIGN KEY (dept_id) REFERENCES departments (id)")
val error = assertFailsWith<KestrelException> { run("INSERT INTO employees VALUES (3, 99)") }
assertTrue(error.message!!.contains("FOREIGN KEY constraint failed"), error.message!!)
}
@Test
fun addConstraintForeignKeyRejectsWhenAnExistingRowIsAlreadyOrphaned() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT)")
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER)")
run("INSERT INTO departments VALUES (1, 'eng')")
run("INSERT INTO employees VALUES (1, 1), (2, 99)")
val error = assertFailsWith<KestrelException> {
run("ALTER TABLE employees ADD CONSTRAINT fk_dept FOREIGN KEY (dept_id) REFERENCES departments (id)")
}
assertTrue(error.message!!.contains("FOREIGN KEY constraint failed"), error.message!!)
}
@Test
fun addConstraintForeignKeySelfReferenceValidatesAgainstExistingSiblingRows() {
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, manager_id INTEGER)")
run("INSERT INTO employees VALUES (1, NULL), (2, 1), (3, 99)")
assertFailsWith<KestrelException> {
run("ALTER TABLE employees ADD CONSTRAINT fk_manager FOREIGN KEY (manager_id) REFERENCES employees (id)")
}
run("UPDATE employees SET manager_id = 1 WHERE id = 3")
exec("ALTER TABLE employees ADD CONSTRAINT fk_manager FOREIGN KEY (manager_id) REFERENCES employees (id)")
val error = assertFailsWith<KestrelException> { run("INSERT INTO employees VALUES (4, 100)") }
assertTrue(error.message!!.contains("FOREIGN KEY constraint failed"), error.message!!)
}
@Test
fun addConstraintForeignKeyRejectsWhenReferencedColumnsAreNotEnforcedUnique() {
run("CREATE TABLE departments (id INTEGER, name TEXT)")
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER)")
val error = assertFailsWith<KestrelException> {
run("ALTER TABLE employees ADD CONSTRAINT fk_dept FOREIGN KEY (dept_id) REFERENCES departments (id)")
}
assertTrue(error.message!!.contains("PRIMARY KEY or UNIQUE"), error.message!!)
}
@Test
fun addConstraintForeignKeyRejectsAColumnCountMismatch() {
// Caught by the parser itself (parseInlineForeignKey, shared with CREATE TABLE's own
// table-level FOREIGN KEY clause) before Executor.executeAddForeignKeyConstraint's own
// redundant arity check would even run - same as CREATE TABLE's identical redundancy.
run("CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT)")
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER)")
val error = assertFailsWith<KestrelException> {
run("ALTER TABLE employees ADD CONSTRAINT fk_bad FOREIGN KEY (dept_id) REFERENCES departments (id, name)")
}
assertTrue(error.message!!.contains("column(s)"), error.message!!)
}
@Test
fun addConstraintForeignKeyOnDeleteCascadeAppliesAfterBeingAdded() {
run("CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT)")
run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER)")
run("INSERT INTO departments VALUES (1, 'eng')")
run("INSERT INTO employees VALUES (1, 1), (2, 1)")
exec("ALTER TABLE employees ADD CONSTRAINT fk_dept FOREIGN KEY (dept_id) REFERENCES departments (id) ON DELETE CASCADE")
run("DELETE FROM departments WHERE id = 1")
assertEquals(emptyList(), select("SELECT id FROM employees").rows)
}
@Test
fun addConstraintRejectsANameAlreadyUsedByADifferentKindOfConstraintOnTheSameTable() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER, email TEXT)")
run("ALTER TABLE t ADD CONSTRAINT c1 CHECK (age > 0)")
val error = assertFailsWith<KestrelException> { run("ALTER TABLE t ADD CONSTRAINT c1 UNIQUE (email)") }
assertTrue(error.message!!.contains("c1"), error.message!!)
}