test: cover index access and join execution
This commit is contained in:
1 parent
99227e6c78
commit
bf8e197760
2 files changed
+233
No files matched your search
@@ -0,0 +1,115 @@
|
||||
package kestrel
|
||||
|
||||
import kestrel.catalog.Catalog
|
||||
import kestrel.engine.ExplainResult
|
||||
import kestrel.engine.Executor
|
||||
import kestrel.engine.SelectResult
|
||||
import kestrel.engine.StatementResult
|
||||
import kestrel.sql.lexer.Lexer
|
||||
import kestrel.sql.parser.Parser
|
||||
import kestrel.storage.Value
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class IndexTest {
|
||||
private val catalog = Catalog()
|
||||
private val executor = Executor(catalog)
|
||||
|
||||
private fun run(sql: String): List<StatementResult> =
|
||||
Parser(Lexer(sql).tokenize()).parseProgram().map { executor.execute(it) }
|
||||
|
||||
private fun select(sql: String): SelectResult = run(sql).single() as SelectResult
|
||||
|
||||
private fun explain(sql: String): List<String> = (run(sql).single() as ExplainResult).plan
|
||||
|
||||
private fun seed() {
|
||||
run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER)")
|
||||
run(
|
||||
"INSERT INTO users VALUES " +
|
||||
"(1, 'ada', 36), (2, 'linus', 54), (3, 'grace', 85), (4, 'don', 54), (5, 'edsger', 40)"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun equalityLookupMatchesFullScan() {
|
||||
seed()
|
||||
val scanned = select("SELECT name FROM users WHERE age = 54").rows
|
||||
run("CREATE INDEX idxAge ON users (age)")
|
||||
val indexed = select("SELECT name FROM users WHERE age = 54").rows
|
||||
assertEquals(scanned, indexed)
|
||||
assertEquals(2, indexed.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rangeLookupMatchesFullScan() {
|
||||
seed()
|
||||
val scanned = select("SELECT name FROM users WHERE age >= 40 AND age < 85").rows
|
||||
run("CREATE INDEX idxAge ON users (age)")
|
||||
val indexed = select("SELECT name FROM users WHERE age >= 40 AND age < 85").rows
|
||||
assertEquals(scanned, indexed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun plannerChoosesIndexForEquality() {
|
||||
seed()
|
||||
assertTrue(explain("EXPLAIN SELECT name FROM users WHERE age = 54").any { it.contains("Seq Scan") })
|
||||
run("CREATE INDEX idxAge ON users (age)")
|
||||
val plan = explain("EXPLAIN SELECT name FROM users WHERE age = 54")
|
||||
assertTrue(plan.any { it.contains("Index Scan using idxAge") }, "expected index scan, got $plan")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun plannerChoosesIndexForRange() {
|
||||
seed()
|
||||
run("CREATE INDEX idxAge ON users (age)")
|
||||
val plan = explain("EXPLAIN SELECT name FROM users WHERE age > 50")
|
||||
assertTrue(plan.any { it.contains("Index Scan using idxAge") && it.contains("age > 50") }, "got $plan")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun plannerFallsBackToScanWithoutIndex() {
|
||||
seed()
|
||||
val plan = explain("EXPLAIN SELECT name FROM users WHERE age > 50")
|
||||
assertTrue(plan.any { it.contains("Seq Scan on users") }, "got $plan")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun indexStaysCorrectAfterUpdateAndDelete() {
|
||||
seed()
|
||||
run("CREATE INDEX idxAge ON users (age)")
|
||||
run("UPDATE users SET age = 54 WHERE name = 'ada'")
|
||||
run("DELETE FROM users WHERE name = 'linus'")
|
||||
val indexed = select("SELECT name FROM users WHERE age = 54").rows
|
||||
.map { (it.single() as Value.TextValue).value }
|
||||
assertEquals(setOf("ada", "don"), indexed.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dropIndexRemovesItFromPlanner() {
|
||||
seed()
|
||||
run("CREATE INDEX idxAge ON users (age)")
|
||||
run("DROP INDEX idxAge")
|
||||
val plan = explain("EXPLAIN SELECT name FROM users WHERE age = 54")
|
||||
assertTrue(plan.any { it.contains("Seq Scan on users") }, "got $plan")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun duplicateIndexIsRejected() {
|
||||
seed()
|
||||
run("CREATE INDEX idxAge ON users (age)")
|
||||
assertFailsWith<KestrelException> { run("CREATE INDEX idxAge ON users (id)") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun indexOnUnknownColumnIsRejected() {
|
||||
seed()
|
||||
assertFailsWith<KestrelException> { run("CREATE INDEX bad ON users (missing)") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun droppingUnknownIndexIsRejected() {
|
||||
assertFailsWith<KestrelException> { run("DROP INDEX ghost") }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package kestrel
|
||||
|
||||
import kestrel.catalog.Catalog
|
||||
import kestrel.engine.ExplainResult
|
||||
import kestrel.engine.Executor
|
||||
import kestrel.engine.SelectResult
|
||||
import kestrel.engine.StatementResult
|
||||
import kestrel.sql.lexer.Lexer
|
||||
import kestrel.sql.parser.Parser
|
||||
import kestrel.storage.Value
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class JoinTest {
|
||||
private val catalog = Catalog()
|
||||
private val executor = Executor(catalog)
|
||||
|
||||
private fun run(sql: String): List<StatementResult> =
|
||||
Parser(Lexer(sql).tokenize()).parseProgram().map { executor.execute(it) }
|
||||
|
||||
private fun select(sql: String): SelectResult = run(sql).single() as SelectResult
|
||||
|
||||
private fun explain(sql: String): List<String> = (run(sql).single() as ExplainResult).plan
|
||||
|
||||
private fun text(value: String) = Value.TextValue(value)
|
||||
private fun int(value: Long) = Value.IntegerValue(value)
|
||||
|
||||
private fun seed() {
|
||||
run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)")
|
||||
run("INSERT INTO users VALUES (1, 'ada'), (2, 'linus'), (3, 'grace')")
|
||||
run("CREATE TABLE orders (id INTEGER PRIMARY KEY, userId INTEGER, total INTEGER)")
|
||||
run("INSERT INTO orders VALUES (10, 1, 100), (11, 1, 40), (12, 2, 70)")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun innerJoinMatchesOnCondition() {
|
||||
seed()
|
||||
val result = select(
|
||||
"SELECT u.name, o.total FROM users u INNER JOIN orders o ON u.id = o.userId ORDER BY o.total ASC"
|
||||
)
|
||||
assertEquals(listOf("name", "total"), result.columns)
|
||||
assertEquals(
|
||||
listOf(listOf(text("ada"), int(40)), listOf(text("linus"), int(70)), listOf(text("ada"), int(100))),
|
||||
result.rows
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun leftJoinKeepsUnmatchedRowsWithNulls() {
|
||||
seed()
|
||||
val result = select(
|
||||
"SELECT u.name, o.total FROM users u LEFT JOIN orders o ON u.id = o.userId ORDER BY o.total ASC"
|
||||
)
|
||||
assertEquals(listOf(text("grace"), Value.NullValue), result.rows[0])
|
||||
assertEquals(listOf(text("ada"), int(40)), result.rows[1])
|
||||
assertEquals(listOf(text("linus"), int(70)), result.rows[2])
|
||||
assertEquals(listOf(text("ada"), int(100)), result.rows[3])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hashJoinAndNestedLoopProduceSameResult() {
|
||||
seed()
|
||||
val hashed = select(
|
||||
"SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.userId ORDER BY o.total ASC"
|
||||
).rows
|
||||
val nested = select(
|
||||
"SELECT u.name, o.total FROM users u JOIN orders o ON u.id >= o.userId AND u.id <= o.userId ORDER BY o.total ASC"
|
||||
).rows
|
||||
assertEquals(hashed, nested)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun plannerUsesHashJoinForEquality() {
|
||||
seed()
|
||||
val plan = explain("EXPLAIN SELECT u.name FROM users u JOIN orders o ON u.id = o.userId")
|
||||
assertTrue(plan.any { it.contains("Hash Join") }, "got $plan")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun plannerUsesNestedLoopForInequality() {
|
||||
seed()
|
||||
val plan = explain("EXPLAIN SELECT u.name FROM users u JOIN orders o ON u.id < o.userId")
|
||||
assertTrue(plan.any { it.contains("Nested Loop") }, "got $plan")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun threeTableJoinResolvesColumns() {
|
||||
seed()
|
||||
run("CREATE TABLE items (orderId INTEGER, label TEXT)")
|
||||
run("INSERT INTO items VALUES (10, 'book'), (11, 'pen'), (12, 'lamp')")
|
||||
val result = select(
|
||||
"SELECT u.name, i.label FROM users u " +
|
||||
"JOIN orders o ON u.id = o.userId " +
|
||||
"JOIN items i ON o.id = i.orderId ORDER BY i.label ASC"
|
||||
)
|
||||
assertEquals(
|
||||
listOf(listOf(text("ada"), text("book")), listOf(text("linus"), text("lamp")), listOf(text("ada"), text("pen"))),
|
||||
result.rows
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unqualifiedColumnResolvesAcrossJoin() {
|
||||
seed()
|
||||
val result = select("SELECT name, total FROM users u JOIN orders o ON u.id = o.userId WHERE total > 50 ORDER BY total ASC")
|
||||
assertEquals(listOf(listOf(text("linus"), int(70)), listOf(text("ada"), int(100))), result.rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ambiguousColumnIsRejected() {
|
||||
seed()
|
||||
assertFailsWith<KestrelException> {
|
||||
select("SELECT id FROM users u JOIN orders o ON u.id = o.userId")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user