test(sql): cover aDeclaredColumnCollationKeepsComparingTheSameWayAfte...
This commit is contained in:
1 parent
ffb95375fe
commit
19e84498c6
1 file changed
+154
@@ -308,3 +308,157 @@ class DiskExecutorColumnCollationTest {
|
||||
assertFailsWith<DiskTableException> { db.run("CREATE UNIQUE INDEX idxU2 ON u2 (name)") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aDeclaredColumnCollationKeepsComparingTheSameWayAfterARealFileCloseAndReopen() {
|
||||
val db = newDatabase()
|
||||
db.seedNames()
|
||||
assertEquals(
|
||||
listOf(int(1), int(2)),
|
||||
db.select("SELECT id FROM people WHERE name = 'BOB' ORDER BY id").rows.map { it.single() }
|
||||
)
|
||||
db.reopen()
|
||||
assertEquals(
|
||||
listOf(int(1), int(2)),
|
||||
db.select("SELECT id FROM people WHERE name = 'BOB' ORDER BY id").rows.map { it.single() }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* [DiskIndexAccessPlanner]'s equivalent of [kestrel.ColumnCollationTest.indexedEqualityHonorsTheColumnsDeclaredCollation]:
|
||||
* [DiskTable.findBySecondary] used to encode both the inserted row's key AND the search
|
||||
* literal straight to bytes via [kestrel.store.codec.KeyCodec], with no regard for the
|
||||
* column's declared `COLLATE` - so once `CREATE INDEX` made the planner choose an index scan
|
||||
* over the full scan the un-indexed [bareEqualityHonorsTheColumnsDeclaredCollationWithoutAnExplicitClause]
|
||||
* exercises, the case-variant row silently dropped out of the result instead of raising an
|
||||
* error, since [DiskExecutor] only re-checks WHERE against candidates the chosen access plan
|
||||
* already returned.
|
||||
*/
|
||||
@Test
|
||||
fun indexedEqualityHonorsTheColumnsDeclaredCollation() {
|
||||
val db = newDatabase()
|
||||
db.seedNames()
|
||||
val scanned = db.select("SELECT id FROM people WHERE name = 'BOB' ORDER BY id").rows
|
||||
db.run("CREATE INDEX idxName ON people (name)")
|
||||
val plan = db.explain("EXPLAIN SELECT id FROM people WHERE name = 'BOB'")
|
||||
assertTrue(plan.any { it.contains("Index Scan using idxName") }, "expected index scan, got $plan")
|
||||
val indexed = db.select("SELECT id FROM people WHERE name = 'BOB' ORDER BY id").rows
|
||||
assertEquals(scanned, indexed)
|
||||
assertEquals(listOf(listOf(int(1)), listOf(int(2))), indexed)
|
||||
}
|
||||
|
||||
/** Mirrors [indexedEqualityHonorsTheColumnsDeclaredCollation] for a trailing-space collation instead of case. */
|
||||
@Test
|
||||
fun indexedEqualityHonorsAnRtrimDeclaredCollation() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE tags (id INTEGER PRIMARY KEY, tag TEXT COLLATE RTRIM)")
|
||||
db.run("INSERT INTO tags VALUES (1, 'a'), (2, 'a '), (3, 'b')")
|
||||
db.run("CREATE INDEX idxTag ON tags (tag)")
|
||||
val plan = db.explain("EXPLAIN SELECT id FROM tags WHERE tag = 'a'")
|
||||
assertTrue(plan.any { it.contains("Index Scan using idxTag") }, "expected index scan, got $plan")
|
||||
assertEquals(listOf(listOf(int(1)), listOf(int(2))), db.select("SELECT id FROM tags WHERE tag = 'a' ORDER BY id").rows)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors [kestrel.ColumnCollationTest.compositeIndexEqualityHonorsPerColumnDeclaredCollationIndependently]:
|
||||
* a composite secondary index's key is `encodeKey(first) ++ encodeKey(last) ++ rid` - each
|
||||
* column position must fold through its OWN declared collation independently (`first` is
|
||||
* NOCASE, `last` stays BINARY) rather than, say, folding the whole composite key uniformly or
|
||||
* not at all.
|
||||
*/
|
||||
@Test
|
||||
fun compositeIndexEqualityHonorsPerColumnDeclaredCollationIndependently() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE contacts (id INTEGER PRIMARY KEY, first TEXT COLLATE NOCASE, last TEXT)")
|
||||
db.run("INSERT INTO contacts VALUES (1, 'Bob', 'Smith'), (2, 'bob', 'SMITH')")
|
||||
db.run("CREATE INDEX idxContact ON contacts (first, last)")
|
||||
val plan = db.explain("EXPLAIN SELECT id FROM contacts WHERE first = 'BOB' AND last = 'Smith'")
|
||||
assertTrue(plan.any { it.contains("Index Scan using idxContact") }, "got $plan")
|
||||
assertEquals(
|
||||
listOf(listOf(int(1))),
|
||||
db.select("SELECT id FROM contacts WHERE first = 'BOB' AND last = 'Smith'").rows
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Safety net mirroring [kestrel.ColumnCollationTest.explicitCollateOverrideOnAnIndexedColumnFallsBackToASeqScanInsteadOfMisusingTheIndex]:
|
||||
* [DiskIndexAccessPlanner.asColumnPredicate] only recognizes a bare [kestrel.sql.ast.ColumnExpression],
|
||||
* so an explicit `COLLATE`-wrapped operand is excluded from pushdown entirely instead of being
|
||||
* (wrongly) served by an index whose physical bytes are folded for the column's OWN collation.
|
||||
*/
|
||||
@Test
|
||||
fun explicitCollateOverrideOnAnIndexedColumnFallsBackToASeqScanInsteadOfMisusingTheIndex() {
|
||||
val db = newDatabase()
|
||||
db.seedNames()
|
||||
db.run("CREATE INDEX idxName ON people (name)")
|
||||
val plan = db.explain("EXPLAIN SELECT id FROM people WHERE name COLLATE BINARY = 'BOB'")
|
||||
assertTrue(plan.any { it.contains("Seq Scan") }, "expected seq scan fallback, got $plan")
|
||||
assertEquals(emptyList(), db.select("SELECT id FROM people WHERE name COLLATE BINARY = 'BOB'").rows)
|
||||
}
|
||||
|
||||
/** Control negative: an index over a column with no declared `COLLATE` behaves exactly as before this session. */
|
||||
@Test
|
||||
fun indexedEqualityWithNoDeclaredCollationStaysBinary() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE plain3 (id INTEGER PRIMARY KEY, name TEXT)")
|
||||
db.run("INSERT INTO plain3 VALUES (1, 'Bob'), (2, 'bob')")
|
||||
db.run("CREATE INDEX idxPlain3 ON plain3 (name)")
|
||||
val plan = db.explain("EXPLAIN SELECT id FROM plain3 WHERE name = 'Bob'")
|
||||
assertTrue(plan.any { it.contains("Index Scan using idxPlain3") }, "got $plan")
|
||||
assertEquals(listOf(listOf(int(1))), db.select("SELECT id FROM plain3 WHERE name = 'Bob'").rows)
|
||||
}
|
||||
|
||||
/**
|
||||
* [DiskTable.findByPrimaryKey]'s point-lookup counterpart of [indexedEqualityHonorsTheColumnsDeclaredCollation]:
|
||||
* a single-column, inline `PRIMARY KEY` is backed by its own B+Tree exactly like a secondary
|
||||
* index, so `WHERE name = 'BOB'` against a `NOCASE`-declared `PRIMARY KEY` column must be
|
||||
* planned as [DiskAccessPlan.PrimaryKeyPoint] and still find the case-variant row.
|
||||
*/
|
||||
@Test
|
||||
fun primaryKeyPointLookupHonorsTheColumnsDeclaredCollation() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE pk1 (name TEXT COLLATE NOCASE PRIMARY KEY, hits INTEGER)")
|
||||
db.run("INSERT INTO pk1 VALUES ('Bob', 10)")
|
||||
val plan = db.explain("EXPLAIN SELECT hits FROM pk1 WHERE name = 'BOB'")
|
||||
assertTrue(plan.any { it.contains("Primary Key Scan") }, "expected primary key point lookup, got $plan")
|
||||
assertEquals(listOf(listOf(int(10))), db.select("SELECT hits FROM pk1 WHERE name = 'BOB'").rows)
|
||||
}
|
||||
|
||||
/**
|
||||
* [encodePrimaryKey]'s composite counterpart of [primaryKeyPointLookupHonorsTheColumnsDeclaredCollation]:
|
||||
* a table-level composite `PRIMARY KEY (region, code)` folds each position through ITS OWN
|
||||
* declared collation independently ([DiskAccessPlan.CompositePrimaryKeyPoint] mirrors
|
||||
* [compositeIndexEqualityHonorsPerColumnDeclaredCollationIndependently] but for the PK's own
|
||||
* B+Tree rather than a secondary index) - `region` is NOCASE (matches both rows) but `code`
|
||||
* stays plain BINARY (only an exact-case match counts), so only row 1 survives both conjuncts.
|
||||
*/
|
||||
@Test
|
||||
fun compositePrimaryKeyPointLookupHonorsPerColumnDeclaredCollationIndependently() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE zones (region TEXT COLLATE NOCASE, code TEXT, hits INTEGER, PRIMARY KEY (region, code))")
|
||||
db.run("INSERT INTO zones VALUES ('EU', 'west', 1), ('eu', 'WEST', 2)")
|
||||
val plan = db.explain("EXPLAIN SELECT hits FROM zones WHERE region = 'eu' AND code = 'west'")
|
||||
assertTrue(plan.any { it.contains("Primary Key Scan") }, "expected composite primary key point lookup, got $plan")
|
||||
assertEquals(listOf(listOf(int(1))), db.select("SELECT hits FROM zones WHERE region = 'eu' AND code = 'west'").rows)
|
||||
}
|
||||
|
||||
/**
|
||||
* [DiskTable.rangeByPrimaryKey]'s composite counterpart of [rangeQueryOnAPrimaryKeyHonorsTheColumnsDeclaredCollation]:
|
||||
* a table-level composite `PRIMARY KEY (order_id, sku)` ranged on its LAST column (`sku`, the
|
||||
* only shape [DiskAccessPlan.CompositePrimaryKeyRange] plans - see its own doc) after an
|
||||
* equality prefix on `order_id` (plain BINARY, irrelevant here) still folds the bound through
|
||||
* `sku`'s own declared `NOCASE` collation, same as the single-column case - `'Mango'` only
|
||||
* survives `sku > 'lemon'` once both sides are canonicalized to `'MANGO' > 'LEMON'`.
|
||||
*/
|
||||
@Test
|
||||
fun rangeQueryOnTheLastColumnOfACompositePrimaryKeyAfterAnEqualityPrefixHonorsTheColumnsDeclaredCollation() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE order_lines4 (order_id INTEGER, sku TEXT COLLATE NOCASE, PRIMARY KEY (order_id, sku))")
|
||||
db.run("INSERT INTO order_lines4 VALUES (1, 'Apple'), (1, 'Mango'), (1, 'kiwi'), (2, 'Zebra')")
|
||||
val plan = db.explain("EXPLAIN SELECT sku FROM order_lines4 WHERE order_id = 1 AND sku > 'lemon'")
|
||||
assertTrue(plan.any { it.contains("Primary Key Range Scan") }, "expected primary key range scan, got $plan")
|
||||
assertEquals(
|
||||
listOf(listOf(Value.TextValue("Mango"))),
|
||||
db.select("SELECT sku FROM order_lines4 WHERE order_id = 1 AND sku > 'lemon'").rows
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user