test(sql): cover renameTableFollowedByEnoughInsertsToSplitTheIndexPer...

This commit is contained in:
dualfroz committed 2026-09-13 16:19:30 +02:00
1 parent aaa90ad4ab
commit 9e4e76b945
1 file changed
+157
@@ -3805,3 +3805,160 @@ class DiskExecutorTest {
assertFailsWith<DiskCatalogException> { db.run("SELECT * FROM t") } assertFailsWith<DiskCatalogException> { db.run("SELECT * FROM t") }
} }
/**
* The regression this guards against: [DiskCatalog.renameTable] used to capture the directory
* key a [DiskTable]'s `onRootsChanged` callback persists under *by value* at table-creation
* time. After a rename moved the row to a new map key, any *later* write that actually split a
* B+Tree (not the rename itself, which persists synchronously) would still try to persist under
* the stale, now-removed key and blow up with `NoSuchElementException`. 300 sequential inserts
* reliably force at least one primary-key index split (same row count [DiskCatalogTest] already
* relies on for the same reason).
*/
@Test
fun renameTableFollowedByEnoughInsertsToSplitTheIndexPersistsCorrectlyAfterReopen() {
val db = newDatabase()
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY)")
db.run("INSERT INTO t VALUES (1)")
db.run("ALTER TABLE t RENAME TO renamed")
for (id in 2..300) db.run("INSERT INTO renamed VALUES ($id)")
db.reopen()
assertEquals(300, db.select("SELECT * FROM renamed").rows.size)
for (id in listOf(1, 150, 300)) {
assertEquals(listOf(listOf(int(id.toLong()))), db.select("SELECT id FROM renamed WHERE id = $id").rows)
}
}
/**
* `PREPARE`/`EXECUTE`/`DEALLOCATE` ported to the disk-backed executor - see [DiskExecutor]'s own
* class doc. These mirror [kestrel.PreparedStatementTest] (the in-memory engine's own suite for
* the same feature) case by case, plus two disk-specific ones proving via a real close/reopen of
* the backing files that the registry is genuinely session-scoped rather than durable - the same
* distinction [kestrel.PersistenceTest] draws for the in-memory engine's own text WAL.
*/
@Test
fun prepareReturnsItsNameAndExecuteRunsAParameterizedSelect() {
val db = newDatabase()
db.seedUsers()
assertEquals(
PrepareResult("byId"),
db.run("PREPARE byId AS SELECT name, age FROM users WHERE id = \$1").single()
)
val result = db.select("EXECUTE byId(2)")
assertEquals(listOf("name", "age"), result.columns)
assertEquals(listOf(listOf(text("linus"), int(54))), result.rows)
}
@Test
fun theSamePreparedStatementCanBeExecutedRepeatedlyWithDifferentArguments() {
val db = newDatabase()
db.seedUsers()
db.run("PREPARE byId AS SELECT name FROM users WHERE id = \$1")
assertEquals(listOf(listOf(text("ada"))), db.select("EXECUTE byId(1)").rows)
assertEquals(listOf(listOf(text("grace"))), db.select("EXECUTE byId(3)").rows)
}
@Test
fun executeSubstitutesMultipleParametersPositionally() {
val db = newDatabase()
db.seedUsers()
db.run("PREPARE inRange AS SELECT name FROM users WHERE age >= \$1 AND age <= \$2 ORDER BY age")
assertEquals(
listOf(listOf(text("ada")), listOf(text("linus"))),
db.select("EXECUTE inRange(30, 60)").rows
)
}
@Test
fun prepareCanWrapInsertAndExecuteInsertsARealRow() {
val db = newDatabase()
db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER DEFAULT 0)")
db.run("PREPARE addUser AS INSERT INTO users VALUES (\$1, \$2, \$3)")
assertEquals(InsertResult(1), db.run("EXECUTE addUser(1, 'ada', 36)").single())
assertEquals(listOf(listOf(int(1), text("ada"), int(36))), db.select("SELECT * FROM users").rows)
assertEquals(InsertResult(1), db.run("EXECUTE addUser(2, 'linus', 54)").single())
assertEquals(2, db.select("SELECT * FROM users").rows.size)
}
@Test
fun prepareCanWrapUpdate() {
val db = newDatabase()
db.seedUsers()
db.run("PREPARE birthday AS UPDATE users SET age = age + \$1 WHERE id = \$2")
assertEquals(UpdateResult(1), db.run("EXECUTE birthday(1, 1)").single())
assertEquals(listOf(listOf(int(37))), db.select("SELECT age FROM users WHERE id = 1").rows)
}
@Test
fun prepareCanWrapDelete() {
val db = newDatabase()
db.seedUsers()
db.run("PREPARE removeUser AS DELETE FROM users WHERE id = \$1")
assertEquals(DeleteResult(1), db.run("EXECUTE removeUser(2)").single())
assertEquals(2, db.select("SELECT * FROM users").rows.size)
}
@Test
fun prepareCanWrapMergeAndAParameterReachesTheMatchedUpdateAssignment() {
val db = newDatabase()
db.run("CREATE TABLE accounts (id INTEGER PRIMARY KEY, name TEXT NOT NULL, balance INTEGER)")
db.run("CREATE TABLE staging (id INTEGER PRIMARY KEY, name TEXT NOT NULL, balance INTEGER)")
db.run("INSERT INTO accounts VALUES (1, 'ada', 100)")
db.run("INSERT INTO staging VALUES (1, 'ada', 500)")
db.run(
"PREPARE topUp AS MERGE INTO accounts USING staging ON accounts.id = staging.id " +
"WHEN MATCHED THEN UPDATE SET balance = staging.balance + \$1"
)
db.run("EXECUTE topUp(10)")
assertEquals(listOf(listOf(int(510))), db.select("SELECT balance FROM accounts WHERE id = 1").rows)
// A second EXECUTE with a DIFFERENT argument proves substitution recomputes fresh every call
// rather than baking in whatever value the first call used.
db.run("EXECUTE topUp(20)")
assertEquals(listOf(listOf(int(520))), db.select("SELECT balance FROM accounts WHERE id = 1").rows)
}
@Test
fun executeWithTheWrongNumberOfArgumentsFailsWithAClearMessage() {
val db = newDatabase()
db.seedUsers()
db.run("PREPARE byId AS SELECT name FROM users WHERE id = \$1")
val tooMany = assertFailsWith<DiskExecutorException> { db.run("EXECUTE byId(1, 2)") }
assertTrue(tooMany.message!!.contains("expects 1 parameter"), tooMany.message)
assertFailsWith<DiskExecutorException> { db.run("EXECUTE byId()") }
}
@Test
fun executeOfAnUnknownNameFails() {
val db = newDatabase()
db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)")
assertFailsWith<DiskExecutorException> { db.run("EXECUTE nope(1)") }
}
@Test
fun preparingTheSameNameTwiceFails() {
val db = newDatabase()
db.seedUsers()
db.run("PREPARE byId AS SELECT name FROM users WHERE id = \$1")
assertFailsWith<DiskExecutorException> { db.run("PREPARE byId AS SELECT age FROM users WHERE id = \$1") }
}
@Test
fun deallocateRemovesAPreparedStatementSoExecuteNoLongerFindsIt() {
val db = newDatabase()
db.seedUsers()
db.run("PREPARE byId AS SELECT name FROM users WHERE id = \$1")
assertEquals(DeallocateResult("byId", all = false), db.run("DEALLOCATE byId").single())
assertFailsWith<DiskExecutorException> { db.run("EXECUTE byId(1)") }
}
@Test
fun deallocateAllRemovesEveryPreparedStatement() {
val db = newDatabase()
db.seedUsers()
db.run("PREPARE p1 AS SELECT name FROM users WHERE id = \$1")
db.run("PREPARE p2 AS SELECT age FROM users WHERE id = \$1")
assertEquals(DeallocateResult(null, all = true), db.run("DEALLOCATE ALL").single())
assertFailsWith<DiskExecutorException> { db.run("EXECUTE p1(1)") }
assertFailsWith<DiskExecutorException> { db.run("EXECUTE p2(1)") }
}