From 9e4e76b945365f1f2bc21af762ca53d51f2d4ac5 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 13 Sep 2026 16:19:30 +0200 Subject: [PATCH] test(sql): cover renameTableFollowedByEnoughInsertsToSplitTheIndexPer... --- .../kestrel/store/sql/DiskExecutorTest.kt | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt index 7feeca4..48d16b7 100644 --- a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt +++ b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt @@ -3805,3 +3805,160 @@ class DiskExecutorTest { assertFailsWith { 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 { db.run("EXECUTE byId(1, 2)") } + assertTrue(tooMany.message!!.contains("expects 1 parameter"), tooMany.message) + assertFailsWith { db.run("EXECUTE byId()") } + } + + @Test + fun executeOfAnUnknownNameFails() { + val db = newDatabase() + db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)") + assertFailsWith { 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 { 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 { 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 { db.run("EXECUTE p1(1)") } + assertFailsWith { db.run("EXECUTE p2(1)") } + } +