diff --git a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt index 3ae751a..d25030f 100644 --- a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt +++ b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt @@ -3012,3 +3012,158 @@ class DiskExecutorTest { assertFailsWith { db.run("SELECT * FROM seniors") } } + @Test + fun columnLevelCheckAcceptsSatisfyingRowsAndRejectsViolatingOnes() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > 0))") + assertEquals(InsertResult(1), db.run("INSERT INTO t VALUES (1, 30)").single()) + val error = assertFailsWith { db.run("INSERT INTO t VALUES (2, -5)") } + assertTrue(error.message!!.contains("CHECK")) + } + + @Test + fun checkPassesWhenExpressionIsUnknownBecauseOfNull() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > 0))") + // three-valued logic: NULL > 0 is UNKNOWN, not FALSE, so the row is accepted + assertEquals(InsertResult(1), db.run("INSERT INTO t (id) VALUES (1)").single()) + } + + @Test + fun tableLevelCheckCanSpanMultipleColumns() { + val db = newDatabase() + db.run("CREATE TABLE ranges (id INTEGER PRIMARY KEY, low INTEGER, high INTEGER, CHECK (low <= high))") + assertEquals(InsertResult(1), db.run("INSERT INTO ranges VALUES (1, 1, 10)").single()) + assertFailsWith { db.run("INSERT INTO ranges VALUES (2, 10, 1)") } + } + + @Test + fun multipleCheckConstraintsAllMustBeSatisfied() { + val db = newDatabase() + db.run( + "CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (age >= 0), " + + "score INTEGER CHECK (score BETWEEN 0 AND 100))" + ) + assertEquals(InsertResult(1), db.run("INSERT INTO t VALUES (1, 30, 90)").single()) + assertFailsWith { db.run("INSERT INTO t VALUES (2, 30, 150)") } + assertFailsWith { db.run("INSERT INTO t VALUES (3, -1, 90)") } + } + + @Test + fun checkIsEnforcedOnUpdateToo() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > 0))") + db.run("INSERT INTO t VALUES (1, 30)") + assertFailsWith { db.run("UPDATE t SET age = -1 WHERE id = 1") } + assertEquals(UpdateResult(1), db.run("UPDATE t SET age = 40 WHERE id = 1").single()) + } + + @Test + fun checkRejectsAggregateFunctions() { + val db = newDatabase() + assertFailsWith { + db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, amount INTEGER CHECK (SUM(amount) > 0))") + } + } + + @Test + fun checkRejectsSubqueries() { + val db = newDatabase() + db.run("CREATE TABLE other (id INTEGER PRIMARY KEY)") + assertFailsWith { + db.run("CREATE TABLE t (id INTEGER PRIMARY KEY CHECK (id IN (SELECT id FROM other)))") + } + } + + @Test + fun checkRejectsUnknownColumn() { + val db = newDatabase() + assertFailsWith { + db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, CHECK (nope > 0))") + } + } + + @Test + fun checkCanReferenceStringAndCallExpressions() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT CHECK (LENGTH(name) > 0))") + assertEquals(InsertResult(1), db.run("INSERT INTO t VALUES (1, 'ada')").single()) + assertFailsWith { db.run("INSERT INTO t VALUES (2, '')") } + } + + @Test + fun checkDefinitionSurvivesClosingAndReopeningTheCatalog() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > 0), CHECK (id > 0))") + db.run("INSERT INTO t VALUES (1, 30)") + db.reopen() + + assertEquals(InsertResult(1), db.run("INSERT INTO t VALUES (2, 40)").single()) + assertFailsWith { db.run("INSERT INTO t VALUES (3, -1)") } + assertFailsWith { db.run("UPDATE t SET age = -5 WHERE id = 1") } + + db.reopen() + assertFailsWith { db.run("INSERT INTO t VALUES (4, -1)") } + assertEquals(2, db.select("SELECT * FROM t").rows.size) + } + + private fun Database.seedDepartmentsAndEmployees() { + run("CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT NOT NULL)") + run( + "CREATE TABLE employees (" + + "id INTEGER PRIMARY KEY, name TEXT NOT NULL, dept_id INTEGER REFERENCES departments(id))" + ) + run("INSERT INTO departments VALUES (1, 'eng'), (2, 'ops')") + } + + private fun Database.seedCascadeDepartments() { + run("CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT NOT NULL)") + run( + "CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT NOT NULL, " + + "dept_id INTEGER REFERENCES departments(id) ON DELETE CASCADE ON UPDATE CASCADE)" + ) + run("INSERT INTO departments VALUES (1, 'eng'), (2, 'ops')") + run("INSERT INTO employees VALUES (1, 'ada', 1), (2, 'linus', 1), (3, 'grace', 2)") + } + + private fun Database.seedSetNullDepartments(childNotNull: Boolean) { + run("CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT NOT NULL)") + val childColumn = if (childNotNull) "dept_id INTEGER NOT NULL" else "dept_id INTEGER" + run( + "CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT NOT NULL, " + + "$childColumn REFERENCES departments(id) ON DELETE SET NULL)" + ) + run("INSERT INTO departments VALUES (1, 'eng')") + run("INSERT INTO employees VALUES (1, 'ada', 1)") + } + + @Test + fun insertRejectsForeignKeyViolation() { + val db = newDatabase() + db.seedDepartmentsAndEmployees() + val error = assertFailsWith { db.run("INSERT INTO employees VALUES (1, 'ada', 99)") } + assertTrue(error.message!!.contains("FOREIGN KEY")) + } + + @Test + fun insertAllowsNullForeignKeyValue() { + val db = newDatabase() + db.seedDepartmentsAndEmployees() + assertEquals(InsertResult(1), db.run("INSERT INTO employees VALUES (1, 'ada', NULL)").single()) + } + + @Test + fun insertSucceedsWhenReferencedRowExists() { + val db = newDatabase() + db.seedDepartmentsAndEmployees() + assertEquals(InsertResult(1), db.run("INSERT INTO employees VALUES (1, 'ada', 1)").single()) + } + + @Test + fun updateRejectsForeignKeyViolationOnChildColumn() { + val db = newDatabase() + db.seedDepartmentsAndEmployees() + db.run("INSERT INTO employees VALUES (1, 'ada', 1)") + assertFailsWith { db.run("UPDATE employees SET dept_id = 99 WHERE id = 1") } + } +