ulysses-you commented on code in PR #59863:
URL: https://github.com/apache/spark/pull/59863#discussion_r4438941671


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/EnsureRequirements.scala:
##########
@@ -924,15 +928,27 @@ case class EnsureRequirements(
       // them. Each side stamps why it is left ungrouped (`UngroupingOrigin`), 
so a reader of the
       // finished plan judges the pair on the layouts it holds; the stamp also 
decides the node's
       // routing (`GroupPartitionsExec.distributePartitions`).
-      (
-        GroupPartitionsExec(rawLeft, leftSpec.joinKeyPositions,
-          Some(mergedPartitionKeys), leftReducers,
+      def regroupOrIdentity(
+          child: SparkPlan,
+          spec: KeyedShuffleSpec,
+          reducers: Option[Seq[Option[KeyReducer]]],
+          replicates: Boolean): SparkPlan = {
+        val gpe = GroupPartitionsExec(child, spec.joinKeyPositions,
+          Some(mergedPartitionKeys), reducers,
           ungroupingOrigin = Option.when(applyPartialClustering)(
-            if (replicateLeftSide) REPLICATED_FOR_JOIN else SPLIT_FOR_JOIN)),
-        GroupPartitionsExec(rawRight, rightSpec.joinKeyPositions,
-          Some(mergedPartitionKeys), rightReducers,
-          ungroupingOrigin = Option.when(applyPartialClustering)(
-            if (replicateRightSide) REPLICATED_FOR_JOIN else SPLIT_FOR_JOIN)))
+            if (replicates) REPLICATED_FOR_JOIN else SPLIT_FOR_JOIN))
+        // Keep both alignment nodes when a replicate side is chosen, even if 
the join type
+        // declines it: a re-run peels a local sort only through a 
`GroupPartitionsExec`, and
+        // that peel is how the replicate-side choice is reproduced.
+        if (!replicateSideChosen &&
+            gpe.outputPartitioning == child.outputPartitioning) {
+          child

Review Comment:
   Updated in 5346f9f5880: the skip is limited to children whose leaves are all 
shuffle query stages. Other children retain the node and its check against the 
planned child partitioning. A controlled scan-rebuild test covers the guard and 
shows the incorrect pairing when the node is removed.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/EnsureRequirements.scala:
##########
@@ -924,15 +928,27 @@ case class EnsureRequirements(
       // them. Each side stamps why it is left ungrouped (`UngroupingOrigin`), 
so a reader of the
       // finished plan judges the pair on the layouts it holds; the stamp also 
decides the node's
       // routing (`GroupPartitionsExec.distributePartitions`).
-      (
-        GroupPartitionsExec(rawLeft, leftSpec.joinKeyPositions,
-          Some(mergedPartitionKeys), leftReducers,
+      def regroupOrIdentity(
+          child: SparkPlan,
+          spec: KeyedShuffleSpec,
+          reducers: Option[Seq[Option[KeyReducer]]],
+          replicates: Boolean): SparkPlan = {
+        val gpe = GroupPartitionsExec(child, spec.joinKeyPositions,
+          Some(mergedPartitionKeys), reducers,
           ungroupingOrigin = Option.when(applyPartialClustering)(
-            if (replicateLeftSide) REPLICATED_FOR_JOIN else SPLIT_FOR_JOIN)),
-        GroupPartitionsExec(rawRight, rightSpec.joinKeyPositions,
-          Some(mergedPartitionKeys), rightReducers,
-          ungroupingOrigin = Option.when(applyPartialClustering)(
-            if (replicateRightSide) REPLICATED_FOR_JOIN else SPLIT_FOR_JOIN)))
+            if (replicates) REPLICATED_FOR_JOIN else SPLIT_FOR_JOIN))
+        // Keep both alignment nodes when a replicate side is chosen, even if 
the join type
+        // declines it: a re-run peels a local sort only through a 
`GroupPartitionsExec`, and
+        // that peel is how the replicate-side choice is reproduced.
+        if (!replicateSideChosen &&

Review Comment:
   We initially changed `ConvertSortMergeJoinToShuffledHashJoin` to look 
through the node. We then considered the pass-through node itself unnecessary: 
it retains grouping and partition-key information in the driver plan and can be 
misleading in the execution plan. This PR removes the node instead of teaching 
only SHJ conversion to look through it.
   
   The node is still constructed before the skip is decided, so this reduces 
retention in the resulting plan, not construction cost. To preserve the 
scan-rebuild guard, 5346f9f5880 restricts the skip to children whose leaves are 
all shuffle query stages.



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/exchange/EnsureRequirementsSuite.scala:
##########
@@ -2613,23 +2614,34 @@ class EnsureRequirementsSuite extends 
SharedSparkSession {
     // already wrapped the non-grouped side, and picked a different side 
wherever one holds more
     // than one split per key. Both shapes read the choice back off the 
`distributePartitions`
     // flags.
-    def distribute(child: SparkPlan): Boolean = child.collectFirst {
-      case g: GroupPartitionsExec => g
-    }.get.distributePartitions
-
-    def flags(smj: SparkPlan): (Boolean, Boolean) =
+    // A side picked to spread keeps its node; a side whose alignment is the 
identity keeps its
+    // child and so has none, which reads as "not distributing". The node is 
asserted on the side
+    // that is expected to distribute before the flags are read, so a node 
that went missing
+    // cannot pass for the replicated side.
+    def flags(smj: SparkPlan, expected: (Boolean, Boolean)): Unit =
       withSQLConf(
           SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> "true",
           SQLConf.V2_BUCKETING_PARTIALLY_CLUSTERED_DISTRIBUTION_ENABLED.key -> 
"true") {
         val Seq(newLeft, newRight) = EnsureRequirements.apply(smj).children
-        (distribute(newLeft), distribute(newRight))
+        val nodes = Seq(newLeft, newRight).map(_.collectFirst {
+          case g: GroupPartitionsExec => g
+        })
+        nodes.zip(Seq(expected._1, expected._2)).foreach { case (node, 
distributes) =>
+          if (distributes) {
+            assert(node.isDefined, "a side that distributes keeps its grouping 
node")
+          }
+        }
+        val distributed = nodes.map(_.exists(_.distributePartitions))

Review Comment:
   Updated in 5346f9f5880: these inputs do not have shuffle-stage leaves, so 
both alignment nodes are retained. I restored the original assertions requiring 
a node on each side before reading `distributePartitions`.



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/exchange/EnsureRequirementsSuite.scala:
##########
@@ -2688,9 +2702,15 @@ class EnsureRequirementsSuite extends SharedSparkSession 
{
           SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> "true",
           SQLConf.V2_BUCKETING_PARTIALLY_CLUSTERED_DISTRIBUTION_ENABLED.key -> 
"true") {
         EnsureRequirements.apply(smj).children.map { child =>
-          val gpe = child.collectFirst { case g: GroupPartitionsExec => g }.get
-          (gpe.ungroupingOrigin,
-            gpe.outputPartitioning.asInstanceOf[KeyedPartitioning].isGrouped)
+          child.collectFirst { case g: GroupPartitionsExec => g }.map { gpe =>
+            (gpe.ungroupingOrigin,
+              gpe.outputPartitioning.asInstanceOf[KeyedPartitioning].isGrouped)
+          }.getOrElse {

Review Comment:
   Updated in 5346f9f5880: the test inputs do not have shuffle-stage leaves, 
including the full outer case. I restored the original assertions requiring a 
node on both sides, including for the declined left outer choice.



##########
sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala:
##########
@@ -726,11 +726,17 @@ class KeyGroupedPartitioningSuite
         assert(collectAllShuffles(joins.head).isEmpty,
           "should not add shuffle for both sides of the join")
         val sides = Seq(joins.head.left, 
joins.head.right).map(collectAllGroupPartitions)
-        val distributed = sides.map(_.map(_.distributePartitions))
-        assert(distributed == Seq(Seq(expectedLeftDistributed), Seq(false)),
+        // A side without a node distributes nothing either: its node would 
have been a
+        // pass-through and is not built, and the side reports the layout it 
already had.

Review Comment:
   Updated in 5346f9f5880: both scan sides retain their nodes with the narrowed 
skip, so I restored the original per-side grouping and partition-count 
assertions for both configuration arms.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/EnsureRequirements.scala:
##########
@@ -913,7 +915,9 @@ case class EnsureRequirements(
         }
       }
 
-      // Now we need to push-down the common partition information to the 
`GroupPartitionsExec`s.
+      // Now we need to push-down the common partition information to the 
sides that need it. A
+      // side whose alignment is the identity keeps its child instead (see 
`regroupOrIdentity`
+      // below), which is common when one side was shuffled onto the other 
side's layout.

Review Comment:
   Updated in 5346f9f5880: the `outputPartitioning` documentation now notes 
that a coalesced or skewed read over a keyed shuffle stage reports 
`UnknownPartitioning` itself, allowing validation to reject it without the 
alignment node.



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