test(sql): cover foreignKeyRequiresReferencedTableToExist

This commit is contained in:
dualfroz committed 2026-09-13 15:12:27 +02:00
1 parent 085763bb52
commit 197e8c974d
1 file changed
+161
@@ -3167,3 +3167,164 @@ class DiskExecutorTest {
assertFailsWith<KestrelException> { db.run("UPDATE employees SET dept_id = 99 WHERE id = 1") }
}
@Test
fun foreignKeyRequiresReferencedTableToExist() {
val db = newDatabase()
assertFailsWith<KestrelException> {
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, x INTEGER REFERENCES missing(id))")
}
}
@Test
fun foreignKeyRequiresReferencedColumnToBeAPrimaryKey() {
val db = newDatabase()
db.run("CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT)")
assertFailsWith<KestrelException> {
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, x TEXT REFERENCES departments(name))")
}
}
@Test
fun tableLevelForeignKeySyntaxIsEquivalentToColumnLevel() {
val db = newDatabase()
db.run("CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT)")
db.run(
"CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER, " +
"FOREIGN KEY (dept_id) REFERENCES departments(id))"
)
db.run("INSERT INTO departments VALUES (1, 'eng')")
assertEquals(InsertResult(1), db.run("INSERT INTO employees VALUES (1, 1)").single())
assertFailsWith<KestrelException> { db.run("INSERT INTO employees VALUES (2, 99)") }
}
@Test
fun deleteIsRestrictedByDefaultWhenChildrenReferenceTheRow() {
val db = newDatabase()
db.seedDepartmentsAndEmployees()
db.run("INSERT INTO employees VALUES (1, 'ada', 1)")
val error = assertFailsWith<KestrelException> { db.run("DELETE FROM departments WHERE id = 1") }
assertTrue(error.message!!.contains("FOREIGN KEY"))
assertEquals(1, db.select("SELECT * FROM departments WHERE id = 1").rows.size)
}
@Test
fun deleteWithoutReferencingChildrenSucceeds() {
val db = newDatabase()
db.seedDepartmentsAndEmployees()
assertEquals(DeleteResult(1), db.run("DELETE FROM departments WHERE id = 2").single())
}
@Test
fun onDeleteCascadeRemovesReferencingChildren() {
val db = newDatabase()
db.seedCascadeDepartments()
assertEquals(DeleteResult(1), db.run("DELETE FROM departments WHERE id = 1").single())
assertEquals(listOf(listOf(text("grace"))), db.select("SELECT name FROM employees ORDER BY id").rows)
}
@Test
fun onUpdateCascadeChangesChildForeignKeyWhenParentPrimaryKeyChanges() {
val db = newDatabase()
db.seedCascadeDepartments()
db.run("UPDATE departments SET id = 10 WHERE id = 1")
val depts = db.select("SELECT dept_id FROM employees ORDER BY id")
assertEquals(listOf(listOf(int(10)), listOf(int(10)), listOf(int(2))), depts.rows)
}
@Test
fun onUpdateRestrictByDefaultBlocksPrimaryKeyChangeWhenChildrenReference() {
val db = newDatabase()
db.seedDepartmentsAndEmployees()
db.run("INSERT INTO employees VALUES (1, 'ada', 1)")
assertFailsWith<KestrelException> { db.run("UPDATE departments SET id = 99 WHERE id = 1") }
}
@Test
fun onDeleteSetNullNullsOutReferencingChildren() {
val db = newDatabase()
db.seedSetNullDepartments(childNotNull = false)
db.run("DELETE FROM departments WHERE id = 1")
assertEquals(listOf(listOf(Value.NullValue)), db.select("SELECT dept_id FROM employees WHERE id = 1").rows)
}
@Test
fun onDeleteSetNullFailsWhenChildColumnIsNotNull() {
val db = newDatabase()
db.seedSetNullDepartments(childNotNull = true)
val error = assertFailsWith<KestrelException> { db.run("DELETE FROM departments WHERE id = 1") }
assertTrue(error.message!!.contains("NOT NULL"))
}
@Test
fun selfReferencingForeignKeyIsAllowedAndCascadesThroughMultipleLevels() {
val db = newDatabase()
db.run(
"CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT NOT NULL, " +
"manager_id INTEGER REFERENCES employees(id) ON DELETE CASCADE)"
)
db.run(
"INSERT INTO employees VALUES " +
"(1, 'ceo', NULL), (2, 'vp', 1), (3, 'manager', 2), (4, 'ic', 3)"
)
assertEquals(DeleteResult(1), db.run("DELETE FROM employees WHERE id = 1").single())
assertEquals(emptyList(), db.select("SELECT id FROM employees").rows)
}
@Test
fun selfReferencingForeignKeyAllowsForwardAndBackwardSiblingReferencesInTheSameInsertBatch() {
val db = newDatabase()
db.run(
"CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT NOT NULL, " +
"manager_id INTEGER REFERENCES employees(id))"
)
// row 1 references row 2 (forward, not yet on disk) and row 3 references row 1 (backward) -
// order-independence within a single multi-row INSERT statement.
assertEquals(
InsertResult(3),
db.run("INSERT INTO employees VALUES (1, 'a', 2), (2, 'b', NULL), (3, 'c', 1)").single()
)
}
@Test
fun dropTableIsRestrictedByDefaultWhenAForeignKeyReferencesIt() {
val db = newDatabase()
db.seedDepartmentsAndEmployees()
val error = assertFailsWith<KestrelException> { db.run("DROP TABLE departments") }
assertTrue(error.message!!.contains("FOREIGN KEY"))
}
@Test
fun dropTableCascadeStripsTheForeignKeyButKeepsTheReferencingTableAndItsRows() {
val db = newDatabase()
db.seedDepartmentsAndEmployees()
db.run("INSERT INTO employees VALUES (1, 'ada', 1)")
db.run("DROP TABLE departments CASCADE")
assertEquals(listOf("employees"), db.catalog.tableNames)
assertEquals(1, db.select("SELECT * FROM employees").rows.size)
// the constraint is gone, so a now-dangling value is accepted
assertEquals(InsertResult(1), db.run("INSERT INTO employees VALUES (2, 'linus', 99)").single())
}
@Test
fun dropTableCascadeStrippedForeignKeySurvivesClosingAndReopeningTheCatalog() {
val db = newDatabase()
db.seedDepartmentsAndEmployees()
db.run("INSERT INTO employees VALUES (1, 'ada', 1)")
db.run("DROP TABLE departments CASCADE")
db.reopen()
assertEquals(listOf("employees"), db.catalog.tableNames)
assertEquals(InsertResult(1), db.run("INSERT INTO employees VALUES (2, 'linus', 99)").single())
}
@Test
fun dropTableCascadeDropsDependentViewsAndStripsForeignKeysInTheSameStatement() {
val db = newDatabase()
db.seedDepartmentsAndEmployees()
db.run("CREATE VIEW deptNames AS SELECT name FROM departments")
db.run("INSERT INTO employees VALUES (1, 'ada', 1)")
db.run("DROP TABLE departments CASCADE")
assertEquals(listOf("employees"), db.catalog.tableNames)
assertTrue(db.catalog.viewNames.isEmpty())
assertEquals(InsertResult(1), db.run("INSERT INTO employees VALUES (2, 'linus', 99)").single())
}