diff --git a/src/test/kotlin/kestrel/store/sql/DiskExecutorColumnCollationTest.kt b/src/test/kotlin/kestrel/store/sql/DiskExecutorColumnCollationTest.kt index db19d41..6058c8b 100644 --- a/src/test/kotlin/kestrel/store/sql/DiskExecutorColumnCollationTest.kt +++ b/src/test/kotlin/kestrel/store/sql/DiskExecutorColumnCollationTest.kt @@ -152,3 +152,159 @@ class DiskExecutorColumnCollationTest { ) } + @Test + fun explicitCollateInASetOperationsOrderByOverridesTheColumnsDeclaredCollation() { + val db = newDatabase() + db.seedNames() + val result = db.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 }) + } + + @Test + fun declaredCollationDoesNotReachThroughADerivedTableInASetOperationsLeftBranch() { + val db = newDatabase() + db.seedNames() + val result = db.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 }) + } + + /** Mirrors [kestrel.ColumnCollationTest.groupByDedupsCaseVariantsThroughTheColumnsDeclaredCollation]. */ + @Test + fun groupByDedupsCaseVariantsThroughTheColumnsDeclaredCollation() { + val db = newDatabase() + db.seedNames() + val result = db.select("SELECT name, COUNT(*) FROM people GROUP BY name ORDER BY name") + assertEquals( + listOf("Alice" to 1L, "Bob" to 2L, "Zoe" to 1L).map { (name, count) -> listOf(Value.TextValue(name), int(count)) }, + result.rows + ) + } + + /** Mirrors [kestrel.ColumnCollationTest.selectDistinctDedupsCaseVariantsThroughTheColumnsDeclaredCollation]. */ + @Test + fun selectDistinctDedupsCaseVariantsThroughTheColumnsDeclaredCollation() { + val db = newDatabase() + db.seedNames() + val result = db.select("SELECT DISTINCT name FROM people ORDER BY name") + assertEquals(listOf("Alice", "Bob", "Zoe").map { listOf(Value.TextValue(it)) }, result.rows) + } + + /** Mirrors [kestrel.ColumnCollationTest.selectDistinctStarDedupsCaseVariantsThroughTheColumnsDeclaredCollation]. */ + @Test + fun selectDistinctStarDedupsCaseVariantsThroughTheColumnsDeclaredCollation() { + val db = newDatabase() + db.run("CREATE TABLE tags (label TEXT COLLATE NOCASE)") + db.run("INSERT INTO tags VALUES ('Bob'), ('bob'), ('Alice')") + val result = db.select("SELECT DISTINCT * FROM tags ORDER BY label") + assertEquals(listOf("Alice", "Bob").map { listOf(Value.TextValue(it)) }, result.rows) + } + + /** Mirrors [kestrel.ColumnCollationTest.selectDistinctOnDedupsCaseVariantsThroughTheColumnsDeclaredCollation]. */ + @Test + fun selectDistinctOnDedupsCaseVariantsThroughTheColumnsDeclaredCollation() { + val db = newDatabase() + db.run("CREATE TABLE people (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE, hits INTEGER)") + db.run("INSERT INTO people VALUES (1, 'Bob', 10), (2, 'bob', 20), (3, 'Alice', 5)") + val result = db.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(Value.TextValue(name), int(hits)) }, + result.rows + ) + } + + /** Mirrors [kestrel.ColumnCollationTest.countDistinctDedupsCaseVariantsThroughTheColumnsDeclaredCollation]. */ + @Test + fun countDistinctDedupsCaseVariantsThroughTheColumnsDeclaredCollation() { + val db = newDatabase() + db.seedNames() + assertEquals(int(3), db.select("SELECT COUNT(DISTINCT name) FROM people").rows.single().single()) + } + + /** Mirrors [kestrel.ColumnCollationTest.plainUnionDedupsCaseVariantsThroughTheColumnsDeclaredCollation]. */ + @Test + fun plainUnionDedupsCaseVariantsThroughTheColumnsDeclaredCollation() { + val db = newDatabase() + db.seedNames() + val result = db.select("SELECT name FROM people WHERE id = 1 UNION SELECT name FROM people WHERE id = 2") + assertEquals(listOf(listOf(Value.TextValue("Bob"))), result.rows) + } + + /** Mirrors [kestrel.ColumnCollationTest.intersectMatchesCaseVariantsThroughTheColumnsDeclaredCollation]. */ + @Test + fun intersectMatchesCaseVariantsThroughTheColumnsDeclaredCollation() { + val db = newDatabase() + db.seedNames() + val result = db.select("SELECT name FROM people WHERE id = 1 INTERSECT SELECT name FROM people WHERE id = 2") + assertEquals(listOf(listOf(Value.TextValue("Bob"))), result.rows) + } + + /** Mirrors [kestrel.ColumnCollationTest.exceptExcludesCaseVariantsThroughTheColumnsDeclaredCollation]. */ + @Test + fun exceptExcludesCaseVariantsThroughTheColumnsDeclaredCollation() { + val db = newDatabase() + db.seedNames() + val result = db.select("SELECT name FROM people WHERE id IN (1, 4) EXCEPT SELECT name FROM people WHERE id = 2") + assertEquals(listOf(listOf(Value.TextValue("Zoe"))), result.rows) + } + + /** Mirrors [kestrel.ColumnCollationTest.recursiveCteWithUnionDedupsCaseVariantsThroughTheColumnsDeclaredCollation]. */ + @Test + fun recursiveCteWithUnionDedupsCaseVariantsThroughTheColumnsDeclaredCollation() { + val db = newDatabase() + db.run("CREATE TABLE origin (name TEXT COLLATE NOCASE)") + db.run("INSERT INTO origin VALUES ('Bob')") + db.run("CREATE TABLE steps (name TEXT COLLATE NOCASE, next_name TEXT COLLATE NOCASE)") + db.run("INSERT INTO steps VALUES ('Bob', 'Carol'), ('Carol', 'bob')") + val result = db.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(Value.TextValue("Bob")), listOf(Value.TextValue("Carol"))), result.rows) + } + + /** + * [DiskTable.rejectDuplicateUniqueConstraints] (declarative `UNIQUE`, heap-scanned on every + * write, no backing index) folded through [kestrel.engine.collationFoldedUniquenessKey] - + * mirrors [kestrel.ColumnCollationTest.uniqueConstraintHonorsTheColumnsDeclaredCollation]. + */ + @Test + fun declarativeUniqueConstraintHonorsTheColumnsDeclaredCollation() { + val db = newDatabase() + db.run("CREATE TABLE u1 (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE UNIQUE)") + db.run("INSERT INTO u1 VALUES (1, 'Bob')") + assertFailsWith { db.run("INSERT INTO u1 VALUES (2, 'bob')") } + } + + /** Control negative: a `UNIQUE` column with no declared `COLLATE` behaves exactly as before this session. */ + @Test + fun declarativeUniqueConstraintWithNoDeclaredCollationStaysBinary() { + val db = newDatabase() + db.run("CREATE TABLE u3 (id INTEGER PRIMARY KEY, name TEXT UNIQUE)") + db.run("INSERT INTO u3 VALUES (1, 'Bob')") + db.run("INSERT INTO u3 VALUES (2, 'bob')") + assertEquals(2, db.select("SELECT id FROM u3").rows.size) + } + + /** + * [DiskTable.backfillSecondaryIndex]'s pre-existing-duplicate check, also folded through + * [kestrel.engine.collationFoldedUniquenessKey] - two rows already differing only by case + * under a `NOCASE`-declared column must now fail `CREATE UNIQUE INDEX` cleanly instead of + * silently building an index that lies about uniqueness. + */ + @Test + fun createUniqueIndexHonorsTheColumnsDeclaredCollationAgainstPreexistingRows() { + val db = newDatabase() + db.run("CREATE TABLE u2 (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE)") + db.run("INSERT INTO u2 VALUES (1, 'Bob'), (2, 'bob')") + assertFailsWith { db.run("CREATE UNIQUE INDEX idxU2 ON u2 (name)") } + } +