test(sql): cover aForeignKeyViolationOnAMergeUpdateIsRejected
This commit is contained in:
1 parent
972d428c84
commit
4c1ba85a21
1 file changed
+222
@@ -313,3 +313,225 @@ class DiskExecutorMergeTest {
|
|||||||
assertEquals(emptyList(), db.select("SELECT * FROM t").rows)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user