test(sql): cover renameTableFollowedByEnoughInsertsToSplitTheIndexPer...
This commit is contained in:
1 parent
aaa90ad4ab
commit
9e4e76b945
1 file changed
+157
@@ -3805,3 +3805,160 @@ class DiskExecutorTest {
|
||||
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)") }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user