feat(engine): extend GroupAggregation with GroupingSetExpansion

This commit is contained in:
dualfroz committed 2026-09-01 22:24:47 +02:00
1 parent 4653c74cf9
commit d47b122397
2 files changed
+84 -36

No files matched your search

@@ -193,46 +193,52 @@ class GroupAggregation(
substituteAggregates(expression.right, group) substituteAggregates(expression.right, group)
) )
is UnaryExpression -> UnaryExpression(expression.operator, substituteAggregates(expression.operand, group)) is UnaryExpression -> UnaryExpression(expression.operator, substituteAggregates(expression.operand, group))
is CallExpression -> CallExpression(expression.name, expression.arguments.map { substituteAggregates(it, group) })
is IsNullExpression -> IsNullExpression(substituteAggregates(expression.operand, group), expression.negated)
is InExpression -> InExpression(
substituteAggregates(expression.operand, group),
expression.values.map { substituteAggregates(it, group) },
expression.negated
)
is BetweenExpression -> BetweenExpression(
substituteAggregates(expression.operand, group),
substituteAggregates(expression.low, group),
substituteAggregates(expression.high, group),
expression.negated
)
is LikeExpression -> LikeExpression(
substituteAggregates(expression.operand, group),
substituteAggregates(expression.pattern, group),
expression.negated
)
is CaseExpression -> CaseExpression(
expression.operand?.let { substituteAggregates(it, group) },
expression.branches.map {
CaseBranch(substituteAggregates(it.condition, group), substituteAggregates(it.result, group))
},
expression.elseExpression?.let { substituteAggregates(it, group) }
)
is CastExpression -> CastExpression(
substituteAggregates(expression.operand, group),
expression.targetType,
expression.precision,
expression.scale
)
else -> expression else -> expression
} }
/**
* `FILTER (WHERE ...)` narrows the rows this one aggregate call sees to those matching its own
* predicate, evaluated per source row exactly like a `WHERE` clause would be - independent of
* every other aggregate in the same SELECT list, which each see [group] unfiltered (or filtered by
* their own, different, FILTER clause) rather than this one's rows removed for everybody.
*/
private fun computeAggregate(function: FunctionExpression, group: List<List<Value>>): Value { private fun computeAggregate(function: FunctionExpression, group: List<List<Value>>): Value {
val name = function.name.uppercase() val filtered = function.filter?.let { predicate -> group.filter { isTrue(evaluateRow(predicate, it)) } } ?: group
if (name == "COUNT") { return AggregateFunctions.compute(
if (function.star) return Value.IntegerValue(group.size.toLong()) function.name, function.argument, function.star, filtered, evaluateRow,
val argument = function.argument ?: return Value.IntegerValue(group.size.toLong()) function.separator, function.orderBy, function.distinct, declaredCollationOf
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 private fun isTrue(value: Value): Boolean = value is Value.BooleanValue && value.value
@@ -0,0 +1,42 @@
package kestrel.engine
import kestrel.KestrelException
import kestrel.sql.ast.CubeSpec
import kestrel.sql.ast.Expression
import kestrel.sql.ast.GroupingSetsSpec
import kestrel.sql.ast.GroupingSpec
import kestrel.sql.ast.RollupSpec
/**
* Turns a [GroupingSpec] into the concrete list of grouping sets [GroupAggregation] groups by - one
* full pass over the source rows per set, later unioned together. `null` (an ordinary `GROUP BY a, b`
* or no `GROUP BY` at all) is exactly one implicit set, [groupBy] itself - this is what lets
* [GroupAggregation] treat the plain case as a special case of the general one instead of a separate
* code path.
*/
object GroupingSetExpansion {
/** CUBE(e1..en) is 2^n sets; capped well short of overflow or a runaway allocation from a typo'd column list. */
private const val MAX_CUBE_COLUMNS = 16
fun setsFor(spec: GroupingSpec?, groupBy: List<Expression>): List<List<Expression>> = when (spec) {
null -> listOf(groupBy)
is RollupSpec -> rollupSets(spec.expressions)
is CubeSpec -> cubeSets(spec.expressions)
is GroupingSetsSpec -> spec.sets
}
/** ROLLUP(e1, e2, e3) -> {e1,e2,e3}, {e1,e2}, {e1}, {} - each prefix of the declared order, longest first. */
private fun rollupSets(expressions: List<Expression>): List<List<Expression>> =
(expressions.size downTo 0).map { expressions.subList(0, it) }
/** CUBE(e1, e2, e3) -> every one of the 2^3 subsets, largest first; membership of expression i is bit i of a descending bitmask. */
private fun cubeSets(expressions: List<Expression>): List<List<Expression>> {
if (expressions.size > MAX_CUBE_COLUMNS) {
throw KestrelException("CUBE supports at most $MAX_CUBE_COLUMNS expressions")
}
val subsetCount = 1 shl expressions.size
return (subsetCount - 1 downTo 0).map { mask ->
expressions.filterIndexed { index, _ -> (mask shr index) and 1 == 1 }
}
}
}