test(sql): cover DiskExecutorExplainTest and tempFile
This commit is contained in:
1 parent
7bed867433
commit
b35dadfe6f
1 file changed
+158
@@ -0,0 +1,158 @@
|
||||
package kestrel.store.sql
|
||||
|
||||
import kestrel.engine.ExplainResult
|
||||
import kestrel.sql.ast.Statement
|
||||
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
|
||||
|
||||
/**
|
||||
* Closes the "this executor has no `explain`" scope cut documented on [DiskExecutor.executeCreateView]:
|
||||
* proves EXPLAIN on the disk-backed executor both describes the right plan shape (Seq Scan/Primary Key
|
||||
* Scan/Index Scan/Nested Loop/GroupAggregate/Sort/Limit/CTE/UNION) AND - the part a plan-text assertion
|
||||
* alone cannot show - never touches a single row of real table data to produce it, verified against the
|
||||
* same [DiskTable] access counters [DiskExecutorTest] itself uses to prove index pushdown really happens.
|
||||
*/
|
||||
class DiskExecutorExplainTest {
|
||||
private fun tempFile(prefix: String): File = Files.createTempFile(prefix, ".db").toFile().also { it.deleteOnExit() }
|
||||
|
||||
private fun parse(sql: String): List<Statement> = Parser(Lexer(sql).tokenize()).parseProgram()
|
||||
|
||||
private fun int(value: Long) = Value.IntegerValue(value)
|
||||
|
||||
private inner class Database(dataFile: File, walFile: File) {
|
||||
val catalog = DiskCatalog.open(dataFile, walFile)
|
||||
val executor = DiskExecutor(catalog)
|
||||
|
||||
fun run(sql: String): List<kestrel.engine.StatementResult> = parse(sql).map { executor.execute(it) }
|
||||
fun explain(sql: String): List<String> = (run(sql).single() as ExplainResult).plan
|
||||
}
|
||||
|
||||
private fun newDatabase(): Database = Database(tempFile("kestrel-disk-explain-data"), tempFile("kestrel-disk-explain-wal"))
|
||||
|
||||
private fun Database.seedUsers() {
|
||||
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)")
|
||||
}
|
||||
|
||||
private fun Database.seedOrders() {
|
||||
run("CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT NOT NULL)")
|
||||
run("INSERT INTO customers VALUES (1, 'ada'), (2, 'linus'), (3, 'grace')")
|
||||
run("CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER NOT NULL, amount INTEGER NOT NULL)")
|
||||
run("INSERT INTO orders VALUES (1, 1, 100), (2, 1, 50), (3, 2, 200)")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun explainOnAnUnindexedWhereReportsASeqScanAndTouchesNoRowData() {
|
||||
val db = newDatabase()
|
||||
db.seedUsers()
|
||||
val table = db.catalog.table("users")
|
||||
val plan = db.explain("EXPLAIN SELECT name FROM users WHERE name = 'linus'")
|
||||
assertTrue(plan.any { it == "Seq Scan on users" })
|
||||
assertEquals(0, table.scanCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun explainOnAPrimaryKeyEqualityReportsAPrimaryKeyScanAndTouchesNoRowData() {
|
||||
val db = newDatabase()
|
||||
db.seedUsers()
|
||||
val table = db.catalog.table("users")
|
||||
val plan = db.explain("EXPLAIN SELECT name FROM users WHERE id = 2")
|
||||
assertTrue(plan.any { it == "Primary Key Scan on users (id = 2)" })
|
||||
assertEquals(0, table.pointLookupCount)
|
||||
assertEquals(0, table.scanCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun explainOnAPrimaryKeyRangeReportsAPrimaryKeyRangeScanAndTouchesNoRowData() {
|
||||
val db = newDatabase()
|
||||
db.seedUsers()
|
||||
val table = db.catalog.table("users")
|
||||
val plan = db.explain("EXPLAIN SELECT name FROM users WHERE id > 1 AND id <= 3")
|
||||
assertTrue(plan.any { it == "Primary Key Range Scan on users (id > 1 AND id <= 3)" })
|
||||
assertEquals(0, table.rangeScanCount)
|
||||
assertEquals(0, table.scanCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun explainOnACompositePrimaryKeyEqualityReportsAPrimaryKeyScan() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE order_lines (order_id INTEGER, line_no INTEGER, sku TEXT, PRIMARY KEY (order_id, line_no))")
|
||||
db.run("INSERT INTO order_lines VALUES (1, 1, 'widget'), (1, 2, 'gadget')")
|
||||
val table = db.catalog.table("order_lines")
|
||||
val plan = db.explain("EXPLAIN SELECT sku FROM order_lines WHERE order_id = 1 AND line_no = 2")
|
||||
assertTrue(plan.any { it == "Primary Key Scan on order_lines (order_id = 1 AND line_no = 2)" })
|
||||
assertEquals(0, table.pointLookupCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun explainOnACompositePrimaryKeyRangeReportsAPrimaryKeyRangeScan() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE order_lines (order_id INTEGER, line_no INTEGER, sku TEXT, PRIMARY KEY (order_id, line_no))")
|
||||
db.run("INSERT INTO order_lines VALUES (1, 1, 'widget'), (1, 2, 'gadget')")
|
||||
val table = db.catalog.table("order_lines")
|
||||
val plan = db.explain("EXPLAIN SELECT sku FROM order_lines WHERE order_id = 1 AND line_no > 1")
|
||||
assertTrue(plan.any { it == "Primary Key Range Scan on order_lines (order_id = 1 AND line_no > 1)" })
|
||||
assertEquals(0, table.rangeScanCount)
|
||||
assertEquals(0, table.scanCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun explainOnASecondaryIndexEqualityNamesTheIndexAndTouchesNoRowData() {
|
||||
val db = newDatabase()
|
||||
db.seedUsers()
|
||||
db.run("CREATE INDEX idx_age ON users(age)")
|
||||
val table = db.catalog.table("users")
|
||||
val lookupsBeforeExplain = table.secondaryLookupCount
|
||||
val scansBeforeExplain = table.scanCount
|
||||
val plan = db.explain("EXPLAIN SELECT name FROM users WHERE age = 54")
|
||||
assertTrue(plan.any { it == "Index Scan using idx_age on users (age = 54)" })
|
||||
assertEquals(lookupsBeforeExplain, table.secondaryLookupCount)
|
||||
assertEquals(scansBeforeExplain, table.scanCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun explainOnASecondaryIndexRangeNamesTheIndexAndTouchesNoRowData() {
|
||||
val db = newDatabase()
|
||||
db.seedUsers()
|
||||
db.run("CREATE INDEX idx_age ON users(age)")
|
||||
val table = db.catalog.table("users")
|
||||
val rangeScansBeforeExplain = table.rangeScanCount
|
||||
val scansBeforeExplain = table.scanCount
|
||||
val plan = db.explain("EXPLAIN SELECT name FROM users WHERE age > 40 AND age <= 85")
|
||||
assertTrue(plan.any { it == "Index Range Scan using idx_age on users (age > 40 AND age <= 85)" })
|
||||
assertEquals(rangeScansBeforeExplain, table.rangeScanCount)
|
||||
assertEquals(scansBeforeExplain, table.scanCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun explainOnACompositeSecondaryIndexPrefixRangeNamesBothThePrefixEqualityAndTheRange() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE order_lines (order_id INTEGER, line_no INTEGER, sku TEXT)")
|
||||
db.run("INSERT INTO order_lines VALUES (1, 1, 'apple'), (1, 2, 'mango'), (2, 1, 'kiwi')")
|
||||
db.run("CREATE INDEX idx_order_sku ON order_lines(order_id, sku)")
|
||||
val table = db.catalog.table("order_lines")
|
||||
val rangeScansBeforeExplain = table.rangeScanCount
|
||||
val scansBeforeExplain = table.scanCount
|
||||
val plan = db.explain("EXPLAIN SELECT line_no FROM order_lines WHERE order_id = 1 AND sku > 'apple'")
|
||||
assertTrue(plan.any { it == "Index Range Scan using idx_order_sku on order_lines (order_id = 1 AND sku > 'apple')" })
|
||||
assertEquals(rangeScansBeforeExplain, table.rangeScanCount)
|
||||
assertEquals(scansBeforeExplain, table.scanCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun explainRevertsToASeqScanAfterTheIndexItWasUsingIsDropped() {
|
||||
val db = newDatabase()
|
||||
db.seedUsers()
|
||||
db.run("CREATE INDEX idx_age ON users(age)")
|
||||
db.run("DROP INDEX idx_age")
|
||||
val plan = db.explain("EXPLAIN SELECT name FROM users WHERE age = 54")
|
||||
assertTrue(plan.any { it == "Seq Scan on users" })
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user