test(sql): cover DiskExecutorSavepointTest and tempFile

This commit is contained in:
dualfroz committed 2026-09-12 12:56:59 +02:00
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")
}