feat: add index access planning and group aggregation
This commit is contained in:
1 parent
9fc6338bbd
commit
def1ceb55d
2 files changed
+212
No files matched your search
@@ -0,0 +1,105 @@
|
||||
package kestrel.engine
|
||||
|
||||
import kestrel.KestrelException
|
||||
import kestrel.sql.ast.BinaryExpression
|
||||
import kestrel.sql.ast.Expression
|
||||
import kestrel.sql.ast.ExpressionProjection
|
||||
import kestrel.sql.ast.FunctionExpression
|
||||
import kestrel.sql.ast.LiteralExpression
|
||||
import kestrel.sql.ast.SelectItem
|
||||
import kestrel.sql.ast.SelectStatement
|
||||
import kestrel.sql.ast.UnaryExpression
|
||||
import kestrel.storage.Value
|
||||
import kestrel.storage.ValueOrdering
|
||||
import kestrel.storage.typeName
|
||||
|
||||
class GroupAggregation(
|
||||
private val resolveIndex: (kestrel.sql.ast.ColumnExpression) -> Int,
|
||||
private val evaluateRow: (Expression, List<Value>) -> Value,
|
||||
private val label: (SelectItem, Int) -> String
|
||||
) {
|
||||
fun aggregate(statement: SelectStatement, orderExpression: Expression?, combined: List<List<Value>>): SelectResult {
|
||||
val groups = LinkedHashMap<List<Value>, MutableList<List<Value>>>()
|
||||
for (row in combined) {
|
||||
val key = statement.groupBy.map { evaluateRow(it, row) }
|
||||
groups.getOrPut(key) { mutableListOf() }.add(row)
|
||||
}
|
||||
var groupRows: List<List<List<Value>>> =
|
||||
if (statement.groupBy.isEmpty() && groups.isEmpty()) listOf(emptyList()) else groups.values.toList()
|
||||
statement.having?.let { having ->
|
||||
groupRows = groupRows.filter { isTrue(evaluateInGroup(having, it)) }
|
||||
}
|
||||
orderExpression?.let { expression ->
|
||||
val comparator = Comparator<List<List<Value>>> { first, second ->
|
||||
ValueOrdering.compare(evaluateInGroup(expression, first), evaluateInGroup(expression, second))
|
||||
}
|
||||
groupRows = groupRows.sortedWith(if (statement.orderBy!!.descending) comparator.reversed() else comparator)
|
||||
}
|
||||
statement.limit?.let {
|
||||
if (it < 0) throw KestrelException("LIMIT must not be negative")
|
||||
groupRows = groupRows.take(it)
|
||||
}
|
||||
val projection = statement.projection as ExpressionProjection
|
||||
val labels = projection.items.mapIndexed { index, item -> label(item, index) }
|
||||
val projected = groupRows.map { group -> projection.items.map { evaluateInGroup(it.expression, group) } }
|
||||
return SelectResult(labels, projected)
|
||||
}
|
||||
|
||||
private fun evaluateInGroup(expression: Expression, group: List<List<Value>>): Value {
|
||||
val substituted = substituteAggregates(expression, group)
|
||||
val resolver = ColumnResolver { columnExpression -> group.first()[resolveIndex(columnExpression)] }
|
||||
return Evaluator(resolver).evaluate(substituted)
|
||||
}
|
||||
|
||||
private fun substituteAggregates(expression: Expression, group: List<List<Value>>): Expression = when (expression) {
|
||||
is FunctionExpression -> LiteralExpression(computeAggregate(expression, group))
|
||||
is BinaryExpression -> BinaryExpression(
|
||||
expression.operator,
|
||||
substituteAggregates(expression.left, group),
|
||||
substituteAggregates(expression.right, group)
|
||||
)
|
||||
is UnaryExpression -> UnaryExpression(expression.operator, substituteAggregates(expression.operand, group))
|
||||
else -> expression
|
||||
}
|
||||
|
||||
private fun computeAggregate(function: FunctionExpression, group: List<List<Value>>): Value {
|
||||
val name = function.name.uppercase()
|
||||
if (name == "COUNT") {
|
||||
if (function.star) return Value.IntegerValue(group.size.toLong())
|
||||
val argument = function.argument ?: return Value.IntegerValue(group.size.toLong())
|
||||
val count = group.count { evaluateRow(argument, it) !is Value.NullValue }
|
||||
return Value.IntegerValue(count.toLong())
|
||||
}
|
||||
val argument = function.argument
|
||||
?: throw KestrelException("aggregate function ${function.name} requires an argument")
|
||||
val values = group.map { evaluateRow(argument, it) }.filter { it !is Value.NullValue }
|
||||
return when (name) {
|
||||
"SUM" -> sum(values)
|
||||
"AVG" -> average(values)
|
||||
"MIN" -> values.minWithOrNull(ValueOrdering) ?: Value.NullValue
|
||||
"MAX" -> values.maxWithOrNull(ValueOrdering) ?: Value.NullValue
|
||||
else -> throw KestrelException("unknown function: ${function.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun sum(values: List<Value>): Value {
|
||||
if (values.isEmpty()) return Value.NullValue
|
||||
if (values.all { it is Value.IntegerValue }) {
|
||||
return Value.IntegerValue(values.sumOf { (it as Value.IntegerValue).value })
|
||||
}
|
||||
return Value.RealValue(values.sumOf { numeric(it) })
|
||||
}
|
||||
|
||||
private fun average(values: List<Value>): Value {
|
||||
if (values.isEmpty()) return Value.NullValue
|
||||
return Value.RealValue(values.sumOf { numeric(it) } / values.size)
|
||||
}
|
||||
|
||||
private fun numeric(value: Value): Double = when (value) {
|
||||
is Value.IntegerValue -> value.value.toDouble()
|
||||
is Value.RealValue -> value.value
|
||||
else -> throw KestrelException("aggregate requires numeric operands but got ${value.typeName}")
|
||||
}
|
||||
|
||||
private fun isTrue(value: Value): Boolean = value is Value.BooleanValue && value.value
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package kestrel.engine
|
||||
|
||||
import kestrel.catalog.Catalog
|
||||
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.Value
|
||||
|
||||
sealed class PrimaryAccess {
|
||||
data object FullScan : PrimaryAccess()
|
||||
class IndexScan(val indexName: String, val description: String, val rowIndices: List<Int>) : PrimaryAccess()
|
||||
}
|
||||
|
||||
class IndexAccessPlanner(private val catalog: Catalog) {
|
||||
fun plan(tableName: String, alias: String, schema: TableSchema, predicate: Expression?): PrimaryAccess {
|
||||
if (predicate == null) return PrimaryAccess.FullScan
|
||||
val byColumn = LinkedHashMap<String, MutableList<Pair<BinaryOperator, Value>>>()
|
||||
for (conjunct in conjuncts(predicate)) {
|
||||
val simple = asColumnPredicate(conjunct, alias, schema) ?: continue
|
||||
byColumn.getOrPut(simple.first) { mutableListOf() }.add(simple.second to simple.third)
|
||||
}
|
||||
for ((columnName, predicates) in byColumn) {
|
||||
val index = catalog.indexOn(tableName, columnName) ?: continue
|
||||
val equal = predicates.firstOrNull { it.first == BinaryOperator.Equal }
|
||||
if (equal != null) {
|
||||
val candidates = index.equalTo(equal.second).sorted()
|
||||
return PrimaryAccess.IndexScan(index.definition.name, "$columnName = ${literalText(equal.second)}", candidates)
|
||||
}
|
||||
var lower: Value? = null
|
||||
var lowerInclusive = false
|
||||
var upper: Value? = null
|
||||
var upperInclusive = false
|
||||
val parts = mutableListOf<String>()
|
||||
for ((operator, literal) in predicates) {
|
||||
when (operator) {
|
||||
BinaryOperator.Greater -> { lower = literal; lowerInclusive = false; parts.add("$columnName > ${literalText(literal)}") }
|
||||
BinaryOperator.GreaterEqual -> { lower = literal; lowerInclusive = true; parts.add("$columnName >= ${literalText(literal)}") }
|
||||
BinaryOperator.Less -> { upper = literal; upperInclusive = false; parts.add("$columnName < ${literalText(literal)}") }
|
||||
BinaryOperator.LessEqual -> { upper = literal; upperInclusive = true; parts.add("$columnName <= ${literalText(literal)}") }
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
if (lower == null && upper == null) continue
|
||||
val candidates = index.rangeOf(lower, lowerInclusive, upper, upperInclusive).sorted()
|
||||
return PrimaryAccess.IndexScan(index.definition.name, parts.joinToString(" AND "), candidates)
|
||||
}
|
||||
return PrimaryAccess.FullScan
|
||||
}
|
||||
|
||||
private fun asColumnPredicate(
|
||||
expression: Expression,
|
||||
alias: String,
|
||||
schema: TableSchema
|
||||
): Triple<String, BinaryOperator, Value>? {
|
||||
if (expression !is BinaryExpression) return null
|
||||
val operator = expression.operator
|
||||
if (operator !in RANGE_OPERATORS) return null
|
||||
val left = expression.left
|
||||
val right = expression.right
|
||||
fun matches(column: ColumnExpression): String? {
|
||||
if (column.qualifier != null && !column.qualifier.equals(alias, ignoreCase = true)) return null
|
||||
return if (schema.columnIndex(column.name) != null) column.name else null
|
||||
}
|
||||
if (left is ColumnExpression && right is LiteralExpression && right.value !is Value.NullValue) {
|
||||
val name = matches(left) ?: return null
|
||||
return Triple(name, operator, right.value)
|
||||
}
|
||||
if (right is ColumnExpression && left is LiteralExpression && left.value !is Value.NullValue) {
|
||||
val name = matches(right) ?: return null
|
||||
return Triple(name, flip(operator), left.value)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun conjuncts(expression: Expression): List<Expression> =
|
||||
if (expression is BinaryExpression && expression.operator == BinaryOperator.And) {
|
||||
conjuncts(expression.left) + conjuncts(expression.right)
|
||||
} else {
|
||||
listOf(expression)
|
||||
}
|
||||
|
||||
private fun flip(operator: BinaryOperator): BinaryOperator = when (operator) {
|
||||
BinaryOperator.Less -> BinaryOperator.Greater
|
||||
BinaryOperator.LessEqual -> BinaryOperator.GreaterEqual
|
||||
BinaryOperator.Greater -> BinaryOperator.Less
|
||||
BinaryOperator.GreaterEqual -> BinaryOperator.LessEqual
|
||||
else -> operator
|
||||
}
|
||||
|
||||
private fun literalText(value: Value): String = when (value) {
|
||||
is Value.IntegerValue -> value.value.toString()
|
||||
is Value.RealValue -> value.value.toString()
|
||||
is Value.TextValue -> "'${value.value}'"
|
||||
is Value.BooleanValue -> value.value.toString()
|
||||
is Value.NullValue -> "NULL"
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val RANGE_OPERATORS = setOf(
|
||||
BinaryOperator.Equal, BinaryOperator.Less, BinaryOperator.LessEqual,
|
||||
BinaryOperator.Greater, BinaryOperator.GreaterEqual
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user