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