test(sql): cover DiskExecutorColumnCollationTest and tempFile
This commit is contained in:
1 parent
7cf0ce3d90
commit
fae2af0764
1 file changed
+154
@@ -0,0 +1,154 @@
|
|||||||
|
package kestrel.store.sql
|
||||||
|
|
||||||
|
import kestrel.engine.ExplainResult
|
||||||
|
import kestrel.engine.SelectResult
|
||||||
|
import kestrel.engine.StatementResult
|
||||||
|
import kestrel.sql.lexer.Lexer
|
||||||
|
import kestrel.sql.parser.Parser
|
||||||
|
import kestrel.storage.Value
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Files
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A column's own declared `COLLATE` (see [kestrel.catalog.Column.collation]) against the disk
|
||||||
|
* engine, mirroring a subset of [kestrel.ColumnCollationTest]'s in-memory scenarios to prove the
|
||||||
|
* same [kestrel.engine.Evaluator]/[kestrel.engine.ColumnResolver.declaredCollation] machinery
|
||||||
|
* [DiskExecutor] reuses behaves identically here, plus the one thing that only matters for this
|
||||||
|
* engine: the declared collation is stored as a byte in [DirectoryEntryCodec]'s own binary schema
|
||||||
|
* encoding (not this engine's text dump, which does not exist here), so it must survive a real
|
||||||
|
* file close/reopen, not just staying alive in the same [DiskCatalog] instance.
|
||||||
|
*/
|
||||||
|
class DiskExecutorColumnCollationTest {
|
||||||
|
private fun tempFile(prefix: String): File = Files.createTempFile(prefix, ".db").toFile().also { it.deleteOnExit() }
|
||||||
|
|
||||||
|
private fun bool(value: Boolean) = Value.BooleanValue(value)
|
||||||
|
private fun int(value: Long) = Value.IntegerValue(value)
|
||||||
|
|
||||||
|
private class Database(val dataFile: File, val walFile: File) {
|
||||||
|
var catalog = DiskCatalog.open(dataFile, walFile)
|
||||||
|
var executor = DiskExecutor(catalog)
|
||||||
|
|
||||||
|
fun run(sql: String): List<StatementResult> = Parser(Lexer(sql).tokenize()).parseProgram().map { executor.execute(it) }
|
||||||
|
fun select(sql: String): SelectResult = run(sql).single() as SelectResult
|
||||||
|
fun explain(sql: String): List<String> = (run(sql).single() as ExplainResult).plan
|
||||||
|
|
||||||
|
fun reopen() {
|
||||||
|
catalog.close()
|
||||||
|
catalog = DiskCatalog.open(dataFile, walFile)
|
||||||
|
executor = DiskExecutor(catalog)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newDatabase(): Database = Database(tempFile("kestrel-disk-column-collation-data"), tempFile("kestrel-disk-column-collation-wal"))
|
||||||
|
|
||||||
|
private fun Database.seedNames() {
|
||||||
|
run("CREATE TABLE people (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE)")
|
||||||
|
run("INSERT INTO people VALUES (1, 'Bob'), (2, 'bob'), (3, 'Alice'), (4, 'Zoe')")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun bareEqualityHonorsTheColumnsDeclaredCollationWithoutAnExplicitClause() {
|
||||||
|
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() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun explicitCollateInTheQueryOverridesTheColumnsDeclaredCollation() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seedNames()
|
||||||
|
assertEquals(bool(false), db.select("SELECT name COLLATE BINARY = 'BOB' FROM people WHERE id = 1").rows.single().single())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun columnWithNoDeclaredCollationStaysBinaryByDefault() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.run("CREATE TABLE plain (id INTEGER PRIMARY KEY, name TEXT)")
|
||||||
|
db.run("INSERT INTO plain VALUES (1, 'Bob')")
|
||||||
|
assertEquals(bool(false), db.select("SELECT name = 'BOB' FROM plain WHERE id = 1").rows.single().single())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun declaredCollationAppliesInsideAnOnConflictDoUpdateAssignment() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE, was_bob BOOLEAN)")
|
||||||
|
db.run("INSERT INTO t VALUES (1, 'Bob', FALSE)")
|
||||||
|
db.run("INSERT INTO t (id, name) VALUES (1, 'x') ON CONFLICT (id) DO UPDATE SET was_bob = (name = 'BOB')")
|
||||||
|
assertEquals(bool(true), db.select("SELECT was_bob FROM t WHERE id = 1").rows.single().single())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun declaredCollationAppliesInAMergeOnCondition() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.run("CREATE TABLE accounts (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE, hits INTEGER)")
|
||||||
|
db.run("CREATE TABLE staging (id INTEGER PRIMARY KEY, name TEXT)")
|
||||||
|
db.run("INSERT INTO accounts VALUES (1, 'Bob', 0)")
|
||||||
|
db.run("INSERT INTO staging VALUES (2, 'BOB')")
|
||||||
|
db.run("MERGE INTO accounts USING staging ON accounts.name = staging.name WHEN MATCHED THEN UPDATE SET hits = hits + 1")
|
||||||
|
assertEquals(int(1), db.select("SELECT hits FROM accounts WHERE id = 1").rows.single().single())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun declaredCollationAppliesInATriggerWhenClauseOnNew() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE)")
|
||||||
|
db.run("CREATE TABLE hits (id INTEGER PRIMARY KEY, count INTEGER)")
|
||||||
|
db.run("INSERT INTO t VALUES (1, 'Bob')")
|
||||||
|
db.run("INSERT INTO hits VALUES (1, 0)")
|
||||||
|
db.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"
|
||||||
|
)
|
||||||
|
db.run("UPDATE t SET name = 'bob' WHERE id = 1")
|
||||||
|
assertEquals(int(100), db.select("SELECT count FROM hits WHERE id = 1").rows.single().single())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun declaredCollationAppliesToDefaultOrderBySortOrder() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seedNames()
|
||||||
|
val result = db.select("SELECT name FROM people ORDER BY name")
|
||||||
|
assertEquals(listOf("Alice", "Bob", "bob", "Zoe").map { listOf(Value.TextValue(it)) }, result.rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun explicitCollateInOrderByOverridesTheColumnsDeclaredCollation() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seedNames()
|
||||||
|
val result = db.select("SELECT name FROM people ORDER BY name COLLATE BINARY")
|
||||||
|
assertEquals(listOf("Alice", "Bob", "Zoe", "bob").map { listOf(Value.TextValue(it)) }, result.rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun declaredCollationAppliesInAWindowFunctionsOrderBy() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seedNames()
|
||||||
|
val result = db.select("SELECT name, ROW_NUMBER() OVER (ORDER BY name) AS rn FROM people ORDER BY rn")
|
||||||
|
assertEquals(
|
||||||
|
listOf("Alice" to 1L, "Bob" to 2L, "bob" to 3L, "Zoe" to 4L).map { (name, rn) -> listOf(Value.TextValue(name), int(rn)) },
|
||||||
|
result.rows
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mirrors [kestrel.ColumnCollationTest]'s set-operation ORDER BY cases against the disk engine. */
|
||||||
|
@Test
|
||||||
|
fun declaredCollationAppliesToASetOperationsOrderByThroughAStarProjection() {
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf(int(3) to "Alice", int(1) to "Bob", int(2) to "bob", int(4) to "Zoe")
|
||||||
|
.map { (id, name) -> listOf(id, Value.TextValue(name)) },
|
||||||
|
result.rows
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user