test: cover explicitGroupByCollateOverridesTheColumnsDeclaredCollation

This commit is contained in:
dualfroz committed 2026-09-08 19:45:14 +02:00
1 parent 1024cd60b0
commit 2a6dad7aee
1 file changed
+157
@@ -160,3 +160,160 @@ class ColumnCollationTest {
assertEquals(listOf("Bob" to 1L, "bob" to 1L).map { (name, count) -> listOf(text(name), int(count)) }, result.rows) assertEquals(listOf("Bob" to 1L, "bob" to 1L).map { (name, count) -> listOf(text(name), int(count)) }, result.rows)
} }
@Test
fun explicitGroupByCollateOverridesTheColumnsDeclaredCollation() {
seedNames()
val result = select("SELECT name COLLATE BINARY, COUNT(*) FROM people GROUP BY name COLLATE BINARY ORDER BY name COLLATE BINARY")
assertEquals(
listOf("Alice" to 1L, "Bob" to 1L, "Zoe" to 1L, "bob" to 1L).map { (name, count) -> listOf(text(name), int(count)) },
result.rows
)
}
@Test
fun selectDistinctDedupsCaseVariantsThroughTheColumnsDeclaredCollation() {
seedNames()
val result = select("SELECT DISTINCT name FROM people ORDER BY name")
assertEquals(listOf("Alice", "Bob", "Zoe").map { listOf(text(it)) }, result.rows)
}
@Test
fun selectDistinctStarDedupsCaseVariantsThroughTheColumnsDeclaredCollation() {
// A single-column table so DISTINCT *'s whole-row key is driven entirely by name's own
// declared NOCASE - with more columns involved, any other differing column (like people's
// own id) would already make every row distinct regardless of collation.
run("CREATE TABLE tags (label TEXT COLLATE NOCASE)")
run("INSERT INTO tags VALUES ('Bob'), ('bob'), ('Alice')")
val result = select("SELECT DISTINCT * FROM tags ORDER BY label")
assertEquals(listOf("Alice", "Bob").map { listOf(text(it)) }, result.rows)
}
@Test
fun selectDistinctWithNoDeclaredCollationStaysBinary() {
run("CREATE TABLE plain (id INTEGER PRIMARY KEY, name TEXT)")
run("INSERT INTO plain VALUES (1, 'Bob'), (2, 'bob')")
val result = select("SELECT DISTINCT name FROM plain ORDER BY name")
assertEquals(listOf("Bob", "bob").map { listOf(text(it)) }, result.rows)
}
@Test
fun selectDistinctOnDedupsCaseVariantsThroughTheColumnsDeclaredCollation() {
run("CREATE TABLE people (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE, hits INTEGER)")
run("INSERT INTO people VALUES (1, 'Bob', 10), (2, 'bob', 20), (3, 'Alice', 5)")
val result = select("SELECT DISTINCT ON (name) name, hits FROM people ORDER BY name, id")
assertEquals(
listOf("Alice" to 5L, "Bob" to 10L).map { (name, hits) -> listOf(text(name), int(hits)) },
result.rows
)
}
@Test
fun countDistinctDedupsCaseVariantsThroughTheColumnsDeclaredCollation() {
seedNames()
assertEquals(int(3), select("SELECT COUNT(DISTINCT name) FROM people").rows.single().single())
}
/**
* `<>` forces [kestrel.engine.SelectRunner]'s nested-loop join path (only a `BinaryExpression`
* `=` condition can hash-join at all), which reuses the same collation-aware
* [kestrel.engine.Evaluator] as `WHERE`. See the `...InAnEqualityHashJoin` tests below for the
* `=` counterpart, which takes the hash-join fast path instead.
*/
@Test
fun joiningOnColumnsWhereOnlyTheLeftSideDeclaresCollationHonorsItInANonEqualityJoinCondition() {
run("CREATE TABLE left_t (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE)")
run("CREATE TABLE right_t (id INTEGER PRIMARY KEY, name TEXT)")
run("INSERT INTO left_t VALUES (1, 'Bob'), (2, 'Zoe')")
run("INSERT INTO right_t VALUES (1, 'BOB')")
assertEquals(
listOf(listOf(int(2))),
select("SELECT left_t.id FROM left_t JOIN right_t ON left_t.name <> right_t.name").rows
)
}
@Test
fun joiningOnColumnsWhereOnlyTheRightSideDeclaresCollationFallsBackToTheRightWhenTheLeftHasNone() {
run("CREATE TABLE left_t (id INTEGER PRIMARY KEY, name TEXT)")
run("CREATE TABLE right_t (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE)")
run("INSERT INTO left_t VALUES (1, 'Bob'), (2, 'Zoe')")
run("INSERT INTO right_t VALUES (1, 'BOB')")
assertEquals(
listOf(listOf(int(2))),
select("SELECT left_t.id FROM left_t JOIN right_t ON left_t.name <> right_t.name").rows
)
}
/**
* `=` takes [kestrel.engine.SelectRunner]'s hash-join fast path (`hashJoinKeys`/`executeJoin`),
* which must bucket [Value.TextValue] keys through the exact same [kestrel.engine.pairCollation]
* the nested-loop `<>` tests above resolve through [kestrel.engine.Evaluator] - otherwise the
* "optimization" would silently change which rows match. `right_t`'s undeclared `name` still
* only matches its own literal case ('bob'), so this also proves the fast path is not simply
* ignoring case altogether.
*/
@Test
fun joiningOnColumnsWhereOnlyTheLeftSideDeclaresCollationHonorsItInAnEqualityHashJoin() {
run("CREATE TABLE left_t (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE)")
run("CREATE TABLE right_t (id INTEGER PRIMARY KEY, name TEXT)")
run("INSERT INTO left_t VALUES (1, 'Bob'), (2, 'Zoe')")
run("INSERT INTO right_t VALUES (1, 'BOB'), (2, 'bob')")
assertEquals(
listOf(listOf(int(1), int(1)), listOf(int(1), int(2))),
select("SELECT left_t.id, right_t.id FROM left_t JOIN right_t ON left_t.name = right_t.name ORDER BY right_t.id").rows
)
}
@Test
fun joiningOnColumnsWhereOnlyTheRightSideDeclaresCollationHonorsItInAnEqualityHashJoin() {
run("CREATE TABLE left_t (id INTEGER PRIMARY KEY, name TEXT)")
run("CREATE TABLE right_t (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE)")
run("INSERT INTO left_t VALUES (1, 'BOB'), (2, 'Zoe')")
run("INSERT INTO right_t VALUES (1, 'Bob')")
assertEquals(
listOf(listOf(int(1))),
select("SELECT left_t.id FROM left_t JOIN right_t ON left_t.name = right_t.name").rows
)
}
@Test
fun explicitCollateOnAnEqualityHashJoinConditionOverridesTheColumnsDeclaredCollation() {
run("CREATE TABLE left_t (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE)")
run("CREATE TABLE right_t (id INTEGER PRIMARY KEY, name TEXT)")
run("INSERT INTO left_t VALUES (1, 'Bob')")
run("INSERT INTO right_t VALUES (1, 'BOB')")
assertEquals(
emptyList(),
select("SELECT left_t.id FROM left_t JOIN right_t ON left_t.name COLLATE BINARY = right_t.name").rows
)
}
@Test
fun equalityHashJoinWithNoDeclaredCollationOnEitherSideStaysBinary() {
run("CREATE TABLE left_t (id INTEGER PRIMARY KEY, name TEXT)")
run("CREATE TABLE right_t (id INTEGER PRIMARY KEY, name TEXT)")
run("INSERT INTO left_t VALUES (1, 'Bob')")
run("INSERT INTO right_t VALUES (1, 'BOB')")
assertEquals(
emptyList(),
select("SELECT left_t.id FROM left_t JOIN right_t ON left_t.name = right_t.name").rows
)
}
@Test
fun declaredCollationAppliesInsideAnOnConflictDoUpdateAssignment() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE, was_bob BOOLEAN)")
run("INSERT INTO t VALUES (1, 'Bob', FALSE)")
run("INSERT INTO t (id, name) VALUES (1, 'x') ON CONFLICT (id) DO UPDATE SET was_bob = (name = 'BOB')")
assertEquals(bool(true), select("SELECT was_bob FROM t WHERE id = 1").rows.single().single())
}
@Test
fun declaredCollationAppliesInAMergeOnCondition() {
run("CREATE TABLE accounts (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE, hits INTEGER)")
run("CREATE TABLE staging (id INTEGER PRIMARY KEY, name TEXT)")
run("INSERT INTO accounts VALUES (1, 'Bob', 0)")
run("INSERT INTO staging VALUES (2, 'BOB')")
run("MERGE INTO accounts USING staging ON accounts.name = staging.name WHEN MATCHED THEN UPDATE SET hits = hits + 1")
assertEquals(int(1), select("SELECT hits FROM accounts WHERE id = 1").rows.single().single())
}