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