Skip to content

Commit 77c17e9

Browse files
committed
[SPARK-58996][SQL] Pin the first-pass replicate-side choice and narrow the count helper
The fallback test now also covers a bare first pass, where the pre-alignment split count and the old distinct-key count pick different sides, and the comment names that first-pass change. The keyed-partition count helper is narrowed to private[sql]. Assisted-by: Claude Code
1 parent 94afe6e commit 77c17e9

3 files changed

Lines changed: 56 additions & 24 deletions

File tree

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,11 +1134,12 @@ object PartitioningCollection {
11341134
}
11351135

11361136
/**
1137-
* The number of partitions of the [[KeyedPartitioning]] representing the keyed members of
1138-
* `partitioning`, if any. Collections validate on construction that their keyed members agree,
1139-
* so the representative's count stands for all of them.
1137+
* The number of partitions of the keyed members of `partitioning`, if it has any. A
1138+
* collection requires all of its members to agree on the count, keyed or not, so the answer
1139+
* is `partitioning.numPartitions` whenever a keyed member exists; the representative only
1140+
* decides whether there is one.
11401141
*/
1141-
def numKeyedPartitions(partitioning: Partitioning): Option[Int] =
1142+
private[sql] def numKeyedPartitions(partitioning: Partitioning): Option[Int] =
11421143
representativeOf(partitioning).map(_.numPartitions)
11431144

11441145
/**

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,8 @@ case class EnsureRequirements(
646646
// apply the grouping & replication of partitions. The counts read the
647647
// pre-alignment plans, for the same reason the statistics do: on a re-run both
648648
// aligned reports hold the same number of keys, so comparing them decides nothing.
649+
// This also changes a first pass, which compared the aligned report's distinct
650+
// keys rather than the splits behind them.
649651
logInfo("Using number of partitions to determine which side of join " +
650652
"to fully cluster partition values")
651653
PartitioningCollection.numKeyedPartitions(unwrappedLeft.outputPartitioning)

sql/core/src/test/scala/org/apache/spark/sql/execution/exchange/EnsureRequirementsSuite.scala

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2069,12 +2069,28 @@ class EnsureRequirementsSuite extends SharedSparkSession {
20692069

20702070
test("SPARK-58996: the replicate-side fallback counts pre-alignment partitions") {
20712071
// The replicate side is chosen by plan statistics; when there is none (the dummy plans carry
2072-
// no `logicalLink`, which forces the fallback), the side with fewer partitions is picked. On
2073-
// a re-run the join children arrive already aligned, and both aligned reports hold the same
2074-
// keys, so counting them decides nothing: the counts must come from the pre-alignment
2075-
// partitioning. Each arm hands the rule the output shape of a first pass -- a local sort
2076-
// over an aligned `GroupPartitionsExec` -- and reads the replicate-side choice back off the
2077-
// `distributePartitions` flags.
2072+
// no `logicalLink`, which forces the fallback), the side with fewer partitions is picked.
2073+
// The counts must come from the pre-alignment partitioning. On a re-run the join children
2074+
// arrive already aligned -- a local sort over an aligned `GroupPartitionsExec` -- and both
2075+
// aligned reports hold the same keys, so counting them decides nothing. On a bare first pass
2076+
// the pre-fix count read the aligned report's distinct keys, because the children loop had
2077+
// already wrapped the non-grouped side, and picked a different side wherever one holds more
2078+
// than one split per key. Both shapes read the choice back off the `distributePartitions`
2079+
// flags.
2080+
def distribute(child: SparkPlan): Boolean = child.collectFirst {
2081+
case g: GroupPartitionsExec => g
2082+
}.get.distributePartitions
2083+
2084+
def flags(smj: SparkPlan): (Boolean, Boolean) =
2085+
withSQLConf(
2086+
SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> "true",
2087+
SQLConf.V2_BUCKETING_PARTIALLY_CLUSTERED_DISTRIBUTION_ENABLED.key -> "true") {
2088+
val Seq(newLeft, newRight) = EnsureRequirements.apply(smj).children
2089+
(distribute(newLeft), distribute(newRight))
2090+
}
2091+
2092+
// The re-run shape: each child is the output of a first pass, a local sort over an aligned
2093+
// `GroupPartitionsExec`.
20782094
def distributeFlags(
20792095
leftKeys: Seq[InternalRow], leftSplits: Int,
20802096
rightKeys: Seq[InternalRow], rightSplits: Int): (Boolean, Boolean) = {
@@ -2085,22 +2101,20 @@ class EnsureRequirementsSuite extends SharedSparkSession {
20852101
(InternalRowComparableWrapper(InternalRow(1), Seq(expr)), splits))))
20862102
SortExec(Seq(SortOrder(expr, Ascending)), global = false, gpe)
20872103
}
2088-
val smj = SortMergeJoinExec(Seq(exprA), Seq(exprB), Inner, None,
2089-
alignedChild(exprA, leftKeys, leftSplits), alignedChild(exprB, rightKeys, rightSplits))
2090-
withSQLConf(
2091-
SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> "true",
2092-
SQLConf.V2_BUCKETING_PARTIALLY_CLUSTERED_DISTRIBUTION_ENABLED.key -> "true") {
2093-
val Seq(newLeft, newRight) = EnsureRequirements.apply(smj).children
2094-
def distribute(child: SparkPlan): Boolean = child.collectFirst {
2095-
case g: GroupPartitionsExec => g
2096-
}.get.distributePartitions
2097-
(distribute(newLeft), distribute(newRight))
2098-
}
2104+
flags(SortMergeJoinExec(Seq(exprA), Seq(exprB), Inner, None,
2105+
alignedChild(exprA, leftKeys, leftSplits), alignedChild(exprB, rightKeys, rightSplits)))
20992106
}
21002107

2101-
// The left side holds one partition against three on the right: it is replicated, so it
2102-
// does not distribute. Counting the aligned reports instead sees one key on both sides and
2103-
// always picks the right side.
2108+
// The bare first-pass shape: the children are the scans themselves.
2109+
def firstPassFlags(
2110+
leftKeys: Seq[InternalRow], rightKeys: Seq[InternalRow]): (Boolean, Boolean) =
2111+
flags(SortMergeJoinExec(Seq(exprA), Seq(exprB), Inner, None,
2112+
DummySparkPlan(outputPartitioning = KeyedPartitioning(Seq(exprA), leftKeys)),
2113+
DummySparkPlan(outputPartitioning = KeyedPartitioning(Seq(exprB), rightKeys))))
2114+
2115+
// Re-run shape. The left side holds one partition against three on the right: it is
2116+
// replicated, so it does not distribute. Counting the aligned reports instead sees one key
2117+
// on both sides and always picks the right side.
21042118
assert(distributeFlags(
21052119
leftKeys = Seq(InternalRow(1)), leftSplits = 1,
21062120
rightKeys = Seq(InternalRow(1), InternalRow(1), InternalRow(1)), rightSplits = 3) ===
@@ -2111,6 +2125,21 @@ class EnsureRequirementsSuite extends SharedSparkSession {
21112125
leftKeys = Seq(InternalRow(1), InternalRow(1), InternalRow(1)), leftSplits = 3,
21122126
rightKeys = Seq(InternalRow(1)), rightSplits = 1) ===
21132127
((true, false)))
2128+
2129+
// Bare first pass where the pre-fix count disagrees: the left side holds three splits under
2130+
// one distinct key, the right two splits under two keys. The pre-fix count saw one distinct
2131+
// key against two and replicated the left side; the pre-alignment count replicates the side
2132+
// with fewer splits.
2133+
assert(firstPassFlags(
2134+
leftKeys = Seq(InternalRow(1), InternalRow(1), InternalRow(1)),
2135+
rightKeys = Seq(InternalRow(1), InternalRow(2))) ===
2136+
((true, false)))
2137+
2138+
// The first-pass control where both counts agree.
2139+
assert(firstPassFlags(
2140+
leftKeys = Seq(InternalRow(1), InternalRow(2)),
2141+
rightKeys = Seq(InternalRow(1), InternalRow(2), InternalRow(3))) ===
2142+
((false, true)))
21142143
}
21152144

21162145
test("SPARK-58996: a single-child operator over a partially clustered layout still gets " +

0 commit comments

Comments
 (0)