feat(engine): extend SelectRunner with resolveIntegerFrameOffset
This commit is contained in:
1 parent
16f7b062e1
commit
98138f80e9
1 file changed
+173
@@ -963,6 +963,179 @@ class SelectRunner(private val catalog: Catalog) {
|
|||||||
window: WindowExpression,
|
window: WindowExpression,
|
||||||
position: Int,
|
position: Int,
|
||||||
ordered: List<Int>,
|
ordered: List<Int>,
|
||||||
|
combined: List<List<Value>>,
|
||||||
|
isStart: Boolean
|
||||||
|
): Int = when (bound.type) {
|
||||||
|
FrameBoundType.UnboundedPreceding -> 0
|
||||||
|
FrameBoundType.UnboundedFollowing -> ordered.size - 1
|
||||||
|
FrameBoundType.CurrentRow ->
|
||||||
|
if (frame.unit == FrameUnit.Rows) position else peerBoundary(window.orderBy, position, ordered, combined, isStart)
|
||||||
|
FrameBoundType.Preceding ->
|
||||||
|
if (frame.unit == FrameUnit.Rows) position - resolveIntegerFrameOffset(bound, combined[ordered[position]], "ROWS")
|
||||||
|
else rangeBoundary(bound, window.orderBy, position, ordered, combined, isStart, subtract = true)
|
||||||
|
FrameBoundType.Following ->
|
||||||
|
if (frame.unit == FrameUnit.Rows) position + resolveIntegerFrameOffset(bound, combined[ordered[position]], "ROWS")
|
||||||
|
else rangeBoundary(bound, window.orderBy, position, ordered, combined, isStart, subtract = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveIntegerFrameOffset(bound: FrameBound, row: List<Value>, unit: String): Int {
|
||||||
|
val value = evaluateRow(bound.offset!!, row)
|
||||||
|
val amount = (value as? Value.IntegerValue)?.value
|
||||||
|
?: throw KestrelException("$unit frame offset must be an integer")
|
||||||
|
if (amount < 0) throw KestrelException("$unit frame offset must not be negative")
|
||||||
|
return amount.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `GROUPS BETWEEN <bound> AND <bound>` counts whole peer groups (rows tied on the OVER's
|
||||||
|
* ORDER BY) rather than physical rows ([FrameUnit.Rows]) or distance in a value
|
||||||
|
* ([FrameUnit.Range]) - `1 PRECEDING` means "the peer group immediately before the current
|
||||||
|
* row's own peer group", however many rows that group has. Resolved entirely in group-index
|
||||||
|
* space (mirroring exactly how [frameRange] resolves ROWS/RANGE in row-index space, including
|
||||||
|
* the same one-sided-clamp trick for correctly detecting an out-of-partition frame as empty
|
||||||
|
* rather than accidentally clamping it back into range), then translated to a row [IntRange]
|
||||||
|
* only once both bounds land on a valid group. Unlike [rangeBoundary], this places no
|
||||||
|
* restriction on the OVER's ORDER BY - zero, one, or many columns of any type all define peer
|
||||||
|
* groups equally well, since [peerGroupRanges] only ever compares them for equality.
|
||||||
|
*/
|
||||||
|
private fun groupsFrameRange(
|
||||||
|
frame: WindowFrame,
|
||||||
|
orderBy: List<OrderKey>,
|
||||||
|
position: Int,
|
||||||
|
ordered: List<Int>,
|
||||||
|
combined: List<List<Value>>
|
||||||
|
): IntRange {
|
||||||
|
val groups = peerGroupRanges(orderBy, ordered, combined)
|
||||||
|
val currentGroup = groups.indexOfFirst { position in it }
|
||||||
|
val row = combined[ordered[position]]
|
||||||
|
val startGroup = resolveGroupBound(frame.start, currentGroup, groups.size, row).coerceAtLeast(0)
|
||||||
|
val endGroup = resolveGroupBound(frame.end, currentGroup, groups.size, row).coerceAtMost(groups.size - 1)
|
||||||
|
return if (startGroup > endGroup) IntRange.EMPTY else groups[startGroup].first..groups[endGroup].last
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveGroupBound(bound: FrameBound, currentGroup: Int, groupCount: Int, row: List<Value>): Int = when (bound.type) {
|
||||||
|
FrameBoundType.UnboundedPreceding -> 0
|
||||||
|
FrameBoundType.UnboundedFollowing -> groupCount - 1
|
||||||
|
FrameBoundType.CurrentRow -> currentGroup
|
||||||
|
FrameBoundType.Preceding -> currentGroup - resolveIntegerFrameOffset(bound, row, "GROUPS")
|
||||||
|
FrameBoundType.Following -> currentGroup + resolveIntegerFrameOffset(bound, row, "GROUPS")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `CURRENT ROW` in a RANGE frame does not mean "just this row" (that is what ROWS is for) - it
|
||||||
|
* means the boundary of the current row's peer group (every row tied with it on the OVER's
|
||||||
|
* ORDER BY, or the whole partition when there is no ORDER BY). [isStart] picks which edge of
|
||||||
|
* that peer group to return.
|
||||||
|
*/
|
||||||
|
private fun peerBoundary(orderBy: List<OrderKey>, position: Int, ordered: List<Int>, combined: List<List<Value>>, isStart: Boolean): Int {
|
||||||
|
if (orderBy.isEmpty()) return if (isStart) 0 else ordered.size - 1
|
||||||
|
fun keyAt(index: Int) = orderBy.map { evaluateRow(it.expression, combined[ordered[index]]) }
|
||||||
|
val key = keyAt(position)
|
||||||
|
var boundary = position
|
||||||
|
if (isStart) {
|
||||||
|
while (boundary > 0 && keyAt(boundary - 1) == key) boundary--
|
||||||
|
} else {
|
||||||
|
while (boundary < ordered.size - 1 && keyAt(boundary + 1) == key) boundary++
|
||||||
|
}
|
||||||
|
return boundary
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `<value> PRECEDING`/`<value> FOLLOWING` in a RANGE frame measure distance in the value of a
|
||||||
|
* single numeric or DATE ORDER BY column rather than a row count, so the frame can span a
|
||||||
|
* different number of physical rows for every current row (e.g. `RANGE BETWEEN 5 PRECEDING AND
|
||||||
|
* 5 FOLLOWING` on `ORDER BY amount` widens or narrows with how densely `amount` is populated
|
||||||
|
* nearby). Binary search works because [ordered] is already sorted on this column - ascending
|
||||||
|
* in "logical" terms via [rangeLogicalValue], which folds DESC into a sign flip so the same
|
||||||
|
* `>=`/`<=` comparisons apply regardless of sort direction.
|
||||||
|
*/
|
||||||
|
private fun rangeBoundary(
|
||||||
|
bound: FrameBound,
|
||||||
|
orderBy: List<OrderKey>,
|
||||||
|
position: Int,
|
||||||
|
ordered: List<Int>,
|
||||||
|
combined: List<List<Value>>,
|
||||||
|
isStart: Boolean,
|
||||||
|
subtract: Boolean
|
||||||
|
): Int {
|
||||||
|
if (orderBy.size != 1) {
|
||||||
|
throw KestrelException("a RANGE frame with a PRECEDING/FOLLOWING bound requires exactly one ORDER BY column")
|
||||||
|
}
|
||||||
|
val key = orderBy.single()
|
||||||
|
val current = rangeLogicalValue(key, combined[ordered[position]])
|
||||||
|
val offset = resolveRangeFrameOffset(bound, combined[ordered[position]])
|
||||||
|
val threshold = if (subtract) current - offset else current + offset
|
||||||
|
return if (isStart) {
|
||||||
|
rangeLowerBound(ordered, combined, key, threshold)
|
||||||
|
} else {
|
||||||
|
rangeUpperBound(ordered, combined, key, threshold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rangeLowerBound(ordered: List<Int>, combined: List<List<Value>>, key: OrderKey, threshold: BigDecimal): Int {
|
||||||
|
var low = 0
|
||||||
|
var high = ordered.size
|
||||||
|
while (low < high) {
|
||||||
|
val mid = (low + high) / 2
|
||||||
|
if (rangeLogicalValue(key, combined[ordered[mid]]) >= threshold) high = mid else low = mid + 1
|
||||||
|
}
|
||||||
|
return low
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rangeUpperBound(ordered: List<Int>, combined: List<List<Value>>, key: OrderKey, threshold: BigDecimal): Int {
|
||||||
|
var low = 0
|
||||||
|
var high = ordered.size
|
||||||
|
while (low < high) {
|
||||||
|
val mid = (low + high) / 2
|
||||||
|
if (rangeLogicalValue(key, combined[ordered[mid]]) <= threshold) low = mid + 1 else high = mid
|
||||||
|
}
|
||||||
|
return low - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rangeLogicalValue(key: OrderKey, row: List<Value>): BigDecimal {
|
||||||
|
val magnitude = rangeMagnitude(evaluateRow(key.expression, row))
|
||||||
|
return if (key.descending) magnitude.negate() else magnitude
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rangeMagnitude(value: Value): BigDecimal = when (value) {
|
||||||
|
is Value.IntegerValue -> BigDecimal(value.value)
|
||||||
|
is Value.DecimalValue -> value.value
|
||||||
|
is Value.RealValue -> BigDecimal.valueOf(value.value)
|
||||||
|
is Value.DateValue -> BigDecimal(value.value.toEpochDay())
|
||||||
|
else -> throw KestrelException("a RANGE frame requires the ORDER BY column to be numeric or DATE")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveRangeFrameOffset(bound: FrameBound, row: List<Value>): BigDecimal {
|
||||||
|
val offset = when (val value = evaluateRow(bound.offset!!, row)) {
|
||||||
|
is Value.IntegerValue -> BigDecimal(value.value)
|
||||||
|
is Value.DecimalValue -> value.value
|
||||||
|
is Value.RealValue -> BigDecimal.valueOf(value.value)
|
||||||
|
else -> throw KestrelException("RANGE frame offset must be numeric")
|
||||||
|
}
|
||||||
|
if (offset.signum() < 0) throw KestrelException("RANGE frame offset must not be negative")
|
||||||
|
return offset
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits [ordered] (positions into [combined], already sorted by the OVER's ORDER BY) into
|
||||||
|
* contiguous "peer group" ranges of rows tied on the order key - or one range spanning the
|
||||||
|
* whole partition when there is no ORDER BY, matching the default SQL frame. Shared by
|
||||||
|
* [assignAggregateWindowValues] (running aggregate through the end of a peer group) and
|
||||||
|
* [WindowFunctionKind.LastValue] (last row of a peer group).
|
||||||
|
*/
|
||||||
|
private fun peerGroupRanges(orderBy: List<OrderKey>, ordered: List<Int>, combined: List<List<Value>>): List<IntRange> {
|
||||||
|
if (orderBy.isEmpty()) return listOf(ordered.indices)
|
||||||
|
val keys = ordered.map { index -> orderBy.map { key -> evaluateRow(key.expression, combined[index]) } }
|
||||||
|
val ranges = mutableListOf<IntRange>()
|
||||||
|
var i = 0
|
||||||
|
while (i < ordered.size) {
|
||||||
|
var j = i
|
||||||
|
while (j < ordered.size && keys[j] == keys[i]) j++
|
||||||
|
ranges.add(i until j)
|
||||||
|
i = j
|
||||||
|
}
|
||||||
|
return ranges
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun primaryRows(): List<Row> {
|
private fun primaryRows(): List<Row> {
|
||||||
|
|||||||
Reference in new issue
Block a user