test(sql): cover rollbackRestoresIndexStateEvenAcrossASavepoint
This commit is contained in:
1 parent
97944a7894
commit
e0a11b123c
1 file changed
+171
@@ -159,3 +159,174 @@ class DiskExecutorSavepointTest {
|
|||||||
db.run("COMMIT")
|
db.run("COMMIT")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun rollbackRestoresIndexStateEvenAcrossASavepoint() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
db.run("CREATE INDEX idxBalance ON accounts (balance)")
|
||||||
|
db.run("BEGIN")
|
||||||
|
db.run("SAVEPOINT s1")
|
||||||
|
db.run("INSERT INTO accounts VALUES (3, 100)")
|
||||||
|
assertEquals(2, db.rowCount("SELECT * FROM accounts WHERE balance = 100"))
|
||||||
|
db.run("ROLLBACK TO SAVEPOINT s1")
|
||||||
|
assertEquals(1, db.rowCount("SELECT * FROM accounts WHERE balance = 100"))
|
||||||
|
db.run("ROLLBACK")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun savepointOutsideATransactionIsRejected() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
assertFailsWith<KestrelException> { db.run("SAVEPOINT s1") }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun releaseSavepointOutsideATransactionIsRejected() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
assertFailsWith<KestrelException> { db.run("RELEASE SAVEPOINT s1") }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun rollbackToSavepointOutsideATransactionIsRejected() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
assertFailsWith<KestrelException> { db.run("ROLLBACK TO SAVEPOINT s1") }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun rollbackToAnUnknownSavepointNameIsRejected() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
db.run("BEGIN")
|
||||||
|
db.run("SAVEPOINT s1")
|
||||||
|
assertFailsWith<KestrelException> { db.run("ROLLBACK TO SAVEPOINT nope") }
|
||||||
|
db.run("ROLLBACK")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun releaseAnUnknownSavepointNameIsRejected() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
db.run("BEGIN")
|
||||||
|
db.run("SAVEPOINT s1")
|
||||||
|
assertFailsWith<KestrelException> { db.run("RELEASE SAVEPOINT nope") }
|
||||||
|
db.run("ROLLBACK")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fullRollbackClearsEveryOutstandingSavepoint() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
db.run("BEGIN")
|
||||||
|
db.run("SAVEPOINT s1")
|
||||||
|
db.run("INSERT INTO accounts VALUES (3, 300)")
|
||||||
|
db.run("ROLLBACK")
|
||||||
|
db.run("BEGIN")
|
||||||
|
assertFailsWith<KestrelException> { db.run("ROLLBACK TO SAVEPOINT s1") }
|
||||||
|
db.run("ROLLBACK")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun commitClearsEveryOutstandingSavepoint() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
db.run("BEGIN")
|
||||||
|
db.run("SAVEPOINT s1")
|
||||||
|
db.run("INSERT INTO accounts VALUES (3, 300)")
|
||||||
|
db.run("COMMIT")
|
||||||
|
db.run("BEGIN")
|
||||||
|
assertFailsWith<KestrelException> { db.run("ROLLBACK TO SAVEPOINT s1") }
|
||||||
|
db.run("ROLLBACK")
|
||||||
|
assertEquals(3, db.rowCount("SELECT * FROM accounts"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun releaseKeywordAloneWithoutSavepointKeywordIsAccepted() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
db.run("BEGIN")
|
||||||
|
db.run("SAVEPOINT s1")
|
||||||
|
db.run("INSERT INTO accounts VALUES (3, 300)")
|
||||||
|
db.run("RELEASE s1")
|
||||||
|
assertEquals(3, db.rowCount("SELECT * FROM accounts"))
|
||||||
|
db.run("COMMIT")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun beginResultTypeIsReturnedForTheEnclosingTransaction() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
assertEquals(BeginResult, db.run("BEGIN").single())
|
||||||
|
db.run("ROLLBACK")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The SQL-level twin of `RecoveryManagerTest.fullRollbackAfterAPartialRollbackDoesNotMisapplyAnAlreadyUndoneBeforeImage` -
|
||||||
|
* proves the whole stack (DiskExecutor -> DiskCatalog -> StorageEngine -> TransactionManager ->
|
||||||
|
* RecoveryManager) wires the compensation-record undo-of-undo correctly end to end, not just at the
|
||||||
|
* page level.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun fullRollbackAfterASavepointRollbackUndoesEverythingIncludingThePreSavepointWrite() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
db.run("BEGIN")
|
||||||
|
db.run("INSERT INTO accounts VALUES (3, 300)")
|
||||||
|
db.run("SAVEPOINT s1")
|
||||||
|
db.run("INSERT INTO accounts VALUES (4, 400)")
|
||||||
|
db.run("ROLLBACK TO SAVEPOINT s1")
|
||||||
|
assertEquals(3, db.rowCount("SELECT * FROM accounts"), "the pre-savepoint insert must still be visible in-session")
|
||||||
|
assertEquals(RollbackResult, db.run("ROLLBACK").single())
|
||||||
|
assertEquals(2, db.rowCount("SELECT * FROM accounts"), "a full rollback must also undo the pre-savepoint insert")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A crash that lands right after `ROLLBACK TO SAVEPOINT` but before the enclosing transaction ever
|
||||||
|
* commits or rolls back must still leave the database as if the whole transaction never happened:
|
||||||
|
* recovery resolves any still-open transaction with a full rollback regardless of what savepoints
|
||||||
|
* it touched along the way. This is the reopen-based proof that
|
||||||
|
* [kestrel.store.wal.RecoveryManager.rollbackTo]'s compensation records are genuinely durable, not
|
||||||
|
* just correct in the live process's buffer pool.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun rollbackToSavepointSurvivesACrashBeforeTheTransactionResolves() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
db.run("BEGIN")
|
||||||
|
db.run("INSERT INTO accounts VALUES (3, 300)")
|
||||||
|
db.run("SAVEPOINT s1")
|
||||||
|
db.run("INSERT INTO accounts VALUES (4, 400)")
|
||||||
|
db.run("ROLLBACK TO SAVEPOINT s1")
|
||||||
|
// No COMMIT, no ROLLBACK: crash right here, mid-transaction.
|
||||||
|
db.reopen()
|
||||||
|
|
||||||
|
assertEquals(2, db.rowCount("SELECT * FROM accounts"), "the entire unresolved transaction must be undone by crash recovery")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The durable-happy-path counterpart to [rollbackToSavepointSurvivesACrashBeforeTheTransactionResolves]:
|
||||||
|
* a transaction that rolls back to a savepoint and then commits must persist exactly the rows that
|
||||||
|
* survived the rollback, including a write made *after* the savepoint rollback - proving the
|
||||||
|
* transaction's WAL chain still threads correctly (new writes point back at the compensation
|
||||||
|
* record, not at the undone update it replaced) once it resumes.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun dataWrittenAfterASavepointRollbackSurvivesAReopenAfterCommit() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
db.run("BEGIN")
|
||||||
|
db.run("INSERT INTO accounts VALUES (3, 300)")
|
||||||
|
db.run("SAVEPOINT s1")
|
||||||
|
db.run("INSERT INTO accounts VALUES (4, 400)")
|
||||||
|
db.run("ROLLBACK TO SAVEPOINT s1")
|
||||||
|
db.run("INSERT INTO accounts VALUES (5, 500)")
|
||||||
|
db.run("COMMIT")
|
||||||
|
db.reopen()
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf(int(1), int(2), int(3), int(5)),
|
||||||
|
db.select("SELECT id FROM accounts ORDER BY id").rows.map { it[0] }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user