Skip to content

Commit 9f2e46a

Browse files
committed
[SPARK-59272][SQL][FOLLOWUP] Split the two refusal cases in the comments, and pin the new one
Three comment corrections from a review round, and the unit test the changed contract was missing. `createShuffleSpec` refuses to project any marked layout, but what the unprojected spec is then good for differs between the two cases. Where every partition expression covers a clustering key the spec is usable, and a consumer is laid out on this side's own key order. Where the layout narrows, one `keyPositions` entry is empty and `canCreatePartitioning` and `areKeysCompatible` both turn the spec away, which is what the sentence this commit brings back used to say. The `@param joinKeyPositions` doc and the comment at the guard now split the two. `ShuffleSpecSuite` gains the covering case beside the narrowing one it already had, so the contract this PR changed is pinned at the unit that changed. It fails with the guard back at its narrowing-only form. The gate comment in `EnsureRequirements` loses a sentence fragment and the cost argument against re-asking on the `compatibleAsIs` path. `describesSameKeys` walks the same keys on the push path, so the reason that holds is the other one already given: there the children are the ones the pairing read. The `SPARK-59050` test comment numbered the shuffles wrong. The final join's own shuffle cannot have existed while that join storage-partitioned, so before SPARK-59050 the plan held the first and third of today's four. The fourth is that join's one-side shuffle, not a re-shuffle of a regrouped side, since a declined pairing discards the node it tried. Raised by dongjoon-hyun.
1 parent f88b7bc commit 9f2e46a

4 files changed

Lines changed: 50 additions & 22 deletions

File tree

sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -917,8 +917,13 @@ case class KeyedPartitioning(
917917
// either would also need a `GroupPartitionsExec` on this side, and that node gives the keyed
918918
// claim up rather than perform a non-identity regrouping of a marked layout.
919919
//
920-
// The unprojected spec that comes back is still usable, and is what a consumer is offered:
921-
// the other child is laid out on the keys in the order this one reports them.
920+
// What the unprojected spec is good for depends on the layout. Where every partition
921+
// expression covers a clustering key, so the projection would only have re-sorted, the spec
922+
// is usable and is what a consumer is offered: the other child is laid out on the keys in the
923+
// order this one reports them. Where the layout narrows, one `keyPositions` entry is empty,
924+
// and `canCreatePartitioning` and `areKeysCompatible` both turn the spec away. Nothing is
925+
// lost there, because `keysSatisfy` does not call such a child grouped on the operation keys
926+
// either.
922927
if (mayContainUnknownPartitionKeys) {
923928
return result
924929
}
@@ -1770,12 +1775,14 @@ case class IdentityReducer(transform: TransformExpression) extends Reducer[Any,
17701775
* projection changed it. `None` therefore means `partitioning` is the one
17711776
* the child reports, and a consumer needs no `GroupPartitionsExec` to
17721777
* produce it. Only `v2BucketingAllowKeysSubsetOfPartitionKeys` projects at
1773-
* all, and it reaches `None` two ways: an identity projection over already
1774-
* grouped and sorted keys rebuilds the same partitioning, and a marked
1775-
* claim is refused outright, since neither narrowing nor sorting its keys
1776-
* leaves the routing it promises for its undeclared rows. Both say the
1777-
* child is grouped on the operation keys as it stands, and both leave a
1778-
* spec a consumer can be laid out on. See
1778+
* all, and it reaches `None` two ways. An identity projection over already
1779+
* grouped and sorted keys rebuilds the same partitioning. A marked claim is
1780+
* refused outright, since neither narrowing nor sorting its keys leaves the
1781+
* routing it promises for its undeclared rows. The first always leaves a
1782+
* spec a consumer can be laid out on, and so does the second where every
1783+
* partition expression covers a clustering key. A marked claim that narrows
1784+
* does not: one `keyPositions` entry is empty there, which
1785+
* `canCreatePartitioning` and `areKeysCompatible` both turn away. See
17791786
* `KeyedPartitioning.createShuffleSpec`.
17801787
*/
17811788
case class KeyedShuffleSpec(

sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/ShuffleSpecSuite.scala

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,29 @@ class ShuffleSpecSuite extends SparkFunSuite with SQLHelper {
650650
}
651651
}
652652

653+
test("createShuffleSpec: a marked covering projection yields a usable unprojected spec") {
654+
val a = $"a".int
655+
// Grouped but not sorted, which is what a one-side shuffle onto a union's key order leaves
656+
// behind. Every partition expression covers a clustering key here, so the projection that is
657+
// refused would only have re-sorted the keys.
658+
val marked = KeyedPartitioning(Seq(a),
659+
Seq(InternalRow(3), InternalRow(4), InternalRow(1), InternalRow(2)))
660+
.withLayout(_.copy(mayContainUnknownPartitionKeys = true))
661+
assert(marked.isGrouped, "test setup: the keys are distinct, so the layout is grouped")
662+
withSQLConf(
663+
SQLConf.V2_BUCKETING_SHUFFLE_ENABLED.key -> "true",
664+
SQLConf.V2_BUCKETING_ALLOW_KEYS_SUBSET_OF_PARTITION_KEYS.key -> "true") {
665+
val spec = marked.createShuffleSpec(ClusteredDistribution(Seq(a)))
666+
.asInstanceOf[KeyedShuffleSpec]
667+
// The refusal leaves the child's own layout, and unlike the narrowing case above that spec
668+
// is usable: a consumer is laid out on the keys in the order this side reports them. Sorting
669+
// them would send the consumer's undeclared rows where this side does not hold them.
670+
assert(spec.joinKeyPositions.isEmpty)
671+
assert(spec.canCreatePartitioning)
672+
assert(spec.partitioning.partitionKeys == marked.partitionKeys)
673+
}
674+
}
675+
653676
test("areKeysCompatible: unknown keys require the same function, not just a compatible one") {
654677
// A bucket-like reducible function: like the built-in `bucket`, a pair of the same function
655678
// with coarser/finer bucket counts is compatible (a reducer exists) but not the same

sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/EnsureRequirements.scala

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -794,27 +794,25 @@ case class EnsureRequirements(
794794
}
795795

796796
// The pairing is only worth committing to if both children still declare the same aligned key
797-
// sequence once the grouping has been pushed into them. They can fail that. A
798-
// `GroupPartitionsExec` gives up its keyed claim when it turns out to regroup a layout that
799-
// pins undeclared rows to `hash(key) % numPartitions` (see
800-
// `KeyLayout.mayContainUnknownPartitionKeys`), and only the node knows the permutation it
801-
// performs, so that answer arrives after the pairing was chosen. Asking before returning is
802-
// what keeps the join from skipping both shuffles for a child that no longer satisfies its
803-
// distribution, which is a plan `ValidateRequirements` rejects and every AQE rule that needs a
804-
// valid plan then refuses to touch.
797+
// sequence once the grouping has been pushed into them. A `GroupPartitionsExec` gives up its
798+
// keyed claim when it turns out to regroup a layout that pins undeclared rows to
799+
// `hash(key) % numPartitions` (see `KeyLayout.mayContainUnknownPartitionKeys`), and only the
800+
// node knows the permutation it performs, so that answer arrives after the pairing was chosen.
801+
// Asking before returning is what keeps the join from skipping both shuffles for a child that
802+
// no longer satisfies its distribution, which is a plan `ValidateRequirements` rejects and
803+
// every AQE rule that needs a valid plan then refuses to touch.
805804
//
806805
// The check is pairwise, not a per-side `satisfies`. Partially clustered distribution leaves
807806
// both children value-aligned yet not grouped on purpose, so a per-side gate would refuse that
808807
// whole family. What both sides owe each other is the key sequence `alignToExpectedKeys`
809808
// guarantees, each key repeated as many times as the merge expects, whichever side replicates.
810-
// Through `KeyLayout.describesSameKeys`, which carries the reason the key types are compared as
811-
// well as the rows.
809+
// `KeyLayout.describesSameKeys` asks exactly that, and it compares the key types as well as
810+
// the rows.
812811
//
813812
// Only the push branch rebuilds the children, so only it has to be asked. Where it did not run,
814813
// the children are the ones the pairing read: `KeyedShuffleSpec.isCompatibleWith` already ends
815814
// in `describesSameKeys`, and all keyed members of a `PartitioningCollection` share one
816815
// `KeyLayout`, so whichever member the spec matched on declares what the representative does.
817-
// Asking again there would walk both sides' partition keys for an answer that cannot be no.
818816
def declaredLayout(plan: SparkPlan): Option[KeyLayout] =
819817
PartitioningCollection.representativeOf(plan.outputPartitioning).map(_.layout)
820818
def sidesDeclareSameKeys: Boolean =

sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8249,9 +8249,9 @@ class KeyGroupedPartitioningSuite
82498249
checkAnswer(df, expected)
82508250
// The regrouped marked side can no longer claim the hash-routing contract. Four one-side
82518251
// shuffles, all keyed with unknown partition keys: rt onto the union, rs onto the marked
8252-
// side once the second join declines, rt2 onto ra2, and the final join's re-shuffle of the
8253-
// side that was regrouped. Before SPARK-59050 the final join storage-partitioned (only the
8254-
// first, third and fourth shuffles) and silently lost the id=5 row.
8252+
// side once the second join declines, rt2 onto ra2, and the final join's one-side shuffle.
8253+
// Before SPARK-59050 the final join storage-partitioned (only the first and third
8254+
// shuffles) and silently lost the id=5 row.
82558255
//
82568256
// The second join's shuffle is this fix's. The give-up happens inside the node, so it used
82578257
// to arrive after that join had already committed to the pairing and skipped both

0 commit comments

Comments
 (0)