From e4195113f124212186f868c8baee3d5e860ec464 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 13 Sep 2026 19:12:00 +0200 Subject: [PATCH] test(sql): cover cascadingTriggerFiresATriggerOnADifferentTable --- .../store/sql/DiskExecutorTriggerTest.kt | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/src/test/kotlin/kestrel/store/sql/DiskExecutorTriggerTest.kt b/src/test/kotlin/kestrel/store/sql/DiskExecutorTriggerTest.kt index a3cd3fc..fe6668e 100644 --- a/src/test/kotlin/kestrel/store/sql/DiskExecutorTriggerTest.kt +++ b/src/test/kotlin/kestrel/store/sql/DiskExecutorTriggerTest.kt @@ -154,3 +154,161 @@ class DiskExecutorTriggerTest { assertEquals(listOf(listOf(text("first")), listOf(text("second"))), db.select("SELECT * FROM audit").rows) } + @Test + fun cascadingTriggerFiresATriggerOnADifferentTable() { + val db = newDatabase() + db.run("CREATE TABLE a (id INTEGER)") + db.run("CREATE TABLE b (id INTEGER)") + db.run("CREATE TABLE c (id INTEGER)") + db.run("CREATE TRIGGER a_to_b AFTER INSERT ON a FOR EACH ROW BEGIN INSERT INTO b VALUES (NEW.id); END") + db.run("CREATE TRIGGER b_to_c AFTER INSERT ON b FOR EACH ROW BEGIN INSERT INTO c VALUES (NEW.id); END") + db.run("INSERT INTO a VALUES (7)") + assertEquals(listOf(listOf(int(7))), db.select("SELECT * FROM b").rows) + assertEquals(listOf(listOf(int(7))), db.select("SELECT * FROM c").rows) + } + + @Test + fun recursiveTriggerHitsTheDepthGuardInsteadOfLoopingForeverAndRollsBackCleanly() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER)") + db.run("CREATE TRIGGER loopy AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO t VALUES (NEW.id + 1); END") + val exception = assertFailsWith { db.run("INSERT INTO t VALUES (1)") } + assertTrue(exception.message!!.contains("recursion", ignoreCase = true), exception.message!!) + assertEquals(0, db.select("SELECT * FROM t").rows.size) + } + + @Test + fun dropTriggerStopsItFromFiringAndCannotBeDroppedTwice() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER)") + db.run("CREATE TABLE audit (id INTEGER)") + db.run("CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO audit VALUES (NEW.id); END") + db.run("DROP TRIGGER log") + db.run("INSERT INTO t VALUES (1)") + assertEquals(0, db.select("SELECT * FROM audit").rows.size) + assertFailsWith { db.run("DROP TRIGGER log") } + } + + @Test + fun droppingTheTableDropsItsTriggersToo() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER)") + db.run("CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW BEGIN UPDATE t SET id = id; END") + db.run("DROP TABLE t") + assertFailsWith { db.run("DROP TRIGGER log") } + } + + @Test + fun renamingTheTableKeepsItsTriggersFiring() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER)") + db.run("CREATE TABLE audit (id INTEGER)") + db.run("CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO audit VALUES (NEW.id); END") + db.run("ALTER TABLE t RENAME TO t2") + db.run("INSERT INTO t2 VALUES (42)") + assertEquals(listOf(listOf(int(42))), db.select("SELECT * FROM audit").rows) + } + + @Test + fun createTriggerRejectsUnknownTable() { + val db = newDatabase() + assertFailsWith { + db.run("CREATE TRIGGER log AFTER INSERT ON missing FOR EACH ROW BEGIN DELETE FROM missing WHERE 1 = 0; END") + } + } + + @Test + fun createTriggerRejectsDuplicateName() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER)") + db.run("CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW BEGIN DELETE FROM t WHERE 1 = 0; END") + assertFailsWith { + db.run("CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW BEGIN DELETE FROM t WHERE 1 = 0; END") + } + } + + @Test + fun whenClauseRejectsAnUnqualifiedColumn() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER)") + assertFailsWith { + db.run("CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW WHEN id > 0 BEGIN DELETE FROM t WHERE 1 = 0; END") + } + } + + @Test + fun whenClauseRejectsNewOnADeleteTrigger() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER)") + assertFailsWith { + db.run("CREATE TRIGGER log BEFORE DELETE ON t FOR EACH ROW WHEN NEW.id > 0 BEGIN DELETE FROM t WHERE 1 = 0; END") + } + } + + @Test + fun whenClauseRejectsOldOnAnInsertTrigger() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER)") + assertFailsWith { + db.run("CREATE TRIGGER log BEFORE INSERT ON t FOR EACH ROW WHEN OLD.id > 0 BEGIN DELETE FROM t WHERE 1 = 0; END") + } + } + + @Test + fun whenClauseAcceptsBothSidesOnAnUpdateTrigger() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER)") + db.run("CREATE TRIGGER log BEFORE UPDATE ON t FOR EACH ROW WHEN NEW.id > OLD.id BEGIN DELETE FROM t WHERE 1 = 0; END") + } + + @Test + fun whenClauseRejectsAnUnknownColumn() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER)") + assertFailsWith { + db.run("CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW WHEN NEW.nope > 0 BEGIN DELETE FROM t WHERE 1 = 0; END") + } + } + + @Test + fun whenClauseRejectsASubquery() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER)") + assertFailsWith { + db.run( + "CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW WHEN EXISTS (SELECT 1 FROM t) " + + "BEGIN DELETE FROM t WHERE 1 = 0; END" + ) + } + } + + @Test + fun bodyStatementRejectsAnUnknownTargetTable() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER)") + assertFailsWith { + db.run("CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW BEGIN DELETE FROM missing WHERE 1 = 0; END") + } + } + + @Test + fun onConflictRowThatLandsWithNoConflictFiresInsertTriggers() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, x INTEGER)") + db.run("CREATE TABLE audit (id INTEGER)") + db.run("CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO audit VALUES (NEW.id); END") + db.run("INSERT INTO t VALUES (1, 1) ON CONFLICT (id) DO NOTHING") + assertEquals(listOf(listOf(int(1))), db.select("SELECT * FROM audit").rows) + } + + @Test + fun onConflictDoNothingFiresNoTriggerAtAllForTheConflictingRow() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, x INTEGER)") + db.run("INSERT INTO t VALUES (1, 1)") + db.run("CREATE TABLE audit (id INTEGER)") + db.run("CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO audit VALUES (NEW.id); END") + db.run("INSERT INTO t VALUES (1, 99) ON CONFLICT (id) DO NOTHING") + assertEquals(0, db.select("SELECT * FROM audit").rows.size) + } +