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


##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/ShuffleSpecSuite.scala:
##########
@@ -690,4 +690,34 @@ class ShuffleSpecSuite extends SparkFunSuite with 
SQLHelper {
       expected = false
     )
   }
+
+  test("SPARK-59080: a collection whose members cover different key subsets 
disagrees") {

Review Comment:
   Exactly right, and your list of the three is exactly the set: the 
coarse-member-first plan test, the Pandas cogroup one with the swapped key 
order, and the e2e. All three fail on the pre-PR code; this fourth one passes 
there by design, which the PR description says too.
   
   It became the guard's test rather than the fix's when the design moved: an 
earlier shape narrowed the collection so the `require` was satisfiable, and 
this test asserted that narrowing. The fix stopped touching catalyst, so what 
was left worth pinning is the contract — the members disagree on purpose, every 
one of them stays available for `isCompatibleWith`, and asking the collection 
for a single partitioning throws.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/EnsureRequirements.scala:
##########
@@ -247,30 +246,50 @@ case class EnsureRequirements(
         }
       }
 
+      // A `ShuffleSpecCollection` answers `isCompatibleWith` if *any* of its 
members does, so the
+      // collection alone does not say which member the sides agreed on. The 
projection pushed into
+      // a compatible child and the partitioning built for a re-shuffled child 
both have to come
+      // from one member, otherwise the sides end up grouped on different 
keys, or on a key set the
+      // child does not even have. Pick that member once, preferring the 
finest when several
+      // qualify. Only the branch that shuffles a child reads these, hence 
`lazy`.
+      lazy val matchedIndexes = bestSpecOpt.toSeq.flatMap { best =>
+        childrenIndexes.filter(i => best.isCompatibleWith(specs(i)))
+      }
+      lazy val bestMemberOpt = bestSpecOpt.flatMap { best =>
+        val matchedMembers = matchedIndexes.map(i => flattenSpec(specs(i)))
+        // No member serving every matched child means there is no layout to 
align them on, so they
+        // all take the ordinary shuffle. That needs three or more clustered 
children, since with
+        // two the member that reported the match serves both, and no operator 
has three today.
+        flattenSpec(best)
+          .filter(m => matchedMembers.forall(_.exists(m.isCompatibleWith)))

Review Comment:
   Agreed, and thank you for checking the `requiredChildDistribution` 
implementations independently — I measured the same thing and got the same 
answer: joins and cogroups are the only operators with more than one clustered 
child, and both have exactly two.
   
   One thing worth adding, since it changes how alarming the untested branch 
is. That fallback used to *guess* a member: it took the finest of all of them 
when no single one served every matched child. A blind review round pointed out 
that a child then gets declared co-partitioned on a layout it does not have, 
with no shuffle inserted — silent wrong results. So it was changed to bail out 
instead, which is what you see. The untested arm is now the conservative one, 
and if an operator with three clustered children ever lands, the failure mode 
is a lost SPJ rather than a wrong answer. A test then, agreed.
   



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