test: cover transactions and persistence
This commit is contained in:
1 parent
bf8e197760
commit
eac947c911
2 files changed
+188
No files matched your search
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<StatementResult> =
|
||||||
|
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<KestrelException> { 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<KestrelException> { run("COMMIT") }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun rollbackWithoutBeginIsRejected() {
|
||||||
|
assertFailsWith<KestrelException> { run("ROLLBACK") }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nestedBeginIsRejected() {
|
||||||
|
run("BEGIN")
|
||||||
|
assertFailsWith<KestrelException> { run("BEGIN") }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user