test: cover ColumnCollationTest and run

This commit is contained in:
dualfroz committed 2026-09-08 19:43:51 +02:00
1 parent 459530ab7c
commit 1024cd60b0
1 file changed
+162
@@ -0,0 +1,162 @@
package kestrel
import kestrel.catalog.Catalog
import kestrel.engine.ExplainResult
import kestrel.engine.Executor
import kestrel.engine.SelectResult
import kestrel.engine.StatementResult
import kestrel.persistence.SqlWriter
import kestrel.sql.ast.CreateTableStatement
import kestrel.sql.ast.Statement
import kestrel.sql.lexer.Lexer
import kestrel.sql.parser.Parser
import kestrel.storage.Value
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
/**
* `col TYPE COLLATE name` - a column's own declared collating sequence (see
* [kestrel.catalog.Column.collation]), which a *bare* reference to it compares with whenever the
* query itself writes no explicit `COLLATE` - see [kestrel.CollateTest] for that explicit,
* expression-level form, which always overrides this one. Threaded through
* [kestrel.engine.ColumnResolver.declaredCollation] into every resolver built directly over a
* table's own schema: plain `WHERE`/`SELECT`/`HAVING` (via [kestrel.engine.SelectRunner]'s
* per-source lookup, including across a JOIN), `INSERT ... ON CONFLICT DO UPDATE` (EXCLUDED-aware),
* `MERGE`'s `ON`/`WHEN` clauses, and a trigger's `NEW`/`OLD`. [kestrel.store.sql.
* DiskExecutorColumnCollationTest] mirrors the core cases against the disk engine, including a
* real file close/reopen (the declared collation is persisted in [kestrel.store.sql.
* DirectoryEntryCodec], not just this engine's text dump).
*/
class ColumnCollationTest {
private val catalog = Catalog()
private val executor = Executor(catalog)
private fun run(sql: String): List<StatementResult> =
Parser(Lexer(sql).tokenize()).parseProgram().map { executor.execute(it) }
private fun parseOne(sql: String): Statement = Parser(Lexer(sql).tokenize()).parseProgram().single()
private fun select(sql: String): SelectResult = run(sql).single() as SelectResult
private fun explain(sql: String): List<String> = (run(sql).single() as ExplainResult).plan
private fun int(value: Long) = Value.IntegerValue(value)
private fun text(value: String) = Value.TextValue(value)
private fun bool(value: Boolean) = Value.BooleanValue(value)
private fun 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() {
seedNames()
assertEquals(bool(true), select("SELECT name = 'BOB' FROM people WHERE id = 1").rows.single().single())
assertEquals(
listOf(int(1), int(2)),
select("SELECT id FROM people WHERE name = 'BOB' ORDER BY id").rows.map { it.single() }
)
}
@Test
fun explicitCollateInTheQueryOverridesTheColumnsDeclaredCollation() {
seedNames()
assertEquals(bool(false), select("SELECT name COLLATE BINARY = 'BOB' FROM people WHERE id = 1").rows.single().single())
}
@Test
fun explicitCollateOnTheOtherOperandStillOverridesADeclaredCollationOnThisOne() {
seedNames()
// 'BOB' COLLATE BINARY on the right wins over people.name's own declared NOCASE on the left.
assertEquals(bool(false), select("SELECT name = 'BOB' COLLATE BINARY FROM people WHERE id = 1").rows.single().single())
}
@Test
fun columnWithNoDeclaredCollationStaysBinaryByDefault() {
run("CREATE TABLE plain (id INTEGER PRIMARY KEY, name TEXT)")
run("INSERT INTO plain VALUES (1, 'Bob')")
assertEquals(bool(false), select("SELECT name = 'BOB' FROM plain WHERE id = 1").rows.single().single())
}
@Test
fun declaredCollationAppliesInAnInList() {
seedNames()
assertEquals(bool(true), select("SELECT name IN ('BOB', 'other') FROM people WHERE id = 1").rows.single().single())
}
@Test
fun declaredCollationAppliesInBetween() {
run("CREATE TABLE words (id INTEGER PRIMARY KEY, w TEXT COLLATE NOCASE)")
run("INSERT INTO words VALUES (1, 'm')")
assertEquals(bool(true), select("SELECT w BETWEEN 'A' AND 'Z' FROM words").rows.single().single())
}
@Test
fun declaredCollationAppliesToASearchedCaseOperandMatch() {
run("CREATE TABLE words (id INTEGER PRIMARY KEY, w TEXT COLLATE NOCASE)")
run("INSERT INTO words VALUES (1, 'YES')")
assertEquals(
listOf(listOf(text("matched"))),
select("SELECT CASE w WHEN 'yes' THEN 'matched' ELSE 'no' END FROM words").rows
)
}
@Test
fun declaredCollationAppliesToDefaultOrderBySortOrder() {
seedNames()
// No explicit `ORDER BY name COLLATE NOCASE` needed - the column's own declared NOCASE
// already interleaves case. 'Bob' (id 1) and 'bob' (id 2) compare equal under NOCASE, so
// the sort (stable) keeps them in their original scan-order relative position.
val result = select("SELECT name FROM people ORDER BY name")
assertEquals(listOf("Alice", "Bob", "bob", "Zoe").map { listOf(text(it)) }, result.rows)
}
@Test
fun explicitCollateInOrderByOverridesTheColumnsDeclaredCollation() {
seedNames()
val result = select("SELECT name FROM people ORDER BY name COLLATE BINARY")
assertEquals(listOf("Alice", "Bob", "Zoe", "bob").map { listOf(text(it)) }, result.rows)
}
@Test
fun declaredCollationAppliesInAWindowFunctionsOrderBy() {
seedNames()
val result = 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(text(name), int(rn)) },
result.rows
)
}
@Test
fun declaredCollationAppliesInGroupBysOwnOrderBy() {
seedNames()
// GROUP BY itself now also dedups through name's declared NOCASE (closing the scope cut this
// test used to document), so 'Bob' and 'bob' collapse into a single group - the group keeps
// whichever row it saw first in scan order ('Bob', id 1). Only three groups remain, and the
// ordering of those groups (GroupAggregation's own ORDER BY handling) is still under test.
val result = select("SELECT name FROM people GROUP BY name ORDER BY name")
assertEquals(listOf("Alice", "Bob", "Zoe").map { listOf(text(it)) }, result.rows)
}
@Test
fun groupByDedupsCaseVariantsThroughTheColumnsDeclaredCollation() {
seedNames()
val result = select("SELECT name, COUNT(*) FROM people GROUP BY name ORDER BY name")
assertEquals(
listOf("Alice" to 1L, "Bob" to 2L, "Zoe" to 1L).map { (name, count) -> listOf(text(name), int(count)) },
result.rows
)
}
@Test
fun groupByWithNoDeclaredCollationStillDedupsByBinaryEquality() {
run("CREATE TABLE plain (id INTEGER PRIMARY KEY, name TEXT)")
run("INSERT INTO plain VALUES (1, 'Bob'), (2, 'bob')")
val result = select("SELECT name, COUNT(*) FROM plain GROUP BY name ORDER BY name")
assertEquals(listOf("Bob" to 1L, "bob" to 1L).map { (name, count) -> listOf(text(name), int(count)) }, result.rows)
}