test(sql): cover onConflictDoUpdateFiresUpdateTriggersNotInsertTriggers
This commit is contained in:
1 parent
e4195113f1
commit
bacc234c9f
1 file changed
+122
@@ -312,3 +312,125 @@ class DiskExecutorTriggerTest {
|
||||
assertEquals(0, db.select("SELECT * FROM audit").rows.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onConflictDoUpdateFiresUpdateTriggersNotInsertTriggers() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER)")
|
||||
db.run("INSERT INTO accounts VALUES (1, 100)")
|
||||
db.run("CREATE TABLE inserts (id INTEGER)")
|
||||
db.run("CREATE TABLE updates (id INTEGER, old_balance INTEGER, new_balance INTEGER)")
|
||||
db.run("CREATE TRIGGER log_insert AFTER INSERT ON accounts FOR EACH ROW BEGIN INSERT INTO inserts VALUES (NEW.id); END")
|
||||
db.run(
|
||||
"CREATE TRIGGER log_update AFTER UPDATE ON accounts FOR EACH ROW " +
|
||||
"BEGIN INSERT INTO updates VALUES (NEW.id, OLD.balance, NEW.balance); END"
|
||||
)
|
||||
db.run("INSERT INTO accounts VALUES (1, 999) ON CONFLICT (id) DO UPDATE SET balance = excluded.balance")
|
||||
assertEquals(0, db.select("SELECT * FROM inserts").rows.size)
|
||||
assertEquals(listOf(listOf(int(1), int(100), int(999))), db.select("SELECT * FROM updates").rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onConflictBeforeUpdateTriggerCanRejectTheWholeStatement() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER)")
|
||||
db.run("INSERT INTO accounts VALUES (1, 100)")
|
||||
db.run("CREATE TABLE guard (x INTEGER NOT NULL)")
|
||||
db.run("CREATE TRIGGER reject BEFORE UPDATE ON accounts FOR EACH ROW BEGIN INSERT INTO guard VALUES (NULL); END")
|
||||
assertFailsWith<KestrelException> {
|
||||
db.run("INSERT INTO accounts VALUES (1, 999) ON CONFLICT (id) DO UPDATE SET balance = excluded.balance")
|
||||
}
|
||||
assertEquals(int(100), db.select("SELECT balance FROM accounts WHERE id = 1").rows[0][0])
|
||||
}
|
||||
|
||||
/** Disk-specific: the trigger cascade and the ON CONFLICT statement itself share one savepoint, so a BEFORE trigger's rejection survives a real close/reopen as if the whole statement never ran. */
|
||||
@Test
|
||||
fun onConflictBeforeUpdateTriggerRejectionSurvivesAReopen() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER)")
|
||||
db.run("INSERT INTO accounts VALUES (1, 100)")
|
||||
db.run("CREATE TABLE guard (x INTEGER NOT NULL)")
|
||||
db.run("CREATE TRIGGER reject BEFORE UPDATE ON accounts FOR EACH ROW BEGIN INSERT INTO guard VALUES (NULL); END")
|
||||
assertFailsWith<KestrelException> {
|
||||
db.run("INSERT INTO accounts VALUES (1, 999) ON CONFLICT (id) DO UPDATE SET balance = excluded.balance")
|
||||
}
|
||||
db.reopen()
|
||||
assertEquals(int(100), db.select("SELECT balance FROM accounts WHERE id = 1").rows[0][0])
|
||||
assertEquals(0, db.select("SELECT * FROM guard").rows.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onConflictMultiRowStatementFiresTheRightTriggerPerRow() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, hits INTEGER)")
|
||||
db.run("INSERT INTO t VALUES (1, 10)")
|
||||
db.run("CREATE TABLE inserts (id INTEGER)")
|
||||
db.run("CREATE TABLE updates (id INTEGER)")
|
||||
db.run("CREATE TRIGGER log_insert AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO inserts VALUES (NEW.id); END")
|
||||
db.run("CREATE TRIGGER log_update AFTER UPDATE ON t FOR EACH ROW BEGIN INSERT INTO updates VALUES (NEW.id); END")
|
||||
db.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))), db.select("SELECT * FROM inserts").rows)
|
||||
assertEquals(listOf(listOf(int(1))), db.select("SELECT * FROM updates").rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bodyStatementInAnInsertTriggerCanOnlySeeNewNotOld() {
|
||||
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("INSERT INTO t VALUES (5)")
|
||||
assertEquals(listOf(listOf(int(5))), db.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.
|
||||
db.run("CREATE TRIGGER bad AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO audit VALUES (OLD.id); END")
|
||||
assertFailsWith<KestrelException> { db.run("INSERT INTO t VALUES (6)") }
|
||||
}
|
||||
|
||||
/**
|
||||
* Disk-specific: proves a fired trigger's writes and the firing statement's own writes share the
|
||||
* user's explicit `BEGIN`, not a savepoint-scoped transaction of their own - a `ROLLBACK` after
|
||||
* both undoes everything, same as [DiskExecutorOnConflictTest.onConflictInsideAnExplicitTransactionParticipatesInItsCommit].
|
||||
*/
|
||||
@Test
|
||||
fun triggerFiringInsideAnExplicitTransactionParticipatesInItsCommit() {
|
||||
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("BEGIN")
|
||||
db.run("INSERT INTO t VALUES (1)")
|
||||
db.run("ROLLBACK")
|
||||
assertEquals(0, db.select("SELECT * FROM t").rows.size)
|
||||
assertEquals(0, db.select("SELECT * FROM audit").rows.size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Durability proof unique to the disk-backed executor: a trigger cascade's rollback-on-failure
|
||||
* (via the internal storage-engine savepoint [DiskExecutor.withTriggerAtomicity] takes, not a
|
||||
* whole-catalog snapshot) must leave nothing behind even after a real close/reopen, and a
|
||||
* trigger's own definition must itself survive to keep firing after reopen - mirrors the reopen
|
||||
* tests already established for ON CONFLICT ([DiskExecutorOnConflictTest.conflictResolutionSurvivesAReopen]).
|
||||
*/
|
||||
@Test
|
||||
fun triggerCascadeAtomicitySurvivesAReopen() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
|
||||
db.run("CREATE TABLE audit (order_id INTEGER NOT NULL)")
|
||||
db.run("CREATE TRIGGER log_fail AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO audit VALUES (NULL); END")
|
||||
assertFailsWith<KestrelException> { db.run("INSERT INTO t VALUES (1)") }
|
||||
db.reopen()
|
||||
assertEquals(0, db.select("SELECT * FROM t").rows.size)
|
||||
assertEquals(0, db.select("SELECT * FROM audit").rows.size)
|
||||
db.run("DROP TRIGGER log_fail")
|
||||
db.run("CREATE TABLE audit2 (id INTEGER)")
|
||||
db.run("CREATE TRIGGER log_ok AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO audit2 VALUES (NEW.id); END")
|
||||
db.reopen()
|
||||
db.run("INSERT INTO t VALUES (9)")
|
||||
assertEquals(listOf(listOf(int(9))), db.select("SELECT * FROM audit2").rows)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user