test: cover declaredCollationAppliesInATriggerWhenClauseOnNew

This commit is contained in:
dualfroz committed 2026-09-08 19:46:28 +02:00
1 parent 2a6dad7aee
commit b870a00b06
1 file changed
+165
@@ -317,3 +317,168 @@ class ColumnCollationTest {
assertEquals(int(1), select("SELECT hits FROM accounts WHERE id = 1").rows.single().single())
}
@Test
fun declaredCollationAppliesInATriggerWhenClauseOnNew() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE)")
run("CREATE TABLE hits (id INTEGER PRIMARY KEY, count INTEGER)")
run("INSERT INTO t VALUES (1, 'Bob')")
run("INSERT INTO hits VALUES (1, 0)")
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"
)
run("UPDATE t SET name = 'bob' WHERE id = 1")
assertEquals(int(100), select("SELECT count FROM hits WHERE id = 1").rows.single().single())
}
/**
* `UNION`/`INTERSECT`/`EXCEPT`'s own `ORDER BY` ([kestrel.engine.SelectRunner.
* declaredSetOperationCollation]) - the last named gap left by #66 ("multiKeyComparator-owa
* sciezka UNION/INTERSECT/EXCEPT ORDER BY dalej binarna"). A `SELECT *` branch's declared
* collation is traced back to the real table each output column comes from.
*/
@Test
fun declaredCollationAppliesToASetOperationsOrderByThroughAStarProjection() {
seedNames()
val result = select(
"SELECT * FROM people WHERE id <= 2 UNION ALL SELECT * FROM people WHERE id > 2 ORDER BY name"
)
// Combined pre-sort order is (1,Bob),(2,bob),(3,Alice),(4,Zoe) - NOCASE ranks Bob/bob equal,
// so the stable sort keeps Bob (scanned first) ahead of bob.
assertEquals(
listOf(int(3) to "Alice", int(1) to "Bob", int(2) to "bob", int(4) to "Zoe").map { (id, name) -> listOf(id, text(name)) },
result.rows
)
}
@Test
fun declaredCollationAppliesToASetOperationsOrderByThroughAnAliasedProjection() {
seedNames()
val result = select(
"SELECT name AS n FROM people WHERE id <= 2 UNION ALL SELECT name AS n FROM people WHERE id > 2 ORDER BY n"
)
assertEquals(listOf("Alice", "Bob", "bob", "Zoe").map { listOf(text(it)) }, result.rows)
}
@Test
fun explicitCollateInASetOperationsOrderByOverridesTheColumnsDeclaredCollation() {
seedNames()
val result = select(
"SELECT * FROM people WHERE id <= 2 UNION ALL SELECT * FROM people WHERE id > 2 ORDER BY name COLLATE BINARY"
)
assertEquals(listOf("Alice", "Bob", "Zoe", "bob"), result.rows.map { (it[1] as Value.TextValue).value })
}
/**
* The declared collation of an output column joined from a second `FROM` source is found past
* the first source's own column count - proves `declaredSetOperationCollation`'s per-source
* offset walk, not just the single-table case above.
*/
@Test
fun declaredCollationAppliesToASetOperationsOrderByAcrossAJoinedSecondTable() {
run("CREATE TABLE ids (id INTEGER PRIMARY KEY)")
run("CREATE TABLE names (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE)")
run("INSERT INTO ids VALUES (1), (2), (3), (4)")
run("INSERT INTO names VALUES (1, 'Bob'), (2, 'bob'), (3, 'Alice'), (4, 'Zoe')")
val result = select(
"SELECT * FROM ids JOIN names ON ids.id = names.id WHERE ids.id <= 2 " +
"UNION ALL SELECT * FROM ids JOIN names ON ids.id = names.id WHERE ids.id > 2 " +
"ORDER BY name"
)
assertEquals(listOf("Alice", "Bob", "bob", "Zoe"), result.rows.map { (it[2] as Value.TextValue).value })
}
/**
* Still-open scope cut (see [kestrel.engine.SelectRunner.declaredSetOperationCollation]'s own
* doc comment): a set operation's left branch reading through a derived table rather than a
* real table directly reports no declared collation, so its ORDER BY stays binary even though
* the underlying table declares NOCASE - symmetric with the pre-existing view/CTE scope cut for
* a plain SELECT's own ORDER BY.
*/
@Test
fun declaredCollationDoesNotReachThroughADerivedTableInASetOperationsLeftBranch() {
seedNames()
val result = select(
"SELECT * FROM (SELECT * FROM people) sub WHERE id <= 2 " +
"UNION ALL SELECT * FROM (SELECT * FROM people) sub2 WHERE id > 2 " +
"ORDER BY name"
)
assertEquals(listOf("Alice", "Bob", "Zoe", "bob"), result.rows.map { (it[1] as Value.TextValue).value })
}
/**
* `UNION`/`INTERSECT`/`EXCEPT`'s own multiset dedup ([kestrel.engine.SelectRunner.
* runSetOperation]) - the largest piece of the scope-cut #67/#68 left open ("UNION/CTE dedup ...
* dalej uzywaja bajtowej rownosci"). Reuses [kestrel.engine.SelectRunner.declaredSetOperationCollation],
* the same resolver the ORDER BY tests above already exercise, but now folded into the dedup key
* itself rather than only the comparator.
*/
@Test
fun plainUnionDedupsCaseVariantsThroughTheColumnsDeclaredCollation() {
seedNames()
val result = select("SELECT name FROM people WHERE id = 1 UNION SELECT name FROM people WHERE id = 2")
assertEquals(listOf(listOf(text("Bob"))), result.rows)
}
@Test
fun plainUnionWithNoDeclaredCollationKeepsCaseVariantsDistinct() {
run("CREATE TABLE plain2 (id INTEGER PRIMARY KEY, name TEXT)")
run("INSERT INTO plain2 VALUES (1, 'Bob'), (2, 'bob')")
val result = select("SELECT name FROM plain2 WHERE id = 1 UNION SELECT name FROM plain2 WHERE id = 2")
assertEquals(listOf(listOf(text("Bob")), listOf(text("bob"))), result.rows)
}
@Test
fun intersectMatchesCaseVariantsThroughTheColumnsDeclaredCollation() {
seedNames()
val result = select("SELECT name FROM people WHERE id = 1 INTERSECT SELECT name FROM people WHERE id = 2")
assertEquals(listOf(listOf(text("Bob"))), result.rows)
}
@Test
fun exceptExcludesCaseVariantsThroughTheColumnsDeclaredCollation() {
seedNames()
val result = select("SELECT name FROM people WHERE id IN (1, 4) EXCEPT SELECT name FROM people WHERE id = 2")
assertEquals(listOf(listOf(text("Zoe"))), result.rows)
}
/**
* `INTERSECT ALL`/`EXCEPT ALL` preserve duplicate counts through [kestrel.engine.SelectRunner.
* multisetFilter], which grouped its right-hand side by raw `Value` equality before this session
* - proving the count itself (not just presence/absence) is now grouped through the declared
* collation: 3 case-variant 'Bob' rows on the left, 2 on the right, min(3,2)=2 kept.
*/
@Test
fun intersectAllMatchesCaseVariantsThroughTheColumnsDeclaredCollationAndPreservesDuplicateCounts() {
run("CREATE TABLE dup (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE)")
run("INSERT INTO dup VALUES (1, 'Bob'), (2, 'Bob'), (3, 'bob'), (4, 'Zoe')")
val result = select(
"SELECT name FROM dup WHERE id IN (1, 2, 3) INTERSECT ALL SELECT name FROM dup WHERE id IN (2, 3)"
)
assertEquals(listOf(listOf(text("Bob")), listOf(text("Bob"))), result.rows)
}
/**
* A recursive CTE's own fixed-point dedup ([kestrel.engine.SelectRunner.buildCteScope]), folded
* through [kestrel.engine.SelectRunner.declaredRecursiveCteCollation] resolved against the
* anchor (base) term - mirrors [declaredSetOperationCollation] but for the base/recursive-term
* split instead of a set operation's left/right. `steps` chains 'Bob' -> 'Carol' -> 'bob'; without
* collation-aware dedup 'bob' would be treated as a new row (it differs from 'Bob' byte-for-byte)
* and the walk would keep going instead of recognizing it as the already-visited start node.
*/
@Test
fun recursiveCteWithUnionDedupsCaseVariantsThroughTheColumnsDeclaredCollation() {
run("CREATE TABLE origin (name TEXT COLLATE NOCASE)")
run("INSERT INTO origin VALUES ('Bob')")
run("CREATE TABLE steps (name TEXT COLLATE NOCASE, next_name TEXT COLLATE NOCASE)")
run("INSERT INTO steps VALUES ('Bob', 'Carol'), ('Carol', 'bob')")
val result = select(
"WITH RECURSIVE walk(name) AS (" +
"SELECT name FROM origin " +
"UNION " +
"SELECT steps.next_name FROM steps JOIN walk ON steps.name = walk.name" +
") SELECT name FROM walk ORDER BY name"
)
assertEquals(listOf(listOf(text("Bob")), listOf(text("Carol"))), result.rows)
}