peter-toth commented on code in PR #59306:
URL: https://github.com/apache/spark/pull/59306#discussion_r4246414728


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -253,23 +256,33 @@ case class GroupPartitionsExec(
       case _ => true
     }
 
+  /** Whether the k-way merge was planned for this node. See 
`sortedMergeOrdering`. */
+  def enableSortedMerge: Boolean = sortedMergeOrdering.isDefined
+
+  /**
+   * The ordering the k-way merge merges by. Before the merge is planned it is 
the child's. After
+   * that it is the one the merge was planned with, see `sortedMergeOrdering`.
+   */
+  private def mergeOrdering: Seq[SortOrder] = 
sortedMergeOrdering.getOrElse(child.outputOrdering)
+
   /** Whether a k-way merge would work at all, leaving aside whether it is 
switched on. */
   @transient private lazy val kWayMergeIsFeasible: Boolean =
-    child.outputOrdering.nonEmpty && childIsSafeForKWayMerge
+    mergeOrdering.nonEmpty && childIsSafeForKWayMerge
 
-  /** Whether this node performs the k-way merge. No config term, see 
`enableSortedMerge`. */
+  /** Whether this node performs the k-way merge. No config term, see 
`sortedMergeOrdering`. */
   @transient private lazy val usesSortedMerge: Boolean =

Review Comment:
   Thanks. A planned merge that cannot run now fails at execution 
(`checkPlannedMergeRuns`), as `checkChildStillMatches` does for the 
partitioning. Planning keeps the fallback, since `EnsureRequirements` then adds 
a `SortExec`.
   
   The check also showed that, without AQE, the `debug()` helper wraps every 
node in `DebugExec`, which was not whitelisted. So `debug()` on a planned merge 
would now fail. `DebugExec` keeps its per-partition state in its evaluator and 
its accumulators only add, so it is now `SafeForKWayMerge`, with a test.
   
   Fixed in 
[`24bb37cf81b`](https://github.com/apache/spark/pull/59306/commits/24bb37cf81b6fd665b34a0ab3458797b1392e76b).
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -253,23 +256,33 @@ case class GroupPartitionsExec(
       case _ => true
     }
 
+  /** Whether the k-way merge was planned for this node. See 
`sortedMergeOrdering`. */
+  def enableSortedMerge: Boolean = sortedMergeOrdering.isDefined
+
+  /**
+   * The ordering the k-way merge merges by. Before the merge is planned it is 
the child's. After
+   * that it is the one the merge was planned with, see `sortedMergeOrdering`.
+   */
+  private def mergeOrdering: Seq[SortOrder] = 
sortedMergeOrdering.getOrElse(child.outputOrdering)
+
   /** Whether a k-way merge would work at all, leaving aside whether it is 
switched on. */
   @transient private lazy val kWayMergeIsFeasible: Boolean =
-    child.outputOrdering.nonEmpty && childIsSafeForKWayMerge
+    mergeOrdering.nonEmpty && childIsSafeForKWayMerge
 
-  /** Whether this node performs the k-way merge. No config term, see 
`enableSortedMerge`. */
+  /** Whether this node performs the k-way merge. No config term, see 
`sortedMergeOrdering`. */
   @transient private lazy val usesSortedMerge: Boolean =
     enableSortedMerge && hasCoalescing && kWayMergeIsFeasible
 
   /**
    * Returns a copy of this node with k-way merge enabled, when the config is 
on, this node
    * coalesces partitions and the merge is feasible. The only read of
-   * `preserveOrderingOnCoalesce`.
+   * `preserveOrderingOnCoalesce`. The copy keeps the ordering checked here, 
see
+   * `sortedMergeOrdering`.
    */
   def tryEnableSortedMerge(): Option[GroupPartitionsExec] = {
     Option.when(conf.v2BucketingPreserveOrderingOnCoalesceEnabled && 
hasCoalescing &&
         kWayMergeIsFeasible) {
-      val newGroupPartitions = copy(enableSortedMerge = true)
+      val newGroupPartitions = copy(sortedMergeOrdering = Some(mergeOrdering))

Review Comment:
   Done in 
[`24bb37cf81b`](https://github.com/apache/spark/pull/59306/commits/24bb37cf81b6fd665b34a0ab3458797b1392e76b),
 `tryEnableSortedMerge` reads the ordering once. It also leaves a node that 
already merges as it is, so the merge is decided once.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -75,14 +75,17 @@ import org.apache.spark.sql.vectorized.ColumnarBatch
  *                         [[REPLICATED_FOR_JOIN]] groups the splits first and 
repeats each group
  *                         into its expected slots, and `None` groups them, 
one partition per key.
  *                         See [[UngroupingOrigin]].
- * @param enableSortedMerge When true, uses [[SortedMergeCoalescedRDD]] to 
perform a k-way merge
- *                          of the coalesced partitions, preserving the 
child's output ordering
- *                          end-to-end. Set by [[EnsureRequirements]] when a 
parent operator
- *                          requires the ordering that this node can satisfy 
via sorted merge.
- *                          This flag is the decision, not a hint. Nothing 
below re-reads the
- *                          config that produced it. A node planned with the 
merge would otherwise
- *                          concatenate instead, and a sort-merge join above 
it would lose rows
- *                          (SPARK-59279).
+ * @param sortedMergeOrdering When set, uses [[SortedMergeCoalescedRDD]] to 
perform a k-way merge
+ *                            of the coalesced partitions by this ordering, 
preserving it
+ *                            end-to-end. Set by [[EnsureRequirements]] when a 
parent operator
+ *                            requires the ordering that this node can satisfy 
via sorted merge.
+ *                            This is the decision, not a hint. Nothing below 
re-reads the config
+ *                            that produced it. A node planned with the merge 
would otherwise
+ *                            concatenate instead, and a sort-merge join above 
it would lose rows
+ *                            (SPARK-59279). The ordering is captured from the 
child when the merge
+ *                            is decided. That is the ordering the parent's 
requirement was checked
+ *                            against. The child's ordering can follow a 
config too, so a later

Review Comment:
   The scan's data does not depend on the config, only what it reports. So the 
other readers only do extra work after such a change, e.g. a reverted AQE 
rewrite. A per-instance freeze like the one for `reportedKeyedPartitioning` 
would not cover every case either. It is a `lazy val`, so a new instance reads 
the config again. `PlanAdaptiveDynamicPruningFilters` makes one at stage 
creation for a scan with a dynamic pruning filter. So I'd keep the freeze in 
the node whose execution depends on the ordering, and leave the scan as it is.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -253,23 +256,33 @@ case class GroupPartitionsExec(
       case _ => true
     }
 
+  /** Whether the k-way merge was planned for this node. See 
`sortedMergeOrdering`. */
+  def enableSortedMerge: Boolean = sortedMergeOrdering.isDefined
+
+  /**
+   * The ordering the k-way merge merges by. Before the merge is planned it is 
the child's. After
+   * that it is the one the merge was planned with, see `sortedMergeOrdering`.
+   */
+  private def mergeOrdering: Seq[SortOrder] = 
sortedMergeOrdering.getOrElse(child.outputOrdering)

Review Comment:
   Done in 
[`24bb37cf81b`](https://github.com/apache/spark/pull/59306/commits/24bb37cf81b6fd665b34a0ab3458797b1392e76b).
 The constructor requires a non-empty ordering. `tryEnableSortedMerge` reads 
the child's ordering directly, and the merge path reads only the frozen one. 
`mergeOrdering` and `kWayMergeIsFeasible` are gone.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -490,8 +512,8 @@ case class GroupPartitionsExec(
       s"Reducers: ${truncatedString(names, "[", ", ", "]", joinKeyMaxFields)}"
     }
     val distributeStr = Iterator(s"DistributePartitions: 
$distributePartitions")
-    // Rendered from the constructor field, as `DistributePartitions` above 
is, and not from
-    // `usesSortedMerge`, which reads the child's ordering. This method feeds 
`simpleString`, which
+    // Rendered from the constructor field `sortedMergeOrdering`, as 
`DistributePartitions` above
+    // is. `usesSortedMerge` would walk the child. This method feeds 
`simpleString`, which
     // `treeString` calls on error paths, so it stays on what the node was 
built with.
     val sortedMergeStr = Iterator(s"SortedMerge: $enableSortedMerge")

Review Comment:
   Done in 
[`24bb37cf81b`](https://github.com/apache/spark/pull/59306/commits/24bb37cf81b6fd665b34a0ab3458797b1392e76b),
 e.g. `SortedMerge: [id#1 ASC NULLS FIRST, name#2 ASC NULLS FIRST]`. A node 
without a merge still shows `SortedMerge: false`.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -428,16 +443,24 @@ case class GroupPartitionsExec(
    * `plannedPartitioning` are decided for one child's partitioning, so a node 
handed another has to
    * be decided again over it.
    *
-   * Rebuilding is all this does: what the operator above reads, the ordering 
it was planned
-   * against included, is the caller's to hold, since nothing here knows what 
that operator
-   * requires.
+   * A merging node is turned away. Its frozen `sortedMergeOrdering` was 
checked against this node's
+   * child, not against `newChild`. No merging node reaches this path today. 
The only caller,
+   * `CombineAdjacentAggregation`, rebuilds a node over a subtree that holds 
an aggregate, which is
+   * not `SafeForKWayMerge`.
+   *
+   * Rebuilding is all this does. Nothing here knows what the operator above 
requires, so checking
+   * that the rebuilt node still meets it is the caller's job.
    */
   def withKeyPositionsFor(newChild: SparkPlan): Option[GroupPartitionsExec] = {
     // An aligned node's slot order comes from the parent distribution rather 
than the child, so the
     // factory cannot re-decide it for `newChild`.
     if (expectedKeyCount.isDefined) {
       return None
     }
+    // The frozen merge ordering was checked against this node's child, not 
against `newChild`.
+    if (enableSortedMerge) {

Review Comment:
   I'd keep refusing it:
   - Rebuilding without the frozen ordering would drop a merge the parent 
relies on, since its sort was not added.
   - Deciding again over `newChild` needs the parent's required ordering, which 
this method does not see.
   - An assertion would fail a query, while refusing only skips the rebuild in 
`CombineAdjacentAggregation`, an optimization.
   
   No query sees the change, since no merging node reaches this method today. 
The method already refuses an aligned node (`expectedKeyCount`) for a similar 
reason.
   



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExecSuite.scala:
##########
@@ -493,7 +505,7 @@ class GroupPartitionsExecSuite extends SharedSparkSession {
       withSQLConf(
           SQLConf.V2_BUCKETING_PRESERVE_ORDERING_ON_COALESCE_ENABLED.key ->
             configEnabled.toString) {
-        val flagged = GroupPartitionsExec(child, enableSortedMerge = true)
+        val flagged = GroupPartitionsExec(child, sortedMergeOrdering = 
Some(child.outputOrdering))

Review Comment:
   Added "SPARK-60044: a planned merge keeps the ordering it was planned with" 
in 
[`24bb37cf81b`](https://github.com/apache/spark/pull/59306/commits/24bb37cf81b6fd665b34a0ab3458797b1392e76b).
 Its child's ordering follows `partitionKeyOrdering.enabled`, like the scan's. 
It checks that `tryEnableSortedMerge` captures the ordering, and that the node, 
a `copy` and a `withNewChildren` over an `InputAdapter` keep it after the 
config is turned off. It fails if `withNewChildInternal` drops the ordering or 
`outputOrdering` reads the child's.
   



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExecSuite.scala:
##########
@@ -450,7 +460,8 @@ class GroupPartitionsExecSuite extends SharedSparkSession {
     
withSQLConf(SQLConf.V2_BUCKETING_PRESERVE_KEY_ORDERING_ON_COALESCE_ENABLED.key 
-> "true") {
       // Even though enableSortedMerge = true, the child is not safe for k-way 
merge,

Review Comment:
   Done in 
[`24bb37cf81b`](https://github.com/apache/spark/pull/59306/commits/24bb37cf81b6fd665b34a0ab3458797b1392e76b).
   



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