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


##########
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:
   Thanks, agreed. Falling back hands the same out-of-order rows to Spark's 
window, which trusts the order as much as the native one does. 136e5cf53f 
removes the fallback. When the order may be false, the window sends no order, 
so the native window rejects its input with the ordering error, as on `main` 
and the previous revision, instead of returning wrong rows.
   
   Restoring the order needs either a nested loop join that keeps it or a sort 
below the window. #6785 does both: it keeps a nested loop join over an ordered 
streamed side in Spark (#6898), and it sorts a window's input natively when the 
partition keys' order is not declared.
   
   From reading the code, a native `WindowGroupLimit` between the join and the 
window gives wrong rows on `main` already, since `PartitionedRankLimitExec` 
declares the partition and order keys' order itself. Keeping the join in Spark 
covers that shape too.
   



##########
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:
   Thanks. 136e5cf53f checks every partition and order key for a collated 
string before anything is converted, so such a window stays in Spark whatever 
its input is. The native window finds partitions and peers by comparing keys 
byte by byte. The check now also covers a window with no `PARTITION BY`, which 
never reached the old one. The declaration still holds only the partition keys.
   
   `a window over a cache sorted by a collated string key stays in Spark` now 
also runs your shape: `dense_rank() OVER (PARTITION BY n ORDER BY k)` over a 
cache sorted by an integer `n` and a `UTF8_LCASE` `k`. It checks Spark's ranks 
and the fallback reason.
   



##########
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:
   Thanks. `keeps_source_order` now accepts a right outer join whose probe 
declares an order. That is exactly when DataFusion keeps the join's unmatched 
probe rows in place, since `HashJoinExec::execute` hands 
`right.output_ordering().is_some()` to the stream. So the window's order is 
declared next to the join's, and your query runs. A right outer join whose 
probe declares nothing is still rejected. Fixed in 136e5cf53f.
   
   `hash_join_declares_streamed_order_on_probe_input` in `planner.rs` now also 
checks the rule for both outer join shapes. The cache test `a native window 
declares its order next to the order its input declares` now runs your shape: a 
left outer join with a one-row broadcast table on `k`, and `sum(v) OVER 
(PARTITION BY a)` above it, where `a` is an alias of `k`. A residual condition 
leaves unmatched rows among the matched ones of key 0, so the query would split 
that key if the join moved them. It checks the rows, that the window sends `a`, 
and that no sort is planned.
   



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