test(sql): cover DiskExecutorColumnCollationTest and tempFile

This commit is contained in:
dualfroz committed 2026-09-11 14:57:14 +02:00
1 parent 7cf0ce3d90
commit fae2af0764
1 file changed
+154
@@ -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<StatementResult> = Parser(Lexer(sql).tokenize()).parseProgram().map { executor.execute(it) }
fun select(sql: String): SelectResult = run(sql).single() as SelectResult
fun explain(sql: String): List<String> = (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
)
}