test(sql): cover DiskIndexAccessPlannerTest and col
This commit is contained in:
1 parent
dc19ec6444
commit
acedd5f727
1 file changed
+155
@@ -0,0 +1,155 @@
|
|||||||
|
package kestrel.store.sql
|
||||||
|
|
||||||
|
import kestrel.catalog.Column
|
||||||
|
import kestrel.catalog.TableSchema
|
||||||
|
import kestrel.sql.ast.BinaryExpression
|
||||||
|
import kestrel.sql.ast.BinaryOperator
|
||||||
|
import kestrel.sql.ast.ColumnExpression
|
||||||
|
import kestrel.sql.ast.Expression
|
||||||
|
import kestrel.sql.ast.LiteralExpression
|
||||||
|
import kestrel.storage.ColumnType
|
||||||
|
import kestrel.storage.Value
|
||||||
|
import java.nio.file.Files
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertIs
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure unit tests for [DiskIndexAccessPlanner]'s decision logic, independent
|
||||||
|
* of any real [DiskTable]/disk I/O - hand-built [TableSchema]/[Expression]
|
||||||
|
* ASTs (the same shapes [Parser][kestrel.sql.parser.Parser] would produce)
|
||||||
|
* are enough to exercise every branch. End-to-end proof that the chosen plan
|
||||||
|
* is actually *used* (not just correctly chosen) lives in [DiskExecutorTest],
|
||||||
|
* which asserts on [DiskTable]'s access counters.
|
||||||
|
*/
|
||||||
|
class DiskIndexAccessPlannerTest {
|
||||||
|
private val planner = DiskIndexAccessPlanner()
|
||||||
|
|
||||||
|
private val schema = TableSchema(
|
||||||
|
"users",
|
||||||
|
listOf(
|
||||||
|
Column("id", ColumnType.Integer, notNull = true, primaryKey = true),
|
||||||
|
Column("age", ColumnType.Integer, notNull = false, primaryKey = false),
|
||||||
|
Column("name", ColumnType.Text, notNull = false, primaryKey = false)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun col(name: String, qualifier: String? = null) = ColumnExpression(name, qualifier)
|
||||||
|
private fun int(value: Long) = LiteralExpression(Value.IntegerValue(value))
|
||||||
|
private fun text(value: String) = LiteralExpression(Value.TextValue(value))
|
||||||
|
private fun cmp(left: Expression, operator: BinaryOperator, right: Expression) = BinaryExpression(operator, left, right)
|
||||||
|
private fun and(left: Expression, right: Expression) = BinaryExpression(BinaryOperator.And, left, right)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aNullPredicateIsAFullScan() {
|
||||||
|
assertEquals(DiskAccessPlan.FullScan, planner.plan(schema, emptyList(), null))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun equalityOnThePrimaryKeyIsAPointLookup() {
|
||||||
|
val plan = planner.plan(schema, emptyList(), cmp(col("id"), BinaryOperator.Equal, int(5)))
|
||||||
|
val point = assertIs<DiskAccessPlan.PrimaryKeyPoint>(plan)
|
||||||
|
assertEquals(Value.IntegerValue(5), point.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun equalityWithTheLiteralOnTheLeftIsStillAPointLookup() {
|
||||||
|
val plan = planner.plan(schema, emptyList(), cmp(int(5), BinaryOperator.Equal, col("id")))
|
||||||
|
val point = assertIs<DiskAccessPlan.PrimaryKeyPoint>(plan)
|
||||||
|
assertEquals(Value.IntegerValue(5), point.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aSingleSidedRangeOnThePrimaryKeyIsAPrimaryKeyRange() {
|
||||||
|
val plan = planner.plan(schema, emptyList(), cmp(col("id"), BinaryOperator.Greater, int(5)))
|
||||||
|
val range = assertIs<DiskAccessPlan.PrimaryKeyRange>(plan)
|
||||||
|
assertEquals(Value.IntegerValue(5), range.lower)
|
||||||
|
assertEquals(false, range.lowerInclusive)
|
||||||
|
assertNull(range.upper)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aTwoSidedRangeOnThePrimaryKeyCombinesBothBounds() {
|
||||||
|
val predicate = and(
|
||||||
|
cmp(col("id"), BinaryOperator.GreaterEqual, int(2)),
|
||||||
|
cmp(col("id"), BinaryOperator.LessEqual, int(8))
|
||||||
|
)
|
||||||
|
val range = assertIs<DiskAccessPlan.PrimaryKeyRange>(planner.plan(schema, emptyList(), predicate))
|
||||||
|
assertEquals(Value.IntegerValue(2), range.lower)
|
||||||
|
assertEquals(true, range.lowerInclusive)
|
||||||
|
assertEquals(Value.IntegerValue(8), range.upper)
|
||||||
|
assertEquals(true, range.upperInclusive)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aRangeSideWithTheLiteralOnTheLeftIsFlippedCorrectly() {
|
||||||
|
// "5 < id" means id > 5, not id < 5.
|
||||||
|
val plan = planner.plan(schema, emptyList(), cmp(int(5), BinaryOperator.Less, col("id")))
|
||||||
|
val range = assertIs<DiskAccessPlan.PrimaryKeyRange>(plan)
|
||||||
|
assertEquals(Value.IntegerValue(5), range.lower)
|
||||||
|
assertEquals(false, range.lowerInclusive)
|
||||||
|
assertNull(range.upper)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun equalityOnAnUnindexedColumnFallsBackToAFullScan() {
|
||||||
|
assertEquals(DiskAccessPlan.FullScan, planner.plan(schema, emptyList(), cmp(col("name"), BinaryOperator.Equal, LiteralExpression(Value.TextValue("bob")))))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun equalityOnAnIndexedNonPrimaryColumnUsesTheSecondaryIndex() {
|
||||||
|
// A live TransactionalIndex is not needed to prove this branch of the
|
||||||
|
// planner - only the (name, columnName) pairing it dispatches by.
|
||||||
|
val plan = planner.plan(schema, listOf(secondaryHandleStub("idx_age", "age")), cmp(col("age"), BinaryOperator.Equal, int(30)))
|
||||||
|
val secondary = assertIs<DiskAccessPlan.Secondary>(plan)
|
||||||
|
assertEquals("idx_age", secondary.indexName)
|
||||||
|
assertEquals(listOf(Value.IntegerValue(30)), secondary.keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aPrimaryKeyEqualityIsPreferredOverASecondaryIndexEqualityInTheSamePredicate() {
|
||||||
|
val predicate = and(cmp(col("id"), BinaryOperator.Equal, int(2)), cmp(col("age"), BinaryOperator.Equal, int(999)))
|
||||||
|
val plan = planner.plan(schema, listOf(secondaryHandleStub("idx_age", "age")), predicate)
|
||||||
|
val point = assertIs<DiskAccessPlan.PrimaryKeyPoint>(plan)
|
||||||
|
assertEquals(Value.IntegerValue(2), point.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun anOrPredicateIsNotPushedDownAndFallsBackToAFullScan() {
|
||||||
|
// conjuncts() only ever splits on AND; an OR clause is one opaque conjunct that
|
||||||
|
// asColumnPredicate cannot turn into a single column/operator/literal triple.
|
||||||
|
val predicate = BinaryExpression(BinaryOperator.Or, cmp(col("id"), BinaryOperator.Equal, int(1)), cmp(col("id"), BinaryOperator.Equal, int(2)))
|
||||||
|
assertEquals(DiskAccessPlan.FullScan, planner.plan(schema, emptyList(), predicate))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aQualifierMismatchAgainstTheGivenAliasIsRejected() {
|
||||||
|
val plan = planner.plan(schema, emptyList(), cmp(col("id", qualifier = "other"), BinaryOperator.Equal, int(5)), alias = "users")
|
||||||
|
assertEquals(DiskAccessPlan.FullScan, plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aQualifiedColumnMatchesWhenTheAliasAgrees() {
|
||||||
|
val plan = planner.plan(schema, emptyList(), cmp(col("id", qualifier = "u"), BinaryOperator.Equal, int(5)), alias = "u")
|
||||||
|
assertIs<DiskAccessPlan.PrimaryKeyPoint>(plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aQualifiedColumnMatchesWhenNoAliasWasGiven() {
|
||||||
|
// UPDATE/DELETE statements have no FROM alias to compare against, so a caller
|
||||||
|
// passing alias = null (the default) intentionally ignores qualifiers entirely.
|
||||||
|
val plan = planner.plan(schema, emptyList(), cmp(col("id", qualifier = "whatever"), BinaryOperator.Equal, int(5)))
|
||||||
|
assertIs<DiskAccessPlan.PrimaryKeyPoint>(plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val orderLineSchema = TableSchema(
|
||||||
|
"order_lines",
|
||||||
|
listOf(
|
||||||
|
Column("order_id", ColumnType.Integer, notNull = true, primaryKey = false),
|
||||||
|
Column("line_no", ColumnType.Integer, notNull = true, primaryKey = false),
|
||||||
|
Column("sku", ColumnType.Text, notNull = false, primaryKey = false)
|
||||||
|
),
|
||||||
|
compositePrimaryKey = listOf("order_id", "line_no")
|
||||||
|
)
|
||||||
|
|
||||||
Reference in new issue
Block a user