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