feat(engine): extend GroupAggregation with GroupingSetExpansion
This commit is contained in:
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user