diff --git a/src/test/kotlin/kestrel/TriggerTest.kt b/src/test/kotlin/kestrel/TriggerTest.kt index 05f1cc9..eb82f94 100644 --- a/src/test/kotlin/kestrel/TriggerTest.kt +++ b/src/test/kotlin/kestrel/TriggerTest.kt @@ -157,3 +157,181 @@ class TriggerTest { assertFailsWith { run("DROP TRIGGER log") } } + @Test + fun droppingTheTableDropsItsTriggersToo() { + run("CREATE TABLE t (id INTEGER)") + run("CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW BEGIN UPDATE t SET id = id; END") + run("DROP TABLE t") + assertFailsWith { run("DROP TRIGGER log") } + } + + @Test + fun renamingTheTableKeepsItsTriggersFiring() { + run("CREATE TABLE t (id INTEGER)") + run("CREATE TABLE audit (id INTEGER)") + run("CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO audit VALUES (NEW.id); END") + run("ALTER TABLE t RENAME TO t2") + run("INSERT INTO t2 VALUES (42)") + assertEquals(listOf(listOf(int(42))), select("SELECT * FROM audit").rows) + } + + @Test + fun rollbackUndoesCreateTrigger() { + run("CREATE TABLE t (id INTEGER)") + run("BEGIN") + run("CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW BEGIN DELETE FROM t WHERE 1 = 0; END") + run("ROLLBACK") + assertFailsWith { run("DROP TRIGGER log") } + } + + @Test + fun createTriggerRejectsUnknownTable() { + assertFailsWith { + run("CREATE TRIGGER log AFTER INSERT ON missing FOR EACH ROW BEGIN DELETE FROM missing WHERE 1 = 0; END") + } + } + + @Test + fun createTriggerRejectsDuplicateName() { + run("CREATE TABLE t (id INTEGER)") + run("CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW BEGIN DELETE FROM t WHERE 1 = 0; END") + assertFailsWith { + run("CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW BEGIN DELETE FROM t WHERE 1 = 0; END") + } + } + + @Test + fun whenClauseRejectsAnUnqualifiedColumn() { + run("CREATE TABLE t (id INTEGER)") + assertFailsWith { + 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() { + run("CREATE TABLE t (id INTEGER)") + assertFailsWith { + 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() { + run("CREATE TABLE t (id INTEGER)") + assertFailsWith { + 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() { + run("CREATE TABLE t (id INTEGER)") + 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() { + run("CREATE TABLE t (id INTEGER)") + assertFailsWith { + 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() { + run("CREATE TABLE t (id INTEGER)") + assertFailsWith { + 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() { + run("CREATE TABLE t (id INTEGER)") + assertFailsWith { + run("CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW BEGIN DELETE FROM missing WHERE 1 = 0; END") + } + } + + @Test + fun onConflictRowThatLandsWithNoConflictFiresInsertTriggers() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, x INTEGER)") + run("CREATE TABLE audit (id INTEGER)") + run("CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO audit VALUES (NEW.id); END") + run("INSERT INTO t VALUES (1, 1) ON CONFLICT (id) DO NOTHING") + assertEquals(listOf(listOf(int(1))), select("SELECT * FROM audit").rows) + } + + @Test + fun onConflictDoNothingFiresNoTriggerAtAllForTheConflictingRow() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, x INTEGER)") + run("INSERT INTO t VALUES (1, 1)") + run("CREATE TABLE audit (id INTEGER)") + run("CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO audit VALUES (NEW.id); END") + run("INSERT INTO t VALUES (1, 99) ON CONFLICT (id) DO NOTHING") + assertEquals(0, select("SELECT * FROM audit").rows.size) + } + + @Test + fun onConflictDoUpdateFiresUpdateTriggersNotInsertTriggers() { + run("CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER)") + run("INSERT INTO accounts VALUES (1, 100)") + run("CREATE TABLE inserts (id INTEGER)") + run("CREATE TABLE updates (id INTEGER, old_balance INTEGER, new_balance INTEGER)") + run("CREATE TRIGGER log_insert AFTER INSERT ON accounts FOR EACH ROW BEGIN INSERT INTO inserts VALUES (NEW.id); END") + run( + "CREATE TRIGGER log_update AFTER UPDATE ON accounts FOR EACH ROW " + + "BEGIN INSERT INTO updates VALUES (NEW.id, OLD.balance, NEW.balance); END" + ) + run("INSERT INTO accounts VALUES (1, 999) ON CONFLICT (id) DO UPDATE SET balance = excluded.balance") + assertEquals(0, select("SELECT * FROM inserts").rows.size) + assertEquals(listOf(listOf(int(1), int(100), int(999))), select("SELECT * FROM updates").rows) + } + + @Test + fun onConflictBeforeUpdateTriggerCanRejectTheWholeStatement() { + run("CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER)") + run("INSERT INTO accounts VALUES (1, 100)") + run("CREATE TABLE guard (x INTEGER NOT NULL)") + run("CREATE TRIGGER reject BEFORE UPDATE ON accounts FOR EACH ROW BEGIN INSERT INTO guard VALUES (NULL); END") + assertFailsWith { + run("INSERT INTO accounts VALUES (1, 999) ON CONFLICT (id) DO UPDATE SET balance = excluded.balance") + } + assertEquals(int(100), select("SELECT balance FROM accounts WHERE id = 1").rows[0][0]) + } + + @Test + fun onConflictMultiRowStatementFiresTheRightTriggerPerRow() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, hits INTEGER)") + run("INSERT INTO t VALUES (1, 10)") + run("CREATE TABLE inserts (id INTEGER)") + run("CREATE TABLE updates (id INTEGER)") + run("CREATE TRIGGER log_insert AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO inserts VALUES (NEW.id); END") + run("CREATE TRIGGER log_update AFTER UPDATE ON t FOR EACH ROW BEGIN INSERT INTO updates VALUES (NEW.id); END") + run( + """ + INSERT INTO t VALUES (1, 1), (2, 2) + ON CONFLICT (id) DO UPDATE SET hits = t.hits + excluded.hits + """.trimIndent() + ) + assertEquals(listOf(listOf(int(2))), select("SELECT * FROM inserts").rows) + assertEquals(listOf(listOf(int(1))), select("SELECT * FROM updates").rows) + } + + @Test + fun bodyStatementInAnInsertTriggerCanOnlySeeNewNotOld() { + run("CREATE TABLE t (id INTEGER)") + run("CREATE TABLE audit (id INTEGER)") + run("CREATE TRIGGER log AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO audit VALUES (NEW.id); END") + run("INSERT INTO t VALUES (5)") + assertEquals(listOf(listOf(int(5))), select("SELECT * FROM audit").rows) + // OLD is simply not bound for an INSERT trigger, so an OLD.id reference in the body falls + // through exactly like any other unresolvable column in an INSERT's VALUES list would. + run("CREATE TRIGGER bad AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO audit VALUES (OLD.id); END") + assertFailsWith { run("INSERT INTO t VALUES (6)") } + } +}