From d47b122397b2a9cd7d800676d1639f14d45b3bf1 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 1 Sep 2026 22:24:47 +0200 Subject: [PATCH] feat(engine): extend GroupAggregation with GroupingSetExpansion --- .../kotlin/kestrel/engine/GroupAggregation.kt | 78 ++++++++++--------- .../kestrel/engine/GroupingSetExpansion.kt | 42 ++++++++++ 2 files changed, 84 insertions(+), 36 deletions(-) create mode 100644 src/main/kotlin/kestrel/engine/GroupingSetExpansion.kt diff --git a/src/main/kotlin/kestrel/engine/GroupAggregation.kt b/src/main/kotlin/kestrel/engine/GroupAggregation.kt index 1a0c69a..b140114 100644 --- a/src/main/kotlin/kestrel/engine/GroupAggregation.kt +++ b/src/main/kotlin/kestrel/engine/GroupAggregation.kt @@ -193,46 +193,52 @@ class GroupAggregation( substituteAggregates(expression.right, 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 } + /** + * `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>): 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 { - 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 { - 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}") + val filtered = function.filter?.let { predicate -> group.filter { isTrue(evaluateRow(predicate, it)) } } ?: group + return AggregateFunctions.compute( + function.name, function.argument, function.star, filtered, evaluateRow, + function.separator, function.orderBy, function.distinct, declaredCollationOf + ) } private fun isTrue(value: Value): Boolean = value is Value.BooleanValue && value.value diff --git a/src/main/kotlin/kestrel/engine/GroupingSetExpansion.kt b/src/main/kotlin/kestrel/engine/GroupingSetExpansion.kt new file mode 100644 index 0000000..89c2653 --- /dev/null +++ b/src/main/kotlin/kestrel/engine/GroupingSetExpansion.kt @@ -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): List> = 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): List> = + (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): List> { + 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 } + } + } +}