test(sql): cover aForeignKeyViolationOnAMergeUpdateIsRejected

This commit is contained in:
dualfroz committed 2026-09-11 20:29:26 +02:00
1 parent 972d428c84
commit 4c1ba85a21
1 file changed
+222
@@ -313,3 +313,225 @@ class DiskExecutorMergeTest {
assertEquals(emptyList(), db.select("SELECT * FROM t").rows)
}
@Test
fun aForeignKeyViolationOnAMergeUpdateIsRejected() {
val db = newDatabase()
db.run("CREATE TABLE departments (id INTEGER PRIMARY KEY)")
db.run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER REFERENCES departments(id))")
db.run("CREATE TABLE moves (id INTEGER, new_dept INTEGER)")
db.run("INSERT INTO departments VALUES (1)")
db.run("INSERT INTO employees VALUES (1, 1)")
db.run("INSERT INTO moves VALUES (1, 999)")
val error = assertFailsWith<KestrelException> {
db.run("MERGE INTO employees USING moves ON employees.id = moves.id WHEN MATCHED THEN UPDATE SET dept_id = moves.new_dept")
}
assertTrue(error.message!!.contains("FOREIGN KEY"), error.message)
assertEquals(listOf(listOf(int(1))), db.select("SELECT dept_id FROM employees").rows)
}
@Test
fun rowTriggersFireForEachMergeDrivenAction() {
val db = newDatabase()
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, label TEXT)")
db.run("CREATE TABLE s (id INTEGER, label TEXT)")
db.run("CREATE TABLE log (event TEXT)")
db.run("INSERT INTO t VALUES (1, 'old'), (2, 'gone')")
db.run("INSERT INTO s VALUES (1, 'new'), (2, 'new'), (3, 'fresh')")
db.run("CREATE TRIGGER log_insert AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO log VALUES (CONCAT('insert:', NEW.label)); END")
db.run("CREATE TRIGGER log_update AFTER UPDATE ON t FOR EACH ROW BEGIN INSERT INTO log VALUES (CONCAT('update:', NEW.label)); END")
db.run("CREATE TRIGGER log_delete AFTER DELETE ON t FOR EACH ROW BEGIN INSERT INTO log VALUES (CONCAT('delete:', OLD.label)); END")
db.exec(
"MERGE INTO t USING s ON t.id = s.id " +
"WHEN MATCHED AND s.id = 2 THEN DELETE " +
"WHEN MATCHED THEN UPDATE SET label = s.label " +
"WHEN NOT MATCHED THEN INSERT (id, label) VALUES (s.id, s.label)"
)
assertEquals(
setOf(text("update:new"), text("delete:gone"), text("insert:fresh")),
db.select("SELECT event FROM log").rows.map { it.single() }.toSet()
)
}
@Test
fun rollingBackATransactionUndoesEveryEffectOfAMerge() {
val db = newDatabase()
db.setUpAccountsAndStaging()
db.run("INSERT INTO accounts VALUES (1, 100)")
db.run("INSERT INTO staging VALUES (1, 50), (2, 20)")
db.run("BEGIN")
db.exec(
"MERGE INTO accounts USING staging ON accounts.id = staging.id " +
"WHEN MATCHED THEN UPDATE SET balance = accounts.balance + staging.amount " +
"WHEN NOT MATCHED THEN INSERT (id, balance) VALUES (staging.id, staging.amount)"
)
db.run("ROLLBACK")
assertEquals(listOf(listOf(int(1), int(100))), db.select("SELECT id, balance FROM accounts").rows)
}
@Test
fun anAliasedTargetAndSourceCanBeReferencedByTheirAlias() {
val db = newDatabase()
db.setUpAccountsAndStaging()
db.run("INSERT INTO accounts VALUES (1, 100)")
db.run("INSERT INTO staging VALUES (1, 25)")
db.exec(
"MERGE INTO accounts AS acc USING staging AS s ON acc.id = s.id " +
"WHEN MATCHED THEN UPDATE SET balance = acc.balance + s.amount"
)
assertEquals(listOf(listOf(int(125))), db.select("SELECT balance FROM accounts").rows)
}
@Test
fun mergeRequiresAtLeastOneWhenClause() {
val db = newDatabase()
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
db.run("CREATE TABLE s (id INTEGER)")
assertFailsWith<KestrelException> { db.run("MERGE INTO t USING s ON t.id = s.id") }
}
@Test
fun mergingIntoAnUnknownTableFails() {
val db = newDatabase()
db.run("CREATE TABLE s (id INTEGER)")
assertFailsWith<KestrelException> {
db.run("MERGE INTO nope USING s ON nope.id = s.id WHEN MATCHED THEN DELETE")
}
}
@Test
fun mergeSurvivesClosingAndReopeningTheCatalog() {
val db = newDatabase()
db.setUpAccountsAndStaging()
db.run("INSERT INTO accounts VALUES (1, 100)")
db.run("INSERT INTO staging VALUES (1, 50), (2, 20)")
db.exec(
"MERGE INTO accounts USING staging ON accounts.id = staging.id " +
"WHEN MATCHED THEN UPDATE SET balance = accounts.balance + staging.amount " +
"WHEN NOT MATCHED THEN INSERT (id, balance) VALUES (staging.id, staging.amount)"
)
db.reopen()
assertEquals(
listOf(listOf(int(1), int(150)), listOf(int(2), int(20))),
db.select("SELECT id, balance FROM accounts ORDER BY id").rows
)
}
@Test
fun returningStarProjectsEveryColumnOfEveryActionedRow() {
val db = newDatabase()
db.setUpAccountsAndStaging()
db.run("INSERT INTO accounts VALUES (1, 100)")
db.run("INSERT INTO staging VALUES (1, 10), (2, 5)")
val result = db.exec(
"MERGE INTO accounts USING staging ON accounts.id = staging.id " +
"WHEN MATCHED THEN UPDATE SET balance = accounts.balance + staging.amount " +
"WHEN NOT MATCHED THEN INSERT (id, balance) VALUES (staging.id, staging.amount) " +
"RETURNING *"
) as MergeResult
assertEquals(listOf("id", "balance"), result.returning!!.columns)
assertEquals(
listOf(listOf(int(1), int(110)), listOf(int(2), int(5))),
result.returning!!.rows
)
}
@Test
fun returningEvaluatesExpressionsAgainstTheAffectedRow() {
val db = newDatabase()
db.setUpAccountsAndStaging()
db.run("INSERT INTO accounts VALUES (1, 100)")
db.run("INSERT INTO staging VALUES (1, 10)")
val result = db.exec(
"MERGE INTO accounts USING staging ON accounts.id = staging.id " +
"WHEN MATCHED THEN UPDATE SET balance = accounts.balance + staging.amount " +
"RETURNING balance * 2 AS doubled"
) as MergeResult
assertEquals(listOf("doubled"), result.returning!!.columns)
assertEquals(listOf(listOf(int(220))), result.returning!!.rows)
}
@Test
fun returningIncludesThePreDeletionImageOfADeletedRow() {
val db = newDatabase()
db.setUpAccountsAndStaging()
db.run("INSERT INTO accounts VALUES (1, 100)")
db.run("INSERT INTO staging VALUES (1, 0)")
val result = db.exec(
"MERGE INTO accounts USING staging ON accounts.id = staging.id WHEN MATCHED THEN DELETE RETURNING id, balance"
) as MergeResult
assertEquals(listOf(listOf(int(1), int(100))), result.returning!!.rows)
assertEquals(emptyList(), db.select("SELECT * FROM accounts").rows)
}
@Test
fun mergeActionLabelsEachReturnedRowByWhichClauseFired() {
val db = newDatabase()
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, label TEXT)")
db.run("CREATE TABLE s (id INTEGER, label TEXT)")
db.run("INSERT INTO t VALUES (1, 'old'), (2, 'gone')")
db.run("INSERT INTO s VALUES (1, 'new'), (2, 'new'), (3, 'fresh')")
val result = db.exec(
"MERGE INTO t USING s ON t.id = s.id " +
"WHEN MATCHED AND s.id = 2 THEN DELETE " +
"WHEN MATCHED THEN UPDATE SET label = s.label " +
"WHEN NOT MATCHED THEN INSERT (id, label) VALUES (s.id, s.label) " +
"RETURNING id, MERGE_ACTION() AS kind"
) as MergeResult
assertEquals(
setOf(listOf(int(1), text("UPDATE")), listOf(int(2), text("DELETE")), listOf(int(3), text("INSERT"))),
result.returning!!.rows.toSet()
)
}
@Test
fun noRowMatchingAnyClauseReturnsAnEmptyReturningResultNotANullOne() {
val db = newDatabase()
db.setUpAccountsAndStaging()
db.run("INSERT INTO accounts VALUES (1, 100)")
db.run("INSERT INTO staging VALUES (1, 5)")
val result = db.exec(
"MERGE INTO accounts USING staging ON accounts.id = staging.id " +
"WHEN MATCHED AND staging.amount > 1000 THEN UPDATE SET balance = 0 " +
"RETURNING *"
) as MergeResult
assertEquals(emptyList(), result.returning!!.rows)
}
@Test
fun mergeActionIsRejectedOutsideOfAMergesOwnReturningClause() {
val db = newDatabase()
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
db.run("INSERT INTO t VALUES (1)")
val error = assertFailsWith<KestrelException> { db.select("SELECT MERGE_ACTION() FROM t") }
assertTrue(error.message!!.contains("MERGE_ACTION"), error.message)
}
@Test
fun returningColumnValidationRunsUpFrontEvenWhenNoSourceRowEverReachesAMatchingClause() {
val db = newDatabase()
db.setUpAccountsAndStaging()
val error = assertFailsWith<KestrelException> {
db.run(
"MERGE INTO accounts USING staging ON accounts.id = staging.id " +
"WHEN MATCHED THEN DELETE RETURNING nope"
)
}
assertTrue(error.message!!.contains("no such column"), error.message)
}
@Test
fun returningSurvivesClosingAndReopeningTheCatalog() {
val db = newDatabase()
db.setUpAccountsAndStaging()
db.run("INSERT INTO accounts VALUES (1, 100)")
db.run("INSERT INTO staging VALUES (1, 50)")
val beforeReopen = db.exec(
"MERGE INTO accounts USING staging ON accounts.id = staging.id " +
"WHEN MATCHED THEN UPDATE SET balance = accounts.balance + staging.amount " +
"RETURNING id, balance"
) as MergeResult
assertEquals(listOf(listOf(int(1), int(150))), beforeReopen.returning!!.rows)
db.reopen()
assertEquals(listOf(listOf(int(1), int(150))), db.select("SELECT id, balance FROM accounts").rows)
}
}