test(sql): cover DiskExecutorSavepointTest and tempFile
This commit is contained in:
1 parent
4023f5c64d
commit
97944a7894
1 file changed
+161
@@ -0,0 +1,161 @@
|
||||
package kestrel.store.sql
|
||||
|
||||
import kestrel.KestrelException
|
||||
import kestrel.engine.BeginResult
|
||||
import kestrel.engine.CommitResult
|
||||
import kestrel.engine.ReleaseSavepointResult
|
||||
import kestrel.engine.RollbackResult
|
||||
import kestrel.engine.RollbackToSavepointResult
|
||||
import kestrel.engine.SavepointResult
|
||||
import kestrel.engine.SelectResult
|
||||
import kestrel.engine.StatementResult
|
||||
import kestrel.sql.lexer.Lexer
|
||||
import kestrel.sql.parser.Parser
|
||||
import kestrel.storage.Value
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
/**
|
||||
* Exercises `SAVEPOINT`/`RELEASE SAVEPOINT`/`ROLLBACK TO SAVEPOINT` through [DiskExecutor] against a
|
||||
* real [DiskCatalog] backed by temp files - see [DiskExecutorTest] for the general end-to-end pattern
|
||||
* this reuses. Most scenarios here are ported 1:1 from [kestrel.SavepointTest] (the in-memory engine's
|
||||
* equivalent), since the stack semantics ([DiskExecutor.indexOfLastSavepoint] et al.) are identical -
|
||||
* the exception is `rollbackToSavepointUndoesCreateTable`, which does not translate: DDL is rejected
|
||||
* inside an explicit transaction on this executor entirely (see [DiskExecutorTest]), so there is
|
||||
* nothing for a savepoint to roll back there. The tests unique to this file
|
||||
* ([fullRollbackAfterASavepointRollbackUndoesEverythingIncludingThePreSavepointWrite],
|
||||
* [rollbackToSavepointSurvivesACrashBeforeTheTransactionResolves],
|
||||
* [dataWrittenAfterASavepointRollbackSurvivesAReopenAfterCommit]) cover what is actually new on the
|
||||
* disk-backed path: unlike [kestrel.engine.Executor]'s whole-catalog snapshot/restore, a savepoint here
|
||||
* partially rolls back the storage engine's own WAL transaction via ARIES-style compensation log
|
||||
* records (see [kestrel.store.wal.LogRecord.CompensationUpdate]), so only an end-to-end reopen (or
|
||||
* simulated crash) test proves the result is actually durable and not just correct in the live
|
||||
* process's buffer pool cache.
|
||||
*/
|
||||
class DiskExecutorSavepointTest {
|
||||
private fun tempFile(prefix: String): File = Files.createTempFile(prefix, ".db").toFile().also { it.deleteOnExit() }
|
||||
|
||||
private fun int(value: Long) = Value.IntegerValue(value)
|
||||
|
||||
private class Database(val dataFile: File, val walFile: File) {
|
||||
var catalog = DiskCatalog.open(dataFile, walFile)
|
||||
private set
|
||||
var executor = DiskExecutor(catalog)
|
||||
private set
|
||||
|
||||
fun run(sql: String): List<StatementResult> = Parser(Lexer(sql).tokenize()).parseProgram().map { executor.execute(it) }
|
||||
fun select(sql: String): SelectResult = run(sql).single() as SelectResult
|
||||
fun rowCount(sql: String): Int = select(sql).rows.size
|
||||
|
||||
/**
|
||||
* Closes and reopens the catalog against the same files, simulating a fresh process. Called
|
||||
* with an explicit transaction still open (no preceding COMMIT/ROLLBACK), this simulates a
|
||||
* crash: the storage engine's own recovery pass on [DiskCatalog.open] is what must resolve it.
|
||||
*/
|
||||
fun reopen() {
|
||||
catalog.close()
|
||||
catalog = DiskCatalog.open(dataFile, walFile)
|
||||
executor = DiskExecutor(catalog)
|
||||
}
|
||||
}
|
||||
|
||||
private fun newDatabase(): Database = Database(tempFile("kestrel-disk-savepoint-data"), tempFile("kestrel-disk-savepoint-wal"))
|
||||
|
||||
private fun Database.seed() {
|
||||
run("CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER)")
|
||||
run("INSERT INTO accounts VALUES (1, 100), (2, 200)")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rollbackToSavepointUndoesOnlyTheInsertsAfterIt() {
|
||||
val db = newDatabase()
|
||||
db.seed()
|
||||
db.run("BEGIN")
|
||||
db.run("INSERT INTO accounts VALUES (3, 300)")
|
||||
assertEquals(SavepointResult("beforeFour"), db.run("SAVEPOINT beforeFour").single())
|
||||
db.run("INSERT INTO accounts VALUES (4, 400)")
|
||||
assertEquals(4, db.rowCount("SELECT * FROM accounts"))
|
||||
assertEquals(RollbackToSavepointResult("beforeFour"), db.run("ROLLBACK TO SAVEPOINT beforeFour").single())
|
||||
assertEquals(3, db.rowCount("SELECT * FROM accounts"))
|
||||
assertEquals(CommitResult, db.run("COMMIT").single())
|
||||
assertEquals(3, db.rowCount("SELECT * FROM accounts"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rollbackToSavepointUndoesUpdatesAndDeletes() {
|
||||
val db = newDatabase()
|
||||
db.seed()
|
||||
db.run("BEGIN")
|
||||
db.run("SAVEPOINT s1")
|
||||
db.run("UPDATE accounts SET balance = 0 WHERE id = 1")
|
||||
db.run("DELETE FROM accounts WHERE id = 2")
|
||||
assertEquals(int(0), db.select("SELECT balance FROM accounts WHERE id = 1").rows[0][0])
|
||||
db.run("ROLLBACK TO SAVEPOINT s1")
|
||||
assertEquals(int(100), db.select("SELECT balance FROM accounts WHERE id = 1").rows[0][0])
|
||||
assertEquals(2, db.rowCount("SELECT * FROM accounts"))
|
||||
db.run("ROLLBACK")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theSavepointItselfStaysUsableAfterRollingBackToIt() {
|
||||
val db = newDatabase()
|
||||
db.seed()
|
||||
db.run("BEGIN")
|
||||
db.run("SAVEPOINT s1")
|
||||
db.run("INSERT INTO accounts VALUES (3, 300)")
|
||||
db.run("ROLLBACK TO SAVEPOINT s1")
|
||||
db.run("INSERT INTO accounts VALUES (4, 400)")
|
||||
assertEquals(3, db.rowCount("SELECT * FROM accounts"))
|
||||
db.run("ROLLBACK TO SAVEPOINT s1")
|
||||
assertEquals(2, db.rowCount("SELECT * FROM accounts"))
|
||||
db.run("COMMIT")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun releaseSavepointForgetsItWithoutUndoingAnything() {
|
||||
val db = newDatabase()
|
||||
db.seed()
|
||||
db.run("BEGIN")
|
||||
db.run("SAVEPOINT s1")
|
||||
db.run("INSERT INTO accounts VALUES (3, 300)")
|
||||
assertEquals(ReleaseSavepointResult("s1"), db.run("RELEASE SAVEPOINT s1").single())
|
||||
assertEquals(3, db.rowCount("SELECT * FROM accounts"))
|
||||
assertFailsWith<KestrelException> { db.run("ROLLBACK TO SAVEPOINT s1") }
|
||||
db.run("COMMIT")
|
||||
assertEquals(3, db.rowCount("SELECT * FROM accounts"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun releaseSavepointAlsoForgetsLaterNestedSavepoints() {
|
||||
val db = newDatabase()
|
||||
db.seed()
|
||||
db.run("BEGIN")
|
||||
db.run("SAVEPOINT spOuter")
|
||||
db.run("INSERT INTO accounts VALUES (3, 300)")
|
||||
db.run("SAVEPOINT spInner")
|
||||
db.run("INSERT INTO accounts VALUES (4, 400)")
|
||||
db.run("RELEASE SAVEPOINT spOuter")
|
||||
assertFailsWith<KestrelException> { db.run("ROLLBACK TO SAVEPOINT spInner") }
|
||||
assertEquals(4, db.rowCount("SELECT * FROM accounts"))
|
||||
db.run("COMMIT")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reusingASavepointNameShadowsTheOlderOneUntilItIsConsumed() {
|
||||
val db = newDatabase()
|
||||
db.seed()
|
||||
db.run("BEGIN")
|
||||
db.run("SAVEPOINT s")
|
||||
db.run("INSERT INTO accounts VALUES (3, 300)")
|
||||
db.run("SAVEPOINT s")
|
||||
db.run("INSERT INTO accounts VALUES (4, 400)")
|
||||
db.run("ROLLBACK TO SAVEPOINT s")
|
||||
assertEquals(3, db.rowCount("SELECT * FROM accounts"))
|
||||
db.run("ROLLBACK TO SAVEPOINT s")
|
||||
assertEquals(3, db.rowCount("SELECT * FROM accounts"))
|
||||
db.run("COMMIT")
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user