sunchao commented on code in PR #7256:
URL: https://github.com/apache/datafusion-comet/pull/7256#discussion_r4416117731
##########
native/core/src/execution/planner/window.rs:
##########
@@ -131,17 +131,21 @@ impl OperatorBuilder for WindowBuilder {
// trigger a retract call.
let window_expr = window_expr?;
let all_bounded = window_expr.iter().all(|e| e.uses_bounded_memory());
+
+ // Both window operators reject an input that does not declare its
partition keys' order.
+ let input =
+ planner.with_declared_order(Arc::clone(&child.native_plan),
&wnd.input_sort_orders)?;
Review Comment:
Could this also declare the order for an empty bucket? I tried a Parquet
table written by one writer with `bucketBy(8, "k").sortBy("k", "v")`, two
distinct `k` values, and legacy bucketed scan output ordering enabled. The head
plans `CometWindowExec` directly over `CometNativeScan` for `dense_rank() OVER
(PARTITION BY k ORDER BY v)` and sends both sort keys. Six of the eight tasks
have no files. `build_parquet_scan_plan` gives those tasks an `EmptyExec`,
which `keeps_source_order` rejects. DataFusion's bounded window checks its
partition ordering before polling the empty stream, so one empty bucket is
enough to fail the query. Vanilla Spark returns all 16 rows. An empty input
satisfies any ordering. Could we handle that case and cover a table with
unpopulated buckets?
##########
spark/src/main/scala/org/apache/spark/sql/comet/CometWindowExec.scala:
##########
@@ -81,10 +81,38 @@ object CometWindowExec extends
CometOperatorSerde[WindowExec] {
if (windowExprProto.forall(_.isDefined) &&
partitionExprs.forall(_.isDefined)
&& sortOrders.forall(_.isDefined)) {
+ // Spark plans no sort below the window when its input reports the order
the window needs,
+ // a sorted cache for instance, but rows from the JVM arrive through a
ScanExec that
+ // declares none, and the native window rejects them. Send the order for
the planner to
+ // declare (see SortedInputExec), unless a native sort below declares it
or the reported
+ // order may be false. A key Comet cannot express or a collated string
key, which the
+ // native side would order by raw bytes, sends the window back to Spark.
+ val requiredOrdering = op.requiredChildOrdering.head
+ val inputSortOrders =
+ if (requiredOrdering.nonEmpty && !op.child.isInstanceOf[CometSortExec]
&&
+ SortOrder.orderingSatisfies(op.child.outputOrdering,
requiredOrdering) &&
+ !CometHashJoinExec.mayReportFalseInputOrder(op.child)) {
+ val orders = requiredOrdering.map { order =>
Review Comment:
Could we preserve the usable part of the cache's ordering for the next
window? On a single-partition cache sorted by `(k, v)`, compute `s = sum(v)
OVER (PARTITION BY k)`, then `sum(s) OVER (PARTITION BY k, v)`. Spark 4.1.3
returns all 24 rows in my example with no intervening sort or exchange. On this
head, both `CometWindowExec`s advertise `(k, v)` to Spark, but the first
serializes only `k`. The second sends both keys and `with_declared_order`
returns early because its input already has an ordering. DataFusion therefore
has no ordering for `v` when the second window starts. Keeping the trusted,
expressible suffix at the first window, or extending a partial declaration
safely at the second, would cover this. Could we add a stacked-window
regression?
##########
spark/src/test/scala/org/apache/comet/exec/CometInMemoryCacheSuite.scala:
##########
@@ -3692,6 +3692,91 @@ class CometInMemoryCacheSuite extends CometTestBase {
}
}
+ test("a native window over a sorted cache declares the cache's order") {
+ // Spark plans no sort below a window over the cache's sort keys, and the
native window
+ // rejected the scan, which declares no order, with "Expects PARTITION BY
expression to be
+ // ordered" (#7223). The partition key is nullable and the order key
descending, so the
+ // declared order has to match the cache's direction and null placement.
dense_rank runs in
+ // DataFusion's BoundedWindowAggExec, and the frame up to UNBOUNDED
FOLLOWING in WindowAggExec.
+ // Within a key, v = k + 20 * j for j in 0 until 100, and a NULL key holds
the multiples of 20.
+ val key = (v: Long) => if (v % 20 == 0) null else v % 20
+ val queries = Seq(
+ "dense_rank() OVER (PARTITION BY k ORDER BY v DESC)" ->
+ ((v: Long) => (100 - v / 20).toInt),
+ "sum(v) OVER (PARTITION BY k ORDER BY v DESC " +
+ "ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)" -> { (v: Long) =>
+ val j = v / 20
+ (j + 1) * (v % 20) + 10 * j * (j + 1)
+ })
+ Seq(true, false).foreach { nativeCache =>
+ withConversions(
+ SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+ SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1",
+ SQLConf.SHUFFLE_PARTITIONS.key -> "2",
+ CometConf.COMET_EXEC_IN_MEMORY_CACHE_ENABLED.key ->
nativeCache.toString) {
+ try {
+ val cached = spark
+ .range(2000)
+ .selectExpr("CASE WHEN id % 20 = 0 THEN NULL ELSE id % 20 END AS
k", "id AS v")
+ .repartition(2, col("k"))
+ .sortWithinPartitions(col("k"), col("v").desc)
+ .cache()
+ cached.count()
+
+ queries.foreach { case (window, expected) =>
+ val df = cached.selectExpr("k", "v", s"$window AS w")
+ val rows = (0L until 2000L).map(v => Row(key(v), v, expected(v)))
+ QueryTest.sameRows(rows, df.collect().toSeq).foreach(fail(_))
+ val plan = df.queryExecution.executedPlan
+ withClue(s"nativeCache=$nativeCache, $window\n$plan\n") {
+ val windows = collect(plan) { case w: CometWindowExec => w }
+ assert(windows.size == 1)
+ assert(windows.head.nativeOp.getWindow.getInputSortOrdersCount
== 2)
Review Comment:
Could we assert the columns, directions and null placements in
`input_sort_orders` here? If only the declaration for `v` changed from `DESC
NULLS LAST` to `ASC NULLS FIRST`, the cache would still deliver the same rows
and the window would still use its separate `order_by_list` to calculate both
results. The count would also stay at two. Checking `k ASC NULLS FIRST, v DESC
NULLS LAST` directly would catch that regression.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]