peter-toth opened a new pull request, #58531:
URL: https://github.com/apache/spark/pull/58531

   ### What changes were proposed in this pull request?
   
   **Stacked on #58527 (SPARK-59080), which is the first commit here. Please 
review only the second commit, and do not merge this before #58527. Draft until 
then.**
   
   `ShuffleSpecCollection` answers two kinds of question with one type. 
Matching (`isCompatibleWith`) is a collection question, and succeeding when 
*any* member matches is why the type exists. The shuffle template 
(`createPartitioning`, `numPartitions`) needs one member, and the collection 
reads `specs.head`, whichever member the alias cross-product enumerated first.
   
   A new `LeafShuffleSpec` sub-trait carries the two single-member methods, the 
seven concrete specs extend it, and the collection extends `ShuffleSpec` alone:
   
   ```
   ShuffleSpec                      isCompatibleWith, canCreatePartitioning
     <- LeafShuffleSpec             + numPartitions, createPartitioning
          SinglePartitionShuffleSpec, RangeShuffleSpec, HashShuffleSpec,
          NullAwareHashShuffleSpec, CoalescedHashShuffleSpec, KeyedShuffleSpec,
          ShufflePartitionIdPassThroughSpec
     <- ShuffleSpecCollection
   ```
   
   `ShuffleSpec` is sealed, so those two are the only kinds. That is what lets 
the flattening helper return `Seq[LeafShuffleSpec]` without a fallback case, 
and it is the only thing giving the split teeth. Nothing outside 
`partitioning.scala` extends `ShuffleSpec`. The helper moves out of 
`EnsureRequirements` to `ShuffleSpecCollection.flatten`, beside the sealed 
hierarchy that makes its match total and alongside the existing 
`PartitioningCollection.flatten`.
   
   `ShuffleSpecCollection` also takes over the `require(specs.nonEmpty, ...)` 
that `numPartitions` used to carry, so the invariant outlives the method that 
stated it.
   
   ### Why are the changes needed?
   
   The two removed methods are not wrong in the same way.
   
   `createPartitioning` has **no local answer at all**: the right member is the 
one the *other* side matched, which is only visible to the caller comparing the 
two sides. SPARK-59080 moved that decision into `EnsureRequirements`, so after 
#58527 the method has no production caller, and only a runtime `require` stands 
between a future caller and a wrong partitioning. The compiler should say that 
instead.
   
   `numPartitions` has **two consumers that want two different aggregations**, 
which is why it should not be answered at all rather than answered as one of 
them:
   
   - `EnsureRequirements` ranks the children by parallelism to pick the 
reference layout. It wants the **max**, and it now takes it over the flattened 
members. That also makes the ranking agree with the member the shuffle is then 
built from, which is already the finest one. Ranking on the head while building 
from the max was incoherent.
   - `SinglePartitionShuffleSpec.isCompatibleWith` asks whether the other side 
is a single partition. It wants **exists**, and it now unwraps a collection the 
way every other spec already does.
   
   ### Does this PR introduce _any_ user-facing change?
   
   No.
   
   Both behaviour changes are exact no-ops unless 
`spark.sql.sources.v2.bucketing.allowKeysSubsetOfPartitionKeys.enabled` is on, 
which it is not by default. `PartitioningCollection` requires its members to 
agree on `numPartitions`, and every spec reports its own partitioning's count, 
so `max` equals `head` and `exists(_.numPartitions == 1)` equals 
`head.numPartitions == 1`. The only producer of a disagreeing member is the 
`project(joinKeyPositions).toGrouped` branch that config gates.
   
   One thing worth flagging rather than leaving to be found. The only 
production consumer of the changed `SinglePartitionShuffleSpec` answer is 
`ValidateRequirements`, through 
`specs.tail.forall(_.isCompatibleWith(specs.head))`. `EnsureRequirements` never 
reaches it, because `SinglePartitionShuffleSpec.canCreatePartitioning` is false 
and the collection's is a `forall`. And there the answer is about a layout 
nobody builds: a projected `KeyedShuffleSpec`'s `numPartitions` is the count 
*after* the `GroupPartitionsExec` that `EnsureRequirements` would insert, while 
`ValidateRequirements` inserts nothing. So "the other side is a single 
partition" can be true of a child that physically has more. The head read had 
that same problem on a different arbitrary member, so this is a pre-existing 
conflation rather than a regression, but `exists` does widen it from one member 
to any. I could not build a query that reaches it: the shape needs a collection 
child, a `SinglePartition` sibling and an AQ
 E shuffle-read rule firing on that pair, and those rules produce 
`CoalescedHashPartitioning`, never `SinglePartition`. I kept `exists` because 
that is how compatibility with a collection is defined everywhere else, and I 
did not add a test for it, because pinning an answer I am not sure is right 
would be worse than leaving it uncovered.
   
   ### How was this patch tested?
   
   Three new tests, and one existing assertion deleted.
   
   A plain fail-on-base measurement is not possible here: the new tests name 
`LeafShuffleSpec`, so they cannot compile against the parent commit. Instead I 
simulated the old head read under the new types, making the ranking read 
`flatten(spec).head` and `SinglePartitionShuffleSpec`'s collection case read 
`isCompatibleWith(specs.head)`. Both behavioural tests fail under that and pass 
without it.
   
   | test | under the simulated head read |
   |---|---|
   | `ShuffleSpecSuite`: a single-partition side matches a collection through 
any member | fails |
   | `EnsureRequirementsSuite`: a collection is ranked on its best member, not 
on whichever came first | fails |
   | `ShuffleSpecSuite`: flattening reaches the members of a nested collection 
| n/a, covers the recursion |
   
   Both behavioural tests assert their member counts as an ordered `Seq` rather 
than a `Set`, so each pins its own premise: that the member a head read would 
take is the wrong one.
   
   Deleted: `createPartitioning: other specs` had a case asserting that a 
collection delegates to `specs.head`. The method is gone and the type now 
rejects the call. The same test's expected `className` in the 
`UNSUPPORTED_CALL` error moves from `ShuffleSpec` to `LeafShuffleSpec`, since 
the defaulted method moved.
   
   Green: `ShuffleSpecSuite`, `DistributionSuite`, `EnsureRequirementsSuite`, 
`ValidateRequirementsSuite`, `KeyGroupedPartitioningSuite`, `PlannerSuite`, 306 
tests in all. `dev/lint-scala` is clean.
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   Generated-by: Claude Code
   


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