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")
|
||||
}
|
||||
|
||||
@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