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


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/ValidateRequirements.scala:
##########
@@ -45,29 +47,81 @@ object ValidateRequirements extends Logging {
     assert(requiredChildDistributions.length == children.length)
     assert(requiredChildOrderings.length == children.length)
 
+    // A `ClusteredDistribution` is the one distribution an operator can owe 
its children together
+    // rather than one by one, so an operator whose children all owe one is 
judged on their mutual
+    // layout below, and every other child, an operator with a single 
clustered child included,
+    // answers for itself. That is every such operator, not only a join: one 
that zips corresponding
+    // partitions, a cogroup for instance, reads a layout both children have 
to hold together.
+    val clusteredMultiChild = children.length > 1 &&
+      requiredChildDistributions.forall(_.isInstanceOf[ClusteredDistribution])
+
+    // The one member a finished plan may report without satisfying the 
distribution is the shape
+    // partially clustered distribution spreads ungrouped, and one producer 
builds it:
+    // `EnsureRequirements.checkKeyGroupCompatible`. Both halves of that 
admission are asked here,
+    // and the answer is passed down to the pairing below so the waiver cannot 
be read one way here
+    // and the other way there. Neither half is a second copy: the operator 
kinds come from the
+    // producer itself. What is left to the member, its count and the 
permission for the collapse it
+    // went through, is asked there.
+    val mayBeUngrouped = clusteredMultiChild &&
+      SQLConf.get.v2BucketingPartiallyClusteredDistributionEnabled &&
+      ShuffledJoin.partiallyClusteredJoinType(plan).isDefined

Review Comment:
   Thanks @cloud-fan! Fixed in 32ddb2663e4. The waiver asks the second gate the 
producer applies before it spreads a side now (`canDuplicateLeftSide || 
canDuplicateRightSide`), folded in at the waiver rather than into 
`partiallyClusteredJoinType`: that helper is the producer's entry to key-group 
checking altogether, so a kind turned away there would lose the 
storage-partitioned join it can still plan without spreading a side. The 
producer-boundary test pins the join type as well as the operator kind, the 
same pair refused for a full outer join and accepted for the inner pair the 
waiver is for.
   
   One correction on attribution: the over-admission is not new to the latest 
revision. The waiver was `ShuffledJoin`-wide before it, so a `FullOuter` pair 
was admitted then too, and the operator-kind revision neither introduced nor 
closed it. It came in with the waiver itself.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/ValidateRequirements.scala:
##########
@@ -45,29 +47,81 @@ object ValidateRequirements extends Logging {
     assert(requiredChildDistributions.length == children.length)
     assert(requiredChildOrderings.length == children.length)
 
+    // A `ClusteredDistribution` is the one distribution an operator can owe 
its children together
+    // rather than one by one, so an operator whose children all owe one is 
judged on their mutual
+    // layout below, and every other child, an operator with a single 
clustered child included,
+    // answers for itself. That is every such operator, not only a join: one 
that zips corresponding
+    // partitions, a cogroup for instance, reads a layout both children have 
to hold together.
+    val clusteredMultiChild = children.length > 1 &&
+      requiredChildDistributions.forall(_.isInstanceOf[ClusteredDistribution])
+
+    // The one member a finished plan may report without satisfying the 
distribution is the shape
+    // partially clustered distribution spreads ungrouped, and one producer 
builds it:
+    // `EnsureRequirements.checkKeyGroupCompatible`. Both halves of that 
admission are asked here,
+    // and the answer is passed down to the pairing below so the waiver cannot 
be read one way here
+    // and the other way there. Neither half is a second copy: the operator 
kinds come from the
+    // producer itself. What is left to the member, its count and the 
permission for the collapse it
+    // went through, is asked there.
+    val mayBeUngrouped = clusteredMultiChild &&
+      SQLConf.get.v2BucketingPartiallyClusteredDistributionEnabled &&
+      ShuffledJoin.partiallyClusteredJoinType(plan).isDefined
+
     val satisfied = 
children.zip(requiredChildDistributions.zip(requiredChildOrderings)).forall {
       case (child, (distribution, ordering))
-          if !child.outputPartitioning.satisfies(distribution)
+          if (!child.outputPartitioning.satisfies(distribution) &&
+              !(mayBeUngrouped &&
+                
PartitioningCollection.representativeOf(child.outputPartitioning).isDefined))
             || !SortOrder.orderingSatisfies(child.outputOrdering, ordering) =>
         logDebug(s"ValidateRequirements failed: $distribution, 
$ordering\n$plan")
         false
       case _ => true
     }
 
-    if (satisfied && children.length > 1 &&
-      
requiredChildDistributions.forall(_.isInstanceOf[ClusteredDistribution])) {
-      // Check the co-partitioning requirement.
-      val specs = 
children.map(_.outputPartitioning).zip(requiredChildDistributions).map {
-        case (p, d) => 
p.createShuffleSpec(d.asInstanceOf[ClusteredDistribution])
-      }
-      if (specs.tail.forall(_.isCompatibleWith(specs.head))) {
-        true
-      } else {
+    // What a multi-child clustered operator reads is the pairing: a pair 
aligned without grouping,
+    // which partially clustered distribution builds on purpose, is one the 
sides agree on while
+    // neither is grouped. The pairing cannot tell how the two sides hold a 
key's rows, since a
+    // spread side and one that repeats the whole group report the same keys 
as two sides that
+    // split the key, so that rests on the producer, which is why the 
ungrouped shape alone is
+    // waived above.
+    if (!satisfied) {
+      false
+    } else if (clusteredMultiChild) {
+      val paired = satisfiesForPairing(children, requiredChildDistributions, 
mayBeUngrouped)
+      if (!paired) {
         logDebug(s"ValidateRequirements failed: children not co-partitioned 
in\n$plan")
-        false
       }
+      paired
     } else {
-      satisfied
+      true
+    }
+  }
+
+  /**
+   * Whether the sides of a multi-child clustered operator line up: every side 
offers the layouts it
+   * reports ([[PartitioningCollection.specsForPairing]]), and one member of 
the first side pairs
+   * with every other side. A plan holds what its members report, so no key is 
deduped and none is
+   * re-sorted to make a pair: a side is judged on the partitions it has, 
under the key the
+   * operation clusters on.
+   *
+   * This is the question `EnsureRequirements` asks of a pair it takes as it 
stands, the

Review Comment:
   Thanks @cloud-fan! Reworded in 32ddb2663e4: the doc says the planner asks 
this of a pair it takes as it stands, though not by the same predicate, since 
its `compatibleAsIs` path reads two unprojected specs while a member here may 
be relabelled onto the key the operation clusters on.
   



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