feat(sql): add containsAggregate and validateDistinctOn to DiskExecutor
This commit is contained in:
1 parent
bb57f5ac91
commit
de2cc8854d
1 file changed
+173
@@ -3187,3 +3187,176 @@ class DiskExecutor(private val catalog: DiskCatalog) : SequenceAccess {
|
|||||||
is ExpressionProjection -> projection.items.any { containsAggregate(it.expression) }
|
is ExpressionProjection -> projection.items.any { containsAggregate(it.expression) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun containsAggregate(expression: Expression): Boolean = when (expression) {
|
||||||
|
is FunctionExpression -> true
|
||||||
|
is BinaryExpression -> containsAggregate(expression.left) || containsAggregate(expression.right)
|
||||||
|
is UnaryExpression -> containsAggregate(expression.operand)
|
||||||
|
is CallExpression -> expression.arguments.any { containsAggregate(it) }
|
||||||
|
is IsNullExpression -> containsAggregate(expression.operand)
|
||||||
|
is InExpression -> containsAggregate(expression.operand) || expression.values.any { containsAggregate(it) }
|
||||||
|
is BetweenExpression ->
|
||||||
|
containsAggregate(expression.operand) || containsAggregate(expression.low) || containsAggregate(expression.high)
|
||||||
|
is LikeExpression -> containsAggregate(expression.operand) || containsAggregate(expression.pattern)
|
||||||
|
is CaseExpression ->
|
||||||
|
(expression.operand?.let { containsAggregate(it) } ?: false) ||
|
||||||
|
expression.branches.any { containsAggregate(it.condition) || containsAggregate(it.result) } ||
|
||||||
|
(expression.elseExpression?.let { containsAggregate(it) } ?: false)
|
||||||
|
is CastExpression -> containsAggregate(expression.operand)
|
||||||
|
is CollateExpression -> containsAggregate(expression.operand)
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** See [kestrel.engine.SelectRunner]'s `Plan.validateDistinctOn` for why both checks exist. */
|
||||||
|
private fun validateDistinctOn(statement: SelectStatement, orderKeys: List<OrderKey>, aggregated: Boolean) {
|
||||||
|
if (aggregated) {
|
||||||
|
throw DiskExecutorException("DISTINCT ON cannot be combined with GROUP BY or aggregate functions")
|
||||||
|
}
|
||||||
|
if (statement.orderBy != null) {
|
||||||
|
val leading = orderKeys.take(statement.distinctOn.size).map { it.expression }
|
||||||
|
if (leading != statement.distinctOn) {
|
||||||
|
throw DiskExecutorException("SELECT DISTINCT ON expressions must match initial ORDER BY expressions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateAggregation(statement: SelectStatement, orderKeys: List<OrderKey>, sources: List<JoinSource>) {
|
||||||
|
val groupColumns = statement.groupBy.filterIsInstance<ColumnExpression>()
|
||||||
|
.map { resolveIndex(sources, it) }.toSet()
|
||||||
|
if (statement.projection is ExpressionProjection) {
|
||||||
|
statement.projection.items.forEach { requireGrouped(it.expression, statement.groupBy, groupColumns, sources) }
|
||||||
|
}
|
||||||
|
statement.having?.let { requireGrouped(it, statement.groupBy, groupColumns, sources) }
|
||||||
|
orderKeys.forEach { requireGrouped(it.expression, statement.groupBy, groupColumns, sources) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requireGrouped(expression: Expression, groupBy: List<Expression>, groupColumns: Set<Int>, sources: List<JoinSource>) {
|
||||||
|
if (expression in groupBy) return
|
||||||
|
when (expression) {
|
||||||
|
is FunctionExpression -> Unit
|
||||||
|
is ColumnExpression -> if (resolveIndex(sources, expression) !in groupColumns) {
|
||||||
|
throw DiskExecutorException(
|
||||||
|
"column ${expression.name} must appear in GROUP BY or be used in an aggregate function"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is BinaryExpression -> {
|
||||||
|
requireGrouped(expression.left, groupBy, groupColumns, sources)
|
||||||
|
requireGrouped(expression.right, groupBy, groupColumns, sources)
|
||||||
|
}
|
||||||
|
is UnaryExpression -> requireGrouped(expression.operand, groupBy, groupColumns, sources)
|
||||||
|
is CallExpression -> expression.arguments.forEach { requireGrouped(it, groupBy, groupColumns, sources) }
|
||||||
|
is IsNullExpression -> requireGrouped(expression.operand, groupBy, groupColumns, sources)
|
||||||
|
is InExpression -> {
|
||||||
|
requireGrouped(expression.operand, groupBy, groupColumns, sources)
|
||||||
|
expression.values.forEach { requireGrouped(it, groupBy, groupColumns, sources) }
|
||||||
|
}
|
||||||
|
is BetweenExpression -> {
|
||||||
|
requireGrouped(expression.operand, groupBy, groupColumns, sources)
|
||||||
|
requireGrouped(expression.low, groupBy, groupColumns, sources)
|
||||||
|
requireGrouped(expression.high, groupBy, groupColumns, sources)
|
||||||
|
}
|
||||||
|
is LikeExpression -> {
|
||||||
|
requireGrouped(expression.operand, groupBy, groupColumns, sources)
|
||||||
|
requireGrouped(expression.pattern, groupBy, groupColumns, sources)
|
||||||
|
}
|
||||||
|
is CaseExpression -> {
|
||||||
|
expression.operand?.let { requireGrouped(it, groupBy, groupColumns, sources) }
|
||||||
|
expression.branches.forEach {
|
||||||
|
requireGrouped(it.condition, groupBy, groupColumns, sources)
|
||||||
|
requireGrouped(it.result, groupBy, groupColumns, sources)
|
||||||
|
}
|
||||||
|
expression.elseExpression?.let { requireGrouped(it, groupBy, groupColumns, sources) }
|
||||||
|
}
|
||||||
|
is CastExpression -> requireGrouped(expression.operand, groupBy, groupColumns, sources)
|
||||||
|
is CollateExpression -> requireGrouped(expression.operand, groupBy, groupColumns, sources)
|
||||||
|
is ScalarSubqueryExpression -> Unit
|
||||||
|
is ExistsExpression -> Unit
|
||||||
|
is InSubqueryExpression -> requireGrouped(expression.operand, groupBy, groupColumns, sources)
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Window functions are only legal in the SELECT list and ORDER BY - never WHERE/GROUP BY/HAVING/ON. */
|
||||||
|
private fun containsWindowFunction(expression: Expression): Boolean = when (expression) {
|
||||||
|
is WindowExpression -> true
|
||||||
|
is BinaryExpression -> containsWindowFunction(expression.left) || containsWindowFunction(expression.right)
|
||||||
|
is UnaryExpression -> containsWindowFunction(expression.operand)
|
||||||
|
is CallExpression -> expression.arguments.any { containsWindowFunction(it) }
|
||||||
|
is IsNullExpression -> containsWindowFunction(expression.operand)
|
||||||
|
is InExpression -> containsWindowFunction(expression.operand) || expression.values.any { containsWindowFunction(it) }
|
||||||
|
is BetweenExpression ->
|
||||||
|
containsWindowFunction(expression.operand) || containsWindowFunction(expression.low) || containsWindowFunction(expression.high)
|
||||||
|
is LikeExpression -> containsWindowFunction(expression.operand) || containsWindowFunction(expression.pattern)
|
||||||
|
is CaseExpression ->
|
||||||
|
(expression.operand?.let { containsWindowFunction(it) } ?: false) ||
|
||||||
|
expression.branches.any { containsWindowFunction(it.condition) || containsWindowFunction(it.result) } ||
|
||||||
|
(expression.elseExpression?.let { containsWindowFunction(it) } ?: false)
|
||||||
|
is CastExpression -> containsWindowFunction(expression.operand)
|
||||||
|
is CollateExpression -> containsWindowFunction(expression.operand)
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rejectWindowFunction(expression: Expression, clause: String) {
|
||||||
|
if (containsWindowFunction(expression)) throw DiskExecutorException("window functions are not allowed in $clause")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Every distinct window-function call reachable from [expression] (nested inside arithmetic, CASE, ...). */
|
||||||
|
private fun collectWindowExpressions(expression: Expression): List<WindowExpression> = when (expression) {
|
||||||
|
is WindowExpression -> listOf(expression)
|
||||||
|
is BinaryExpression -> collectWindowExpressions(expression.left) + collectWindowExpressions(expression.right)
|
||||||
|
is UnaryExpression -> collectWindowExpressions(expression.operand)
|
||||||
|
is CallExpression -> expression.arguments.flatMap { collectWindowExpressions(it) }
|
||||||
|
is IsNullExpression -> collectWindowExpressions(expression.operand)
|
||||||
|
is InExpression -> collectWindowExpressions(expression.operand) + expression.values.flatMap { collectWindowExpressions(it) }
|
||||||
|
is BetweenExpression ->
|
||||||
|
collectWindowExpressions(expression.operand) + collectWindowExpressions(expression.low) + collectWindowExpressions(expression.high)
|
||||||
|
is LikeExpression -> collectWindowExpressions(expression.operand) + collectWindowExpressions(expression.pattern)
|
||||||
|
is CaseExpression ->
|
||||||
|
(expression.operand?.let { collectWindowExpressions(it) } ?: emptyList()) +
|
||||||
|
expression.branches.flatMap { collectWindowExpressions(it.condition) + collectWindowExpressions(it.result) } +
|
||||||
|
(expression.elseExpression?.let { collectWindowExpressions(it) } ?: emptyList())
|
||||||
|
is CastExpression -> collectWindowExpressions(expression.operand)
|
||||||
|
is CollateExpression -> collectWindowExpressions(expression.operand)
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every distinct window-function call in the SELECT list or (post alias-resolution) ORDER BY -
|
||||||
|
* the only two clauses where one is allowed, mirroring [kestrel.engine.SelectRunner]'s own
|
||||||
|
* `windowExpressions` property.
|
||||||
|
*/
|
||||||
|
private fun collectWindowExpressions(projection: Projection, orderKeys: List<OrderKey>): List<WindowExpression> {
|
||||||
|
val projectionWindows = (projection as? ExpressionProjection)?.items?.flatMap { collectWindowExpressions(it.expression) } ?: emptyList()
|
||||||
|
val orderWindows = orderKeys.flatMap { collectWindowExpressions(it.expression) }
|
||||||
|
return (projectionWindows + orderWindows).distinct()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Window functions are rejected outright alongside GROUP BY/aggregates ([aggregated]) -
|
||||||
|
* windowing over an already-grouped result is valid SQL but requires running
|
||||||
|
* [GroupAggregation] first and then a second pass over its output rows, which this executor
|
||||||
|
* does not implement (same scope cut as [kestrel.engine.SelectRunner]). RANK/DENSE_RANK need an
|
||||||
|
* explicit tie-break (unlike ROW_NUMBER, which falls back to partition arrival order when OVER
|
||||||
|
* has no ORDER BY) and nesting one window function inside another's argument/PARTITION BY/
|
||||||
|
* ORDER BY/frame offset is invalid SQL in the first place.
|
||||||
|
*/
|
||||||
|
private fun validateWindows(windowExpressions: List<WindowExpression>, aggregated: Boolean) {
|
||||||
|
if (aggregated) {
|
||||||
|
throw DiskExecutorException("window functions cannot be combined with GROUP BY or aggregate functions in the same query")
|
||||||
|
}
|
||||||
|
for (window in windowExpressions) {
|
||||||
|
if ((window.kind == WindowFunctionKind.Rank || window.kind == WindowFunctionKind.DenseRank) && window.orderBy.isEmpty()) {
|
||||||
|
throw DiskExecutorException("${window.name}() OVER (...) requires an ORDER BY clause")
|
||||||
|
}
|
||||||
|
if (window.frame != null && window.kind !in FRAME_CAPABLE_KINDS) {
|
||||||
|
throw DiskExecutorException("${window.name}() cannot be used with a ROWS/RANGE/GROUPS frame clause")
|
||||||
|
}
|
||||||
|
val nested = listOfNotNull(
|
||||||
|
window.argument, window.offset, window.defaultValue,
|
||||||
|
window.frame?.start?.offset, window.frame?.end?.offset
|
||||||
|
) + window.partitionBy + window.orderBy.map { it.expression }
|
||||||
|
if (nested.any { containsWindowFunction(it) }) {
|
||||||
|
throw DiskExecutorException("window functions cannot be nested")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user