test: cover declaredCollationAppliesInATriggerWhenClauseOnNew
This commit is contained in:
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())
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user