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


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -1457,6 +1457,30 @@ object PartitioningCollection {
     case other => other.satisfies(required)
   }
 
+  /**
+   * The specs `p` offers for `distribution`, one per member that may serve 
it, built the way the
+   * planner builds them (`createShuffleSpec`): a keyed member that covers 
part of the operation's
+   * keys contributes the projection onto the keys it covers, which is the 
layout the alignment
+   * about to be planned will emit for it. A member that is not keyed has no 
projection to make and
+   * is asked for its own. A keyed member is admitted on `keysMaySatisfy`, any 
other member on
+   * `satisfies`, and a count the operation pinned is asked as it stands, the 
way
+   * `maySatisfyAfterProjection` asks it. That is the planner's admission of a 
member
+   * (`EnsureRequirements.createKeyedShuffleSpecs`) less the coverage of every 
operation key it
+   * also requires there: that requirement is the skew heuristic of
+   * `spark.sql.requireAllClusterKeysForCoPartition`, while a member covering 
a subset of the
+   * operation keys is a sound pairing.
+   */
+  private[sql] def specsForPairing(
+      p: Partitioning,
+      distribution: ClusteredDistribution): Seq[ShuffleSpec] =
+    flatten(p).flatMap {
+      case k: KeyedPartitioning =>
+        Option.when(distribution.requiredNumPartitions.forall(_ == 
k.numPartitions) &&
+          k.keysMaySatisfy(distribution))(k.createShuffleSpec(distribution))

Review Comment:
   Thanks @peter-toth! Narrowed in ce59a03fc24. `specsForPairing` now offers 
the layout each member reports (`KeyedShuffleSpec(k, distribution)`, nothing 
projected and nothing grouped), admitted on `satisfies`, so the member the plan 
holds is the member judged. An ungrouped one is the single exception, and it is 
confined to the configuration that builds one through 
`PartitioningCollection.mayUngroupedMember`; `validateInternal`'s per-side 
exemption reads the same predicate, so the two cannot drift. `keysSatisfy` is 
asked from outside the class now, so the family doc lists it beside 
`keysMaySatisfy`, and that is the question the ungrouped exception asks.
   
   The refusal in the default configuration is pinned in 
`ValidateRequirementsSuite` (the first test, after the conf block) and in 
`ShuffleSpecSuite` (the member is offered only where something builds one), and 
the shapes you would expect to still be refused are: a differing key order and 
a differing partition count, now with both permissions on, so the answer is the 
layouts' own rather than the admission's.
   
   Classification: regression of this change. The base asked `satisfies` per 
side, which an ungrouped keyed child fails in every configuration, so nothing 
here is a pre-existing hole being made visible.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -1457,6 +1457,30 @@ object PartitioningCollection {
     case other => other.satisfies(required)
   }
 
+  /**
+   * The specs `p` offers for `distribution`, one per member that may serve 
it, built the way the
+   * planner builds them (`createShuffleSpec`): a keyed member that covers 
part of the operation's
+   * keys contributes the projection onto the keys it covers, which is the 
layout the alignment
+   * about to be planned will emit for it. A member that is not keyed has no 
projection to make and
+   * is asked for its own. A keyed member is admitted on `keysMaySatisfy`, any 
other member on
+   * `satisfies`, and a count the operation pinned is asked as it stands, the 
way
+   * `maySatisfyAfterProjection` asks it. That is the planner's admission of a 
member
+   * (`EnsureRequirements.createKeyedShuffleSpecs`) less the coverage of every 
operation key it
+   * also requires there: that requirement is the skew heuristic of
+   * `spark.sql.requireAllClusterKeysForCoPartition`, while a member covering 
a subset of the
+   * operation keys is a sound pairing.
+   */
+  private[sql] def specsForPairing(

Review Comment:
   Thanks @peter-toth! All three corrected in ce59a03fc24, each now stating the 
invariant rather than a caller: `PartitioningCollection.createShuffleSpec` 
names the planner's `shuffleToCoPartition` as the reader of that admission set; 
`maySatisfyAfterProjection` says the validator does not read it and points at 
`specsForPairing`; `SinglePartitionShuffleSpec` says no production caller puts 
a collection on the `other` side, which I checked by enumerating the 
`isCompatibleWith` call sites: `pickCoPartitionTarget` flattens to leaves 
before it pairs, and `specsForPairing` offers leaves.
   
   One more site said the same thing and is corrected with them, outside the 
list you gave: the SPARK-59289 test comment in `ShuffleSpecSuite`, which read 
"the admission set here is what a finished plan is checked against by 
`ValidateRequirements`".
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/ValidateRequirements.scala:
##########
@@ -45,29 +45,60 @@ 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 with two of them is judged by 
their pairing below,
+    // and every other child, an operator with a single clustered child 
included, answers for
+    // itself.
+    val coPartitioning = children.length > 1 &&
+      requiredChildDistributions.forall(_.isInstanceOf[ClusteredDistribution])
+
     val satisfied = 
children.zip(requiredChildDistributions.zip(requiredChildOrderings)).forall {
       case (child, (distribution, ordering))
-          if !child.outputPartitioning.satisfies(distribution)
+          if (!child.outputPartitioning.satisfies(distribution) &&
+              !(coPartitioning &&
+                
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 co-partitioning 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

Review Comment:
   Thanks @peter-toth! Direction agreed, and it wants its own ticket rather 
than this PR: `KeyLayout` is master and branch-4.x only, while the repair here 
should reach further back.
   
   With the narrowing, no configuration stands in for the role any more: what 
is read says which shape a plan may report, not which part a side plays in the 
alignment.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -1457,6 +1457,30 @@ object PartitioningCollection {
     case other => other.satisfies(required)
   }
 
+  /**
+   * The specs `p` offers for `distribution`, one per member that may serve 
it, built the way the
+   * planner builds them (`createShuffleSpec`): a keyed member that covers 
part of the operation's
+   * keys contributes the projection onto the keys it covers, which is the 
layout the alignment
+   * about to be planned will emit for it. A member that is not keyed has no 
projection to make and
+   * is asked for its own. A keyed member is admitted on `keysMaySatisfy`, any 
other member on
+   * `satisfies`, and a count the operation pinned is asked as it stands, the 
way
+   * `maySatisfyAfterProjection` asks it. That is the planner's admission of a 
member

Review Comment:
   Thanks @peter-toth! Both corrected in ce59a03fc24. The helper's doc states 
the two things the planner does and it deliberately does not, since they build 
a layout the plan does not hold: no projection onto the operation keys under 
`allowKeysSubsetOfPartitionKeys` (a plan the permission applies to holds the 
projection already, in the grouping node `EnsureRequirements` inserted), and no 
grouping of an ungrouped member or re-sorting of a grouped one, `toGrouped` 
being what `tryCreate` builds from.
   
   `satisfiesForPairing` now says this is the question the `compatibleAsIs` 
path asks, and that the push path commits on the two sides it builds 
(`committed`, a `describesSameKeys` comparison of the layouts the reduce left), 
which is sound to ask here because a pair that took the reduce answers the 
strict question through `hasSameReducedKeys`.
   



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