feat(sql): add declaredSetOperationCollation
This commit is contained in:
1 parent
e012659bde
commit
b67ac3386a
1 file changed
+174
@@ -2399,3 +2399,177 @@ class DiskExecutor(private val catalog: DiskCatalog) : SequenceAccess {
|
||||
return SelectResult(left.columns, rows)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors [kestrel.engine.SelectRunner]'s own `declaredSetOperationCollation` 1:1 (separate
|
||||
* copy, historical reasons like the rest of this class): the declared `COLLATE` of a set
|
||||
* operation's combined output column, traced back to a real table column in [statement]'s LEFT
|
||||
* branch only, resolving only for a plain `SELECT ... FROM realTable [JOIN realTable ...]` with
|
||||
* no CTE, view, or derived table anywhere in its FROM clause. Everything else - an expression,
|
||||
* a nested set operation, a subquery source - reports no declared collation, same as before this
|
||||
* function existed.
|
||||
*/
|
||||
private fun declaredSetOperationCollation(statement: SetOperationStatement, columns: List<String>): (ColumnExpression) -> Collation? =
|
||||
(statement.left as? SelectStatement)?.let { declaredCollationForSelectOutput(it, columns) } ?: { null }
|
||||
|
||||
/**
|
||||
* Mirrors [kestrel.engine.SelectRunner]'s own `declaredCollationForSelectOutput` 1:1 (separate
|
||||
* copy, historical reasons like the rest of this class): factored out of
|
||||
* [declaredSetOperationCollation] so [declaredRecursiveCteCollation] can apply the identical
|
||||
* "plain SELECT over real tables" resolution to a recursive CTE's own anchor (base) term.
|
||||
*/
|
||||
private fun declaredCollationForSelectOutput(select: SelectStatement, columns: List<String>): (ColumnExpression) -> Collation? {
|
||||
val items = listOf(select.from) + select.joins.map { it.from }
|
||||
val sources = items.map { item ->
|
||||
(item as? TableSource)?.let { source -> catalog.tableOrNull(source.name)?.schema?.let { (source.alias ?: source.name) to it } }
|
||||
}
|
||||
if (sources.any { it == null }) return { null }
|
||||
val resolvedSources = sources.filterNotNull()
|
||||
fun columnCollation(expression: ColumnExpression): Collation? {
|
||||
val matches = resolvedSources
|
||||
.filter { (alias, _) -> expression.qualifier == null || alias.equals(expression.qualifier, ignoreCase = true) }
|
||||
.mapNotNull { (_, schema) -> schema.column(expression.name) }
|
||||
return matches.singleOrNull()?.collation
|
||||
}
|
||||
return when (val projection = select.projection) {
|
||||
is ExpressionProjection -> outputExpr@{ outputExpr ->
|
||||
val index = columns.indexOfFirst { it.equals(outputExpr.name, ignoreCase = true) }
|
||||
val itemExpression = index.takeIf { it in projection.items.indices }?.let { projection.items[it].expression }
|
||||
(itemExpression as? ColumnExpression)?.let(::columnCollation)
|
||||
}
|
||||
is StarProjection -> outputExpr@{ outputExpr ->
|
||||
val index = columns.indexOfFirst { it.equals(outputExpr.name, ignoreCase = true) }
|
||||
if (index < 0) return@outputExpr null
|
||||
var remaining = index
|
||||
for ((_, schema) in resolvedSources) {
|
||||
if (remaining < schema.columns.size) return@outputExpr schema.columns[remaining].collation
|
||||
remaining -= schema.columns.size
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Mirrors [kestrel.engine.SelectRunner]'s own `declaredRecursiveCteCollation` 1:1 (separate copy, historical reasons like the rest of this class). */
|
||||
private fun declaredRecursiveCteCollation(base: QueryStatement, columns: List<String>): (ColumnExpression) -> Collation? =
|
||||
(base as? SelectStatement)?.let { declaredCollationForSelectOutput(it, columns) } ?: { null }
|
||||
|
||||
private fun multisetFilter(
|
||||
left: List<List<Value>>,
|
||||
right: List<List<Value>>,
|
||||
keepOnMatch: Boolean,
|
||||
keyOf: (List<Value>) -> List<Value>
|
||||
): List<List<Value>> {
|
||||
val remainingInRight = right.groupingBy(keyOf).eachCount().toMutableMap()
|
||||
return left.filter { row ->
|
||||
val key = keyOf(row)
|
||||
val remaining = remainingInRight[key] ?: 0
|
||||
if (remaining > 0) {
|
||||
remainingInRight[key] = remaining - 1
|
||||
keepOnMatch
|
||||
} else {
|
||||
!keepOnMatch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun multiKeyComparator(
|
||||
keys: List<OrderKey>,
|
||||
evaluate: (Expression, List<Value>) -> Value,
|
||||
declaredCollationOf: (ColumnExpression) -> Collation? = { null }
|
||||
): Comparator<List<Value>> =
|
||||
Comparator { first, second ->
|
||||
var result = 0
|
||||
for (key in keys) {
|
||||
val comparison = compareOrderKeyValues(
|
||||
evaluate(key.expression, first), evaluate(key.expression, second), key, declaredCollationOf
|
||||
)
|
||||
if (comparison != 0) {
|
||||
result = comparison
|
||||
break
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/** Resolves ORDER BY columns of a combined UNION/INTERSECT/EXCEPT result purely by output column name (never qualified). */
|
||||
private fun evaluateOutputRow(columns: List<String>, expression: Expression, row: List<Value>, txn: Long): Value {
|
||||
val resolver = ColumnResolver { column ->
|
||||
if (column.qualifier != null) throw DiskExecutorException("no such column: ${column.qualifier}.${column.name}")
|
||||
val index = columns.indexOfFirst { it.equals(column.name, ignoreCase = true) }
|
||||
if (index == -1) throw DiskExecutorException("no such column: ${column.name}")
|
||||
row[index]
|
||||
}
|
||||
return Evaluator(resolver, subqueryRunnerFor(txn)).evaluate(expression)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes every CTE of [query] onto [activeCtes] (in definition order, so a later CTE may
|
||||
* reference an earlier one, mirroring [kestrel.engine.SelectRunner.runWith]), runs [query]'s
|
||||
* body, then pops them back off - a CTE is scoped to the single query that defines it, unlike a
|
||||
* VIEW, which lives in the catalog.
|
||||
*/
|
||||
private fun runWith(query: WithQuery, txn: Long, outer: ColumnResolver?): SelectResult {
|
||||
var pushed = 0
|
||||
try {
|
||||
for (cte in query.ctes) {
|
||||
activeCtes.add(buildCteScope(query.recursive, cte, txn))
|
||||
pushed++
|
||||
}
|
||||
return runQuery(query.body, txn, outer)
|
||||
} finally {
|
||||
repeat(pushed) { activeCtes.removeAt(activeCtes.size - 1) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Materializes one CTE's rows. A non-recursive CTE just runs its query once. A recursive one
|
||||
* (`WITH RECURSIVE name AS (base UNION [ALL] recursiveTerm)`, split by [splitRecursiveCte])
|
||||
* repeatedly re-runs [RecursiveCtePlan.recursiveTerm] with [CteScope.rows] set to only the
|
||||
* *previous iteration's new rows* (the standard fixed-point working-set semantics, not the
|
||||
* whole accumulated set - mirrors [kestrel.engine.SelectRunner.buildCteScope] exactly, including
|
||||
* the [MAX_RECURSION_ITERATIONS] safety cap against a query that never reaches a fixed point).
|
||||
*/
|
||||
private fun buildCteScope(recursive: Boolean, cte: CommonTableExpression, txn: Long): CteScope {
|
||||
val recursivePlan = if (recursive) splitRecursiveCte(cte) else null
|
||||
if (recursivePlan == null) {
|
||||
val result = runQuery(cte.query, txn, outer = null)
|
||||
return CteScope(cte.name, cteSchema(cte, result), result.rows)
|
||||
}
|
||||
val (base, operator, recursiveTerm) = recursivePlan
|
||||
val baseResult = runQuery(base, txn, outer = null)
|
||||
val schema = cteSchema(cte, baseResult)
|
||||
val scope = CteScope(cte.name, schema, baseResult.rows)
|
||||
val declaredCollationOf = declaredRecursiveCteCollation(base, baseResult.columns)
|
||||
val outputExpressions = baseResult.columns.map { ColumnExpression(it) }
|
||||
val seenKeys = if (operator == SetOperator.Union)
|
||||
baseResult.rows.mapTo(HashSet()) { collationFoldedKey(it, outputExpressions, declaredCollationOf) }
|
||||
else null
|
||||
activeCtes.add(scope)
|
||||
try {
|
||||
val accumulated = mutableListOf<List<Value>>()
|
||||
accumulated.addAll(baseResult.rows)
|
||||
var working = baseResult.rows
|
||||
var iterations = 0
|
||||
while (working.isNotEmpty()) {
|
||||
iterations++
|
||||
if (iterations > MAX_RECURSION_ITERATIONS) {
|
||||
throw DiskExecutorException(
|
||||
"recursive common table expression '${cte.name}' did not terminate within $MAX_RECURSION_ITERATIONS iterations"
|
||||
)
|
||||
}
|
||||
scope.rows = working
|
||||
var newRows = runQuery(recursiveTerm, txn, outer = null).rows
|
||||
if (operator == SetOperator.Union) {
|
||||
newRows = newRows.filter { candidate -> seenKeys!!.add(collationFoldedKey(candidate, outputExpressions, declaredCollationOf)) }
|
||||
}
|
||||
if (newRows.isEmpty()) break
|
||||
accumulated.addAll(newRows)
|
||||
working = newRows
|
||||
}
|
||||
scope.rows = accumulated
|
||||
} finally {
|
||||
activeCtes.removeAt(activeCtes.size - 1)
|
||||
}
|
||||
return scope
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user