From eac947c911531a1198bb3e0704bf3d312bf6425a Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 28 Aug 2026 16:26:08 +0200 Subject: [PATCH] test: cover transactions and persistence --- src/test/kotlin/kestrel/PersistenceTest.kt | 92 +++++++++++++++++++++ src/test/kotlin/kestrel/TransactionTest.kt | 96 ++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 src/test/kotlin/kestrel/PersistenceTest.kt create mode 100644 src/test/kotlin/kestrel/TransactionTest.kt diff --git a/src/test/kotlin/kestrel/PersistenceTest.kt b/src/test/kotlin/kestrel/PersistenceTest.kt new file mode 100644 index 0000000..7575dd7 --- /dev/null +++ b/src/test/kotlin/kestrel/PersistenceTest.kt @@ -0,0 +1,92 @@ +package kestrel + +import java.io.File +import java.nio.file.Files +import kestrel.cli.Repl +import kestrel.persistence.Persistence +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertTrue + +class PersistenceTest { + private fun tempDir(): File = Files.createTempDirectory("kestrelData").toFile() + + private fun runScript(dir: File, script: String): String { + val output = StringBuilder() + Repl(output, output, Persistence(dir)).run(script.reader().buffered()) + return output.toString() + } + + @Test + fun dataSurvivesReopenAfterCheckpoint() { + val dir = tempDir() + runScript( + dir, + """ + CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER); + INSERT INTO users VALUES (1, 'ada', 36), (2, 'linus', 54); + CREATE INDEX idxAge ON users (age); + .exit + """.trimIndent() + ) + val reopened = runScript(dir, "SELECT name FROM users ORDER BY id ASC;\n.indexes\n.exit\n") + assertContains(reopened, "ada") + assertContains(reopened, "linus") + assertContains(reopened, "idxAge") + } + + @Test + fun updatesAndDeletesSurviveReopen() { + val dir = tempDir() + runScript( + dir, + """ + CREATE TABLE t (id INTEGER PRIMARY KEY, score INTEGER); + INSERT INTO t VALUES (1, 10), (2, 20), (3, 30); + UPDATE t SET score = 99 WHERE id = 2; + DELETE FROM t WHERE id = 3; + .exit + """.trimIndent() + ) + val reopened = runScript(dir, "SELECT id, score FROM t ORDER BY id ASC;\n.exit\n") + assertContains(reopened, "99") + assertTrue(!reopened.contains("30"), "deleted row must not come back") + } + + @Test + fun committedTransactionSurvivesButRollbackDoesNot() { + val dir = tempDir() + runScript( + dir, + """ + CREATE TABLE t (id INTEGER PRIMARY KEY); + BEGIN; + INSERT INTO t VALUES (1); + COMMIT; + BEGIN; + INSERT INTO t VALUES (2); + ROLLBACK; + .exit + """.trimIndent() + ) + val reopened = runScript(dir, "SELECT id FROM t ORDER BY id ASC;\n.exit\n") + assertContains(reopened, "1") + assertTrue(!reopened.contains("2"), "rolled back insert must not persist") + } + + @Test + fun walReplayRecoversWithoutCheckpoint() { + val dir = tempDir() + val persistence = Persistence(dir) + persistence.appendWal( + listOf( + "CREATE TABLE t (id INTEGER);", + "INSERT INTO t VALUES (1), (2);" + ) + ) + val output = StringBuilder() + Repl(output, output, Persistence(dir)).run("SELECT id FROM t ORDER BY id ASC;\n.exit\n".reader().buffered()) + assertContains(output.toString(), "1") + assertContains(output.toString(), "2") + } +} diff --git a/src/test/kotlin/kestrel/TransactionTest.kt b/src/test/kotlin/kestrel/TransactionTest.kt new file mode 100644 index 0000000..28b0532 --- /dev/null +++ b/src/test/kotlin/kestrel/TransactionTest.kt @@ -0,0 +1,96 @@ +package kestrel + +import kestrel.catalog.Catalog +import kestrel.engine.Executor +import kestrel.engine.SelectResult +import kestrel.engine.StatementResult +import kestrel.sql.lexer.Lexer +import kestrel.sql.parser.Parser +import kestrel.storage.Value +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class TransactionTest { + private val catalog = Catalog() + private val executor = Executor(catalog) + + private fun run(sql: String): List = + Parser(Lexer(sql).tokenize()).parseProgram().map { executor.execute(it) } + + private fun select(sql: String): SelectResult = run(sql).single() as SelectResult + + private fun rowCount(sql: String): Int = select(sql).rows.size + + private fun seed() { + run("CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER)") + run("INSERT INTO accounts VALUES (1, 100), (2, 200)") + } + + @Test + fun rollbackUndoesInsert() { + seed() + run("BEGIN") + run("INSERT INTO accounts VALUES (3, 300)") + assertEquals(3, rowCount("SELECT * FROM accounts")) + run("ROLLBACK") + assertEquals(2, rowCount("SELECT * FROM accounts")) + } + + @Test + fun commitKeepsChanges() { + seed() + run("BEGIN") + run("INSERT INTO accounts VALUES (3, 300)") + run("COMMIT") + assertEquals(3, rowCount("SELECT * FROM accounts")) + } + + @Test + fun rollbackUndoesUpdateAndDelete() { + seed() + run("BEGIN") + run("UPDATE accounts SET balance = 0 WHERE id = 1") + run("DELETE FROM accounts WHERE id = 2") + assertEquals(Value.IntegerValue(0), select("SELECT balance FROM accounts WHERE id = 1").rows[0][0]) + run("ROLLBACK") + assertEquals(Value.IntegerValue(100), select("SELECT balance FROM accounts WHERE id = 1").rows[0][0]) + assertEquals(2, rowCount("SELECT * FROM accounts")) + } + + @Test + fun rollbackUndoesCreateTable() { + seed() + run("BEGIN") + run("CREATE TABLE scratch (a INTEGER)") + run("ROLLBACK") + assertFailsWith { run("SELECT * FROM scratch") } + } + + @Test + fun rollbackRestoresIndexState() { + seed() + run("CREATE INDEX idxBalance ON accounts (balance)") + run("BEGIN") + run("INSERT INTO accounts VALUES (3, 100)") + assertEquals(2, rowCount("SELECT * FROM accounts WHERE balance = 100")) + run("ROLLBACK") + assertEquals(1, rowCount("SELECT * FROM accounts WHERE balance = 100")) + } + + @Test + fun commitWithoutBeginIsRejected() { + assertFailsWith { run("COMMIT") } + } + + @Test + fun rollbackWithoutBeginIsRejected() { + assertFailsWith { run("ROLLBACK") } + } + + @Test + fun nestedBeginIsRejected() { + run("BEGIN") + assertFailsWith { run("BEGIN") } + } +}