test(sql): cover rangeQueryOnAPrimaryKeyHonorsTheColumnsDeclaredColla...
This commit is contained in:
1 parent
19e84498c6
commit
d699a4d4f1
1 file changed
+94
@@ -462,3 +462,97 @@ class DiskExecutorColumnCollationTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [DiskTable.rangeByPrimaryKey]'s counterpart of [primaryKeyPointLookupHonorsTheColumnsDeclaredCollation]
|
||||||
|
* for a range predicate instead of equality: [kestrel.engine.collationFoldedUniquenessKey] folds
|
||||||
|
* both the query's bound and every stored key through the same declared collation before the
|
||||||
|
* B+Tree ever compares bytes, so the ordering the index range-scans over agrees with what
|
||||||
|
* `Evaluator`'s own collation-aware `>`/`<` would decide row-by-row. The literal bound
|
||||||
|
* (`'ALICE'`, all-caps) is deliberately a different case than the stored `'Alice'` row it must
|
||||||
|
* exclude, proving the bound itself is folded too, not just the stored keys. `NoCase.canonicalize`
|
||||||
|
* uppercases, so the bound is deliberately given in lowercase (`'alice'`, not `'ALICE'`) - an
|
||||||
|
* unfolded lowercase bound would compare as bytewise *greater* than every uppercase-folded
|
||||||
|
* stored key (lowercase ASCII sorts after uppercase), wrongly excluding every row instead of
|
||||||
|
* just `'Alice'` itself.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun rangeQueryOnAPrimaryKeyHonorsTheColumnsDeclaredCollation() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.run("CREATE TABLE pk2 (name TEXT COLLATE NOCASE PRIMARY KEY, hits INTEGER)")
|
||||||
|
db.run("INSERT INTO pk2 VALUES ('Alice', 1), ('Bob', 2), ('Zoe', 3)")
|
||||||
|
val plan = db.explain("EXPLAIN SELECT name FROM pk2 WHERE name > 'alice'")
|
||||||
|
assertTrue(plan.any { it.contains("Primary Key Range Scan") }, "expected primary key range scan, got $plan")
|
||||||
|
assertEquals(
|
||||||
|
listOf(listOf(Value.TextValue("Bob")), listOf(Value.TextValue("Zoe"))),
|
||||||
|
db.select("SELECT name FROM pk2 WHERE name > 'alice' ORDER BY name").rows
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [DiskTable.rangeBySecondary]'s counterpart of [indexedEqualityHonorsTheColumnsDeclaredCollation]
|
||||||
|
* for a range predicate: a secondary index entry carries a RID suffix a primary key entry does
|
||||||
|
* not, so the fold has to reuse [kestrel.engine.collationFoldedUniquenessKey]'s
|
||||||
|
* (all-zero-suffix, all-one-suffix) bracketing trick rather than [DiskTable.rangeByPrimaryKey]'s
|
||||||
|
* bare bound - see [DiskTable.rangeBySecondary]'s own doc for why a bare bound would be wrong
|
||||||
|
* here. `>=` (inclusive) deliberately lands exactly on the folded value of an existing row
|
||||||
|
* (`'bob'` folds the same as the query's `'BOB'`) to prove the inclusive boundary itself is
|
||||||
|
* collation-aware, not just strictly-inside values. The bound is deliberately lowercase
|
||||||
|
* (`'bob'`), same reasoning as [rangeQueryOnAPrimaryKeyHonorsTheColumnsDeclaredCollation]: an
|
||||||
|
* unfolded lowercase bound would sort bytewise after every uppercase-folded stored key.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun rangeQueryOnASecondaryIndexHonorsTheColumnsDeclaredCollation() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.run("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE)")
|
||||||
|
db.run("INSERT INTO items VALUES (1, 'Alice'), (2, 'Bob'), (3, 'ZOE')")
|
||||||
|
db.run("CREATE INDEX idxItemName ON items (name)")
|
||||||
|
val plan = db.explain("EXPLAIN SELECT id FROM items WHERE name >= 'bob'")
|
||||||
|
assertTrue(plan.any { it.contains("Index Range Scan using idxItemName") }, "expected index range scan, got $plan")
|
||||||
|
assertEquals(
|
||||||
|
listOf(listOf(int(2)), listOf(int(3))),
|
||||||
|
db.select("SELECT id FROM items WHERE name >= 'bob' ORDER BY id").rows
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [DiskTable.rangeBySecondary]'s prefix+range counterpart of [rangeQueryOnASecondaryIndexHonorsTheColumnsDeclaredCollation]
|
||||||
|
* and [compositeIndexEqualityHonorsPerColumnDeclaredCollationIndependently]: an equality-prefix
|
||||||
|
* (`order_id`, plain BINARY, irrelevant here) plus a range on the composite index's second column
|
||||||
|
* (`sku`, NOCASE) must fold BOTH the stored `sku` values AND the query's own bound through
|
||||||
|
* `sku`'s declared collation before the B+Tree ever compares bytes - `'Mango'` only survives
|
||||||
|
* `sku > 'lemon'` once both sides are canonicalized to `'MANGO' > 'LEMON'`; an unfolded lowercase
|
||||||
|
* bound would sort bytewise after every uppercase-folded stored value (lowercase ASCII sorts
|
||||||
|
* after uppercase), wrongly excluding every row instead of just `'Apple'`/`'kiwi'`.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun rangeQueryOnASecondaryIndexAfterAnEqualityPrefixHonorsTheColumnsDeclaredCollation() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.run("CREATE TABLE order_lines2 (order_id INTEGER, sku TEXT COLLATE NOCASE)")
|
||||||
|
db.run("INSERT INTO order_lines2 VALUES (1, 'Apple'), (1, 'Mango'), (1, 'kiwi'), (2, 'Zebra')")
|
||||||
|
db.run("CREATE INDEX idxOrderSku2 ON order_lines2 (order_id, sku)")
|
||||||
|
val plan = db.explain("EXPLAIN SELECT sku FROM order_lines2 WHERE order_id = 1 AND sku > 'lemon'")
|
||||||
|
assertTrue(plan.any { it.contains("Index Range Scan using idxOrderSku2") }, "expected index range scan, got $plan")
|
||||||
|
assertEquals(
|
||||||
|
listOf(listOf(Value.TextValue("Mango"))),
|
||||||
|
db.select("SELECT sku FROM order_lines2 WHERE order_id = 1 AND sku > 'lemon'").rows
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now that [DiskTable]'s secondary-index B+Tree keys are folded through the column's declared
|
||||||
|
* `COLLATE`, ONGOING enforcement of an already-created `CREATE UNIQUE INDEX` catches a
|
||||||
|
* case-variant duplicate too - this test used to assert the opposite (see git history) as a
|
||||||
|
* documented scope-cut; [kestrel.engine.collationFoldedUniquenessKey]'s own doc comment
|
||||||
|
* previously named this exact gap as "the harder, still-open half" of the disk engine's
|
||||||
|
* COLLATE support.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun uniqueIndexEnforcementAfterCreationHonorsTheColumnsDeclaredCollation() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.run("CREATE TABLE u4 (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE)")
|
||||||
|
db.run("INSERT INTO u4 VALUES (1, 'Bob')")
|
||||||
|
db.run("CREATE UNIQUE INDEX idxU4 ON u4 (name)")
|
||||||
|
assertFailsWith<DiskTableException> { db.run("INSERT INTO u4 VALUES (2, 'bob')") }
|
||||||
|
assertEquals(listOf(listOf(int(1))), db.select("SELECT id FROM u4").rows)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user