test(sql): cover DiskExecutorTruncateTest and tempFile
This commit is contained in:
1 parent
bacc234c9f
commit
dc19ec6444
1 file changed
+201
@@ -0,0 +1,201 @@
|
||||
package kestrel.store.sql
|
||||
|
||||
import kestrel.KestrelException
|
||||
import kestrel.engine.StatementResult
|
||||
import kestrel.engine.TruncateTableResult
|
||||
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
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* `TRUNCATE TABLE` through [DiskExecutor] against a real [DiskCatalog] backed by temp files - see
|
||||
* [DiskExecutorTest] for the general end-to-end pattern this reuses, and [kestrel.TruncateTest] for
|
||||
* the in-memory engine's equivalent scenarios, ported 1:1 wherever they translate.
|
||||
* [restartIdentityIsNotUndoneByARollbackUnlikeTheRowTruncationItself] has no in-memory equivalent -
|
||||
* see [DiskExecutor.executeTruncate]'s own doc for the deliberate asymmetry it demonstrates.
|
||||
*/
|
||||
class DiskExecutorTruncateTest {
|
||||
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 exec(sql: String): StatementResult = run(sql).single()
|
||||
fun select(sql: String): kestrel.engine.SelectResult = exec(sql) as kestrel.engine.SelectResult
|
||||
|
||||
fun reopen() {
|
||||
catalog.close()
|
||||
catalog = DiskCatalog.open(dataFile, walFile)
|
||||
executor = DiskExecutor(catalog)
|
||||
}
|
||||
}
|
||||
|
||||
private fun newDatabase(): Database = Database(tempFile("kestrel-disk-truncate-data"), tempFile("kestrel-disk-truncate-wal"))
|
||||
|
||||
@Test
|
||||
fun truncateRemovesEveryRowButKeepsTheTable() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, label TEXT NOT NULL)")
|
||||
db.run("INSERT INTO t VALUES (1, 'a'), (2, 'b')")
|
||||
val result = db.exec("TRUNCATE TABLE t") as TruncateTableResult
|
||||
assertEquals(listOf("t"), result.tableNames)
|
||||
assertEquals(emptyList(), db.select("SELECT * FROM t").rows)
|
||||
db.run("INSERT INTO t VALUES (1, 'fresh')")
|
||||
assertEquals(listOf(listOf(int(1))), db.select("SELECT id FROM t").rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multipleTablesInOneStatementAreAllTruncated() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE a (id INTEGER PRIMARY KEY)")
|
||||
db.run("CREATE TABLE b (id INTEGER PRIMARY KEY)")
|
||||
db.run("INSERT INTO a VALUES (1)")
|
||||
db.run("INSERT INTO b VALUES (1)")
|
||||
val result = db.exec("TRUNCATE TABLE a, b") as TruncateTableResult
|
||||
assertEquals(setOf("a", "b"), result.tableNames.toSet())
|
||||
assertEquals(emptyList(), db.select("SELECT * FROM a").rows)
|
||||
assertEquals(emptyList(), db.select("SELECT * FROM b").rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun truncatingAnUnknownTableFails() {
|
||||
val db = newDatabase()
|
||||
assertFailsWith<DiskCatalogException> { db.run("TRUNCATE TABLE nope") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun restartIdentityResetsTheOwningSequenceBackToItsStartValue() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE SEQUENCE seq")
|
||||
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY DEFAULT NEXTVAL('seq'), label TEXT)")
|
||||
db.run("INSERT INTO t (label) VALUES ('a'), ('b'), ('c')")
|
||||
db.exec("TRUNCATE TABLE t RESTART IDENTITY")
|
||||
db.run("INSERT INTO t (label) VALUES ('fresh')")
|
||||
assertEquals(listOf(listOf(int(1))), db.select("SELECT id FROM t").rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun continueIdentityIsTheDefaultAndLeavesTheSequencePositionAlone() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE SEQUENCE seq")
|
||||
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY DEFAULT NEXTVAL('seq'), label TEXT)")
|
||||
db.run("INSERT INTO t (label) VALUES ('a'), ('b')")
|
||||
db.exec("TRUNCATE TABLE t")
|
||||
db.run("INSERT INTO t (label) VALUES ('c')")
|
||||
assertEquals(listOf(listOf(int(3))), db.select("SELECT id FROM t").rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun truncateIsRestrictedByDefaultWhenAnotherTableHasAForeignKeyIntoIt() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE departments (id INTEGER PRIMARY KEY)")
|
||||
db.run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER REFERENCES departments(id))")
|
||||
db.run("INSERT INTO departments VALUES (1)")
|
||||
db.run("INSERT INTO employees VALUES (1, 1)")
|
||||
val error = assertFailsWith<DiskCatalogException> { db.run("TRUNCATE TABLE departments") }
|
||||
assertTrue(error.message!!.contains("employees"), error.message)
|
||||
assertEquals(listOf(listOf(int(1))), db.select("SELECT id FROM departments").rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun truncateCascadeAlsoEmptiesEveryTransitiveReferencer() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE departments (id INTEGER PRIMARY KEY)")
|
||||
db.run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept_id INTEGER REFERENCES departments(id))")
|
||||
db.run("CREATE TABLE timesheets (id INTEGER PRIMARY KEY, employee_id INTEGER REFERENCES employees(id))")
|
||||
db.run("INSERT INTO departments VALUES (1)")
|
||||
db.run("INSERT INTO employees VALUES (1, 1)")
|
||||
db.run("INSERT INTO timesheets VALUES (1, 1)")
|
||||
val result = db.exec("TRUNCATE TABLE departments CASCADE") as TruncateTableResult
|
||||
assertEquals(setOf("departments", "employees", "timesheets"), result.tableNames.toSet())
|
||||
assertEquals(emptyList(), db.select("SELECT * FROM timesheets").rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selfReferencingForeignKeyDoesNotBlockTruncatingItsOwnTable() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE employees (id INTEGER PRIMARY KEY, manager_id INTEGER REFERENCES employees(id))")
|
||||
db.run("INSERT INTO employees VALUES (1, NULL), (2, 1)")
|
||||
db.exec("TRUNCATE TABLE employees")
|
||||
assertEquals(emptyList(), db.select("SELECT * FROM employees").rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun truncateNeverFiresRowLevelTriggersUnlikeAWhereLessDelete() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
|
||||
db.run("CREATE TABLE audit (n INTEGER)")
|
||||
db.run("INSERT INTO audit VALUES (0)")
|
||||
db.run("CREATE TRIGGER log_delete AFTER DELETE ON t FOR EACH ROW BEGIN UPDATE audit SET n = n + 1; END")
|
||||
db.run("INSERT INTO t VALUES (1), (2), (3)")
|
||||
db.exec("TRUNCATE TABLE t")
|
||||
assertEquals(listOf(listOf(int(0))), db.select("SELECT n FROM audit").rows)
|
||||
assertEquals(emptyList(), db.select("SELECT * FROM t").rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun truncateSurvivesClosingAndReopeningTheCatalog() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
|
||||
db.run("INSERT INTO t VALUES (1), (2)")
|
||||
db.exec("TRUNCATE TABLE t")
|
||||
db.reopen()
|
||||
assertEquals(emptyList(), db.select("SELECT * FROM t").rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun truncateCanRunInsideAnExplicitTransactionAndSharesItsCommit() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
|
||||
db.run("INSERT INTO t VALUES (1)")
|
||||
db.run("BEGIN")
|
||||
db.exec("TRUNCATE TABLE t")
|
||||
db.run("COMMIT")
|
||||
assertEquals(emptyList(), db.select("SELECT * FROM t").rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rollbackAfterTruncateInsideAnExplicitTransactionRestoresTheRows() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
|
||||
db.run("INSERT INTO t VALUES (1), (2)")
|
||||
db.run("BEGIN")
|
||||
db.exec("TRUNCATE TABLE t")
|
||||
db.run("ROLLBACK")
|
||||
assertEquals(listOf(listOf(int(1)), listOf(int(2))), db.select("SELECT id FROM t ORDER BY id").rows)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlike the in-memory engine (where ALTER SEQUENCE mutates a catalog map entry covered by the
|
||||
* whole-catalog snapshot ROLLBACK restores), [DiskCatalog.alterSequence] always opens and commits
|
||||
* its own storage-engine transaction, independent of the caller's [txn] - the same
|
||||
* already-established non-transactional behavior [DiskCatalog.nextValue] has. So a ROLLBACK here
|
||||
* undoes the row truncation but NOT the sequence restart: the very next NEXTVAL keeps counting
|
||||
* from 1 (post-restart), not from 3 (where it would have been had TRUNCATE never run at all).
|
||||
*/
|
||||
@Test
|
||||
fun restartIdentityIsNotUndoneByARollbackUnlikeTheRowTruncationItself() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE SEQUENCE seq")
|
||||
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY DEFAULT NEXTVAL('seq'), label TEXT)")
|
||||
db.run("INSERT INTO t (label) VALUES ('a'), ('b')")
|
||||
db.run("BEGIN")
|
||||
db.exec("TRUNCATE TABLE t RESTART IDENTITY")
|
||||
db.run("ROLLBACK")
|
||||
assertEquals(listOf(listOf(int(1)), listOf(int(2))), db.select("SELECT id FROM t ORDER BY id").rows)
|
||||
val error = assertFailsWith<KestrelException> { db.run("INSERT INTO t (label) VALUES ('c')") }
|
||||
assertTrue(error.message!!.contains("id"), error.message)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user