From fae2af0764ceaa330b8e5c23d74781105dcc9dac Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 14:57:14 +0200 Subject: [PATCH] test(sql): cover DiskExecutorColumnCollationTest and tempFile --- .../sql/DiskExecutorColumnCollationTest.kt | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 src/test/kotlin/kestrel/store/sql/DiskExecutorColumnCollationTest.kt diff --git a/src/test/kotlin/kestrel/store/sql/DiskExecutorColumnCollationTest.kt b/src/test/kotlin/kestrel/store/sql/DiskExecutorColumnCollationTest.kt new file mode 100644 index 0000000..db19d41 --- /dev/null +++ b/src/test/kotlin/kestrel/store/sql/DiskExecutorColumnCollationTest.kt @@ -0,0 +1,154 @@ +package kestrel.store.sql + +import kestrel.engine.ExplainResult +import kestrel.engine.SelectResult +import kestrel.engine.StatementResult +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 + +/** + * A column's own declared `COLLATE` (see [kestrel.catalog.Column.collation]) against the disk + * engine, mirroring a subset of [kestrel.ColumnCollationTest]'s in-memory scenarios to prove the + * same [kestrel.engine.Evaluator]/[kestrel.engine.ColumnResolver.declaredCollation] machinery + * [DiskExecutor] reuses behaves identically here, plus the one thing that only matters for this + * engine: the declared collation is stored as a byte in [DirectoryEntryCodec]'s own binary schema + * encoding (not this engine's text dump, which does not exist here), so it must survive a real + * file close/reopen, not just staying alive in the same [DiskCatalog] instance. + */ +class DiskExecutorColumnCollationTest { + private fun tempFile(prefix: String): File = Files.createTempFile(prefix, ".db").toFile().also { it.deleteOnExit() } + + private fun bool(value: Boolean) = Value.BooleanValue(value) + private fun int(value: Long) = Value.IntegerValue(value) + + private class Database(val dataFile: File, val walFile: File) { + var catalog = DiskCatalog.open(dataFile, walFile) + var executor = DiskExecutor(catalog) + + fun run(sql: String): List = Parser(Lexer(sql).tokenize()).parseProgram().map { executor.execute(it) } + fun select(sql: String): SelectResult = run(sql).single() as SelectResult + fun explain(sql: String): List = (run(sql).single() as ExplainResult).plan + + fun reopen() { + catalog.close() + catalog = DiskCatalog.open(dataFile, walFile) + executor = DiskExecutor(catalog) + } + } + + private fun newDatabase(): Database = Database(tempFile("kestrel-disk-column-collation-data"), tempFile("kestrel-disk-column-collation-wal")) + + private fun Database.seedNames() { + run("CREATE TABLE people (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE)") + run("INSERT INTO people VALUES (1, 'Bob'), (2, 'bob'), (3, 'Alice'), (4, 'Zoe')") + } + + @Test + fun bareEqualityHonorsTheColumnsDeclaredCollationWithoutAnExplicitClause() { + val db = newDatabase() + db.seedNames() + assertEquals( + listOf(int(1), int(2)), + db.select("SELECT id FROM people WHERE name = 'BOB' ORDER BY id").rows.map { it.single() } + ) + } + + @Test + fun explicitCollateInTheQueryOverridesTheColumnsDeclaredCollation() { + val db = newDatabase() + db.seedNames() + assertEquals(bool(false), db.select("SELECT name COLLATE BINARY = 'BOB' FROM people WHERE id = 1").rows.single().single()) + } + + @Test + fun columnWithNoDeclaredCollationStaysBinaryByDefault() { + val db = newDatabase() + db.run("CREATE TABLE plain (id INTEGER PRIMARY KEY, name TEXT)") + db.run("INSERT INTO plain VALUES (1, 'Bob')") + assertEquals(bool(false), db.select("SELECT name = 'BOB' FROM plain WHERE id = 1").rows.single().single()) + } + + @Test + fun declaredCollationAppliesInsideAnOnConflictDoUpdateAssignment() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE, was_bob BOOLEAN)") + db.run("INSERT INTO t VALUES (1, 'Bob', FALSE)") + db.run("INSERT INTO t (id, name) VALUES (1, 'x') ON CONFLICT (id) DO UPDATE SET was_bob = (name = 'BOB')") + assertEquals(bool(true), db.select("SELECT was_bob FROM t WHERE id = 1").rows.single().single()) + } + + @Test + fun declaredCollationAppliesInAMergeOnCondition() { + val db = newDatabase() + db.run("CREATE TABLE accounts (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE, hits INTEGER)") + db.run("CREATE TABLE staging (id INTEGER PRIMARY KEY, name TEXT)") + db.run("INSERT INTO accounts VALUES (1, 'Bob', 0)") + db.run("INSERT INTO staging VALUES (2, 'BOB')") + db.run("MERGE INTO accounts USING staging ON accounts.name = staging.name WHEN MATCHED THEN UPDATE SET hits = hits + 1") + assertEquals(int(1), db.select("SELECT hits FROM accounts WHERE id = 1").rows.single().single()) + } + + @Test + fun declaredCollationAppliesInATriggerWhenClauseOnNew() { + val db = newDatabase() + db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE)") + db.run("CREATE TABLE hits (id INTEGER PRIMARY KEY, count INTEGER)") + db.run("INSERT INTO t VALUES (1, 'Bob')") + db.run("INSERT INTO hits VALUES (1, 0)") + db.run( + "CREATE TRIGGER bump AFTER UPDATE ON t FOR EACH ROW WHEN NEW.name = 'BOB' " + + "BEGIN UPDATE hits SET count = count + 100 WHERE id = NEW.id; END" + ) + db.run("UPDATE t SET name = 'bob' WHERE id = 1") + assertEquals(int(100), db.select("SELECT count FROM hits WHERE id = 1").rows.single().single()) + } + + @Test + fun declaredCollationAppliesToDefaultOrderBySortOrder() { + val db = newDatabase() + db.seedNames() + val result = db.select("SELECT name FROM people ORDER BY name") + assertEquals(listOf("Alice", "Bob", "bob", "Zoe").map { listOf(Value.TextValue(it)) }, result.rows) + } + + @Test + fun explicitCollateInOrderByOverridesTheColumnsDeclaredCollation() { + val db = newDatabase() + db.seedNames() + val result = db.select("SELECT name FROM people ORDER BY name COLLATE BINARY") + assertEquals(listOf("Alice", "Bob", "Zoe", "bob").map { listOf(Value.TextValue(it)) }, result.rows) + } + + @Test + fun declaredCollationAppliesInAWindowFunctionsOrderBy() { + val db = newDatabase() + db.seedNames() + val result = db.select("SELECT name, ROW_NUMBER() OVER (ORDER BY name) AS rn FROM people ORDER BY rn") + assertEquals( + listOf("Alice" to 1L, "Bob" to 2L, "bob" to 3L, "Zoe" to 4L).map { (name, rn) -> listOf(Value.TextValue(name), int(rn)) }, + result.rows + ) + } + + /** Mirrors [kestrel.ColumnCollationTest]'s set-operation ORDER BY cases against the disk engine. */ + @Test + fun declaredCollationAppliesToASetOperationsOrderByThroughAStarProjection() { + val db = newDatabase() + db.seedNames() + val result = db.select( + "SELECT * FROM people WHERE id <= 2 UNION ALL SELECT * FROM people WHERE id > 2 ORDER BY name" + ) + assertEquals( + listOf(int(3) to "Alice", int(1) to "Bob", int(2) to "bob", int(4) to "Zoe") + .map { (id, name) -> listOf(id, Value.TextValue(name)) }, + result.rows + ) + } +