sunchao commented on code in PR #7256:
URL: https://github.com/apache/datafusion-comet/pull/7256#discussion_r4426184807


##########
spark/src/main/scala/org/apache/spark/sql/comet/CometWindowExec.scala:
##########
@@ -81,10 +88,45 @@ 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 already reports 
the order the window
+      // needs, but rows from the JVM arrive through a ScanExec that declares 
none. Send the
+      // partition keys' order, the only one the native window checks, for the 
planner to declare
+      // (see SortedInputExec). When the order may be false, nothing native 
can declare it, so the
+      // window stays in Spark, unless the doubt is a Comet join in the same 
native plan, which
+      // DataFusion sees itself. A key Comet cannot express, or a collated 
string key, sends the
+      // window back to Spark too.
+      val requiredOrdering = op.requiredChildOrdering.head
+      val inputSortOrders =
+        if (op.partitionSpec.nonEmpty &&
+          SortOrder.orderingSatisfies(op.child.outputOrdering, 
requiredOrdering)) {
+          // The walk that counts Comet joins in the same native plan flags 
everything the other
+          // one does, so the other runs only when this one flags the order.
+          if (!CometHashJoinExec.mayReportFalseInputOrder(op.child, 
countInPlan = true)) {
+            val partitionKeys = requiredOrdering.take(op.partitionSpec.length)

Review Comment:
   The partition keys are sufficient for the ordering declaration, but a window 
still compares its `ORDER BY` keys to find peers. With the native cache reader, 
I cached `(k, s)` sorted by both columns, where `k` is an integer and `s` uses 
`UTF8_LCASE`. For `A, a, B, b` within each `k`, Spark 4.1.3 gives `dense_rank() 
OVER (PARTITION BY k ORDER BY s)` ranks `1, 1, 2, 2`. This commit runs 
`CometWindow` directly over `CometInMemoryTableScan` and gives `1, 2, 3, 4`. 
The previous revision fell back and matched Spark. Checking collation on the 
order keys before conversion would still allow the declaration itself to 
contain only partition keys.



##########
spark/src/main/scala/org/apache/spark/sql/comet/CometWindowExec.scala:
##########
@@ -81,10 +88,45 @@ 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 already reports 
the order the window
+      // needs, but rows from the JVM arrive through a ScanExec that declares 
none. Send the
+      // partition keys' order, the only one the native window checks, for the 
planner to declare
+      // (see SortedInputExec). When the order may be false, nothing native 
can declare it, so the
+      // window stays in Spark, unless the doubt is a Comet join in the same 
native plan, which
+      // DataFusion sees itself. A key Comet cannot express, or a collated 
string key, sends the
+      // window back to Spark too.
+      val requiredOrdering = op.requiredChildOrdering.head
+      val inputSortOrders =
+        if (op.partitionSpec.nonEmpty &&
+          SortOrder.orderingSatisfies(op.child.outputOrdering, 
requiredOrdering)) {
+          // The walk that counts Comet joins in the same native plan flags 
everything the other
+          // one does, so the other runs only when this one flags the order.
+          if (!CometHashJoinExec.mayReportFalseInputOrder(op.child, 
countInPlan = true)) {
+            val partitionKeys = requiredOrdering.take(op.partitionSpec.length)
+            val orders = partitionKeys.flatMap { order =>
+              if (hasNonDefaultStringCollation(order.dataType)) None
+              else exprToProto(order, op.child.output)
+            }
+            if (orders.length < partitionKeys.length) {
+              withFallbackReason(op, inexpressibleInputOrderReason)
+              return None
+            }
+            orders
+          } else if (CometHashJoinExec.mayReportFalseInputOrder(op.child, 
countInPlan = false)) {
+            withFallbackReason(op, uncheckedInputOrderReason)
+            return None

Review Comment:
   Falling back to Spark here leaves the window above a join whose output is 
out of order. I cached 12 rows in one partition, sorted by `k = v % 3`, then 
broadcast two build rows (`z = 0, 1`) and joined on `v % 2 <= z`. The native 
nested-loop join emits keys `0, 1, 2` twice, once for each build row. Spark has 
already omitted the sort, so `count(*) OVER (PARTITION BY k)` counts the 
separate runs. Six rows get 2 and twelve get 4. Vanilla Spark and a control 
with a physical sort after the same native join both return 6 on all 18 rows. 
Caching the join output also reproduces it. The previous revision raised an 
ordering error on this query. The fallback needs to restore the required order 
before the Spark window consumes those rows.



##########
native/core/src/execution/planner/join.rs:
##########
@@ -402,48 +403,59 @@ impl PhysicalPlanner {
         Ok(Arc::new(prepared))
     }
 
-    /// `plan`, declaring `sort_orders` as the order its rows arrive in, 
unless it already
-    /// declares one. Spark's plan knows the order of an input that enters the 
native plan from
-    /// the JVM, and of a scan of sorted files, but the `ScanExec` or the file 
scan the rows
-    /// arrive through declares none. Only that order is declared: when an 
operator in between may
-    /// have moved rows, `plan` is left as it is.
+    /// `plan`, declaring `sort_orders` as the order its rows arrive in, 
unless the order it
+    /// declares already satisfies them. Spark's plan knows the order of an 
input that enters the
+    /// native plan from the JVM, and of a scan of sorted files, but the 
`ScanExec` or the file
+    /// scan the rows arrive through declares none. Only that order is 
declared: when an operator
+    /// in between may have moved rows, `plan` is left as it is.
     ///
-    /// The serde sends an order only when the streamed side's Comet operators 
report it, and it
-    /// checks that nothing under them moves rows (`mayReportFalseOrder`), 
while this walk asks
-    /// the native operators the same thing through `maintains_input_order`. 
The two agree as
-    /// long as a Comet operator that reports an order keeps it natively and 
says so, and a Comet
-    /// leaf that reports one is a source this walk knows or declares its 
order itself, as the
-    /// native range does. One that does not lands here with `plan` unchanged, 
and the join then
-    /// moves its unmatched rows after Spark has dropped the sort above it. 
Nothing here can fall
-    /// back at that point, so the invariant is kept at the operator: see 
"Adding a New Operator"
-    /// in the contributor guide.
-    fn with_declared_order(
+    /// An input that declares only part of `sort_orders`, such as a window 
over another window,
+    /// gets them declared next to its own order.
+    ///
+    /// The serde sends an order only when the Comet operators of a join's 
streamed side or of a
+    /// window's input report it, and it checks that nothing under them moves 
rows
+    /// (`mayReportFalseOrder`), while this walk asks the native operators the 
same thing through
+    /// `maintains_input_order`. The two agree as long as a Comet operator 
that reports an order
+    /// keeps it natively and says so, and a Comet leaf that reports one is a 
source this walk
+    /// knows or declares its order itself, as the native range does. One that 
does not lands
+    /// here with `plan` unchanged, and the join then moves its unmatched rows 
after Spark has
+    /// dropped the sort above it, or the window rejects its input. Nothing 
here can fall back at
+    /// that point, so the invariant is kept at the operator: see "Adding a 
New Operator" in the
+    /// contributor guide.
+    pub(super) fn with_declared_order(
         &self,
         plan: Arc<dyn ExecutionPlan>,
         sort_orders: &[Expr],
     ) -> Result<Arc<dyn ExecutionPlan>, ExecutionError> {
-        if plan.output_ordering().is_some() || 
!Self::keeps_source_order(&plan) {
+        if sort_orders.is_empty() || !Self::keeps_source_order(&plan) {

Review Comment:
   This early return still drops a valid ordering above a left outer broadcast 
hash join. I sorted `range(12)` by `k = id % 3`, projected `k AS a, k AS b, id 
AS v`, and cached the result in one partition. I then left-joined a broadcast 
one-row table on `a`, kept both aliases in the output, and ran `sum(v) OVER 
(PARTITION BY a)`. Spark knows `a` and `b` have the same order and returns all 
12 rows (group sums `18, 22, 26`). The native join declares its probe ordered 
by `b`, and the window sends `a`, but `keeps_source_order` rejects the 
DataFusion right outer join even with that ordered probe. The task fails with 
`All partition by columns should have an ordering`. Changing only the window to 
`PARTITION BY b` returns all 12 expected rows with the same native join and no 
sort.



-- 
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]

Reply via email to