test: cover rollingBackATransactionUndoesEveryEffectOfAMerge

This commit is contained in:
dualfroz committed 2026-09-09 14:31:28 +02:00
1 parent 1798f78918
commit e0058b8af2
1 file changed
+147
+147
View File
@@ -310,3 +310,150 @@ class MergeTest {
)
}
@Test
fun rollingBackATransactionUndoesEveryEffectOfAMerge() {
setUpAccountsAndStaging()
run("INSERT INTO accounts VALUES (1, 100)")
run("INSERT INTO staging VALUES (1, 50), (2, 20)")
run("BEGIN")
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)"
)
run("ROLLBACK")
assertEquals(listOf(listOf(int(1), int(100))), select("SELECT id, balance FROM accounts").rows)
}
@Test
fun anAliasedTargetAndSourceCanBeReferencedByTheirAlias() {
setUpAccountsAndStaging()
run("INSERT INTO accounts VALUES (1, 100)")
run("INSERT INTO staging VALUES (1, 25)")
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))), select("SELECT balance FROM accounts").rows)
}
@Test
fun mergeRequiresAtLeastOneWhenClause() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
run("CREATE TABLE s (id INTEGER)")
assertFailsWith<KestrelException> { run("MERGE INTO t USING s ON t.id = s.id") }
}
@Test
fun mergingIntoAnUnknownTableFails() {
run("CREATE TABLE s (id INTEGER)")
assertFailsWith<KestrelException> {
run("MERGE INTO nope USING s ON nope.id = s.id WHEN MATCHED THEN DELETE")
}
}
@Test
fun returningStarProjectsEveryColumnOfEveryActionedRow() {
setUpAccountsAndStaging()
run("INSERT INTO accounts VALUES (1, 100)")
run("INSERT INTO staging VALUES (1, 10), (2, 5)")
val result = 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() {
setUpAccountsAndStaging()
run("INSERT INTO accounts VALUES (1, 100)")
run("INSERT INTO staging VALUES (1, 10)")
val result = 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() {
setUpAccountsAndStaging()
run("INSERT INTO accounts VALUES (1, 100)")
run("INSERT INTO staging VALUES (1, 0)")
val result = 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(), select("SELECT * FROM accounts").rows)
}
@Test
fun mergeActionLabelsEachReturnedRowByWhichClauseFired() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, label TEXT)")
run("CREATE TABLE s (id INTEGER, label TEXT)")
run("INSERT INTO t VALUES (1, 'old'), (2, 'gone')")
run("INSERT INTO s VALUES (1, 'new'), (2, 'new'), (3, 'fresh')")
val result = 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() {
setUpAccountsAndStaging()
run("INSERT INTO accounts VALUES (1, 100)")
run("INSERT INTO staging VALUES (1, 5)")
val result = 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() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
run("INSERT INTO t VALUES (1)")
val error = assertFailsWith<KestrelException> { select("SELECT MERGE_ACTION() FROM t") }
assertTrue(error.message!!.contains("MERGE_ACTION"), error.message)
}
@Test
fun mergeActionInAPlainInsertReturningIsRejectedOnceARowActuallyReturns() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
val error = assertFailsWith<KestrelException> {
run("INSERT INTO t VALUES (1) RETURNING MERGE_ACTION()")
}
assertTrue(error.message!!.contains("MERGE_ACTION"), error.message)
}
@Test
fun returningColumnValidationRunsUpFrontEvenWhenNoSourceRowEverReachesAMatchingClause() {
setUpAccountsAndStaging()
val error = assertFailsWith<KestrelException> {
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)
}
}