From bf8e19776090626ab72e95666f4401a2bf0b64a7 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 28 Aug 2026 09:48:17 +0200 Subject: [PATCH] test: cover index access and join execution --- src/test/kotlin/kestrel/IndexTest.kt | 115 ++++++++++++++++++++++++++ src/test/kotlin/kestrel/JoinTest.kt | 118 +++++++++++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 src/test/kotlin/kestrel/IndexTest.kt create mode 100644 src/test/kotlin/kestrel/JoinTest.kt diff --git a/src/test/kotlin/kestrel/IndexTest.kt b/src/test/kotlin/kestrel/IndexTest.kt new file mode 100644 index 0000000..56dcf6e --- /dev/null +++ b/src/test/kotlin/kestrel/IndexTest.kt @@ -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 = + 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 = (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 { run("CREATE INDEX idxAge ON users (id)") } + } + + @Test + fun indexOnUnknownColumnIsRejected() { + seed() + assertFailsWith { run("CREATE INDEX bad ON users (missing)") } + } + + @Test + fun droppingUnknownIndexIsRejected() { + assertFailsWith { run("DROP INDEX ghost") } + } +} diff --git a/src/test/kotlin/kestrel/JoinTest.kt b/src/test/kotlin/kestrel/JoinTest.kt new file mode 100644 index 0000000..3733522 --- /dev/null +++ b/src/test/kotlin/kestrel/JoinTest.kt @@ -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 = + 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 = (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 { + select("SELECT id FROM users u JOIN orders o ON u.id = o.userId") + } + } +}