ulysses-you commented on code in PR #58279:
URL: https://github.com/apache/spark/pull/58279#discussion_r3923892661


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/EnsureRequirements.scala:
##########
@@ -663,10 +666,8 @@ case class EnsureRequirements(
               } else {
                 (unwrappedLeft, leftSpec)
               }
-              // Original `KeyedPartitioning` can be obtained from the child 
directly if the child
-              // satisfied the distribution requirement; or from the child's 
child if it didn't as
-              // the child must be a `GroupPartitionsExec` inserted by 
`EnsureRequirement`
-              // to satisfy the distribution requirement.
+              // The pre-alignment plan of the side that keeps its splits: its 
partitioning
+              // still holds the original partition keys, one per input split.

Review Comment:
   Fixed as suggested: `partiallyClusteredPositions` comes from the innermost 
grouping, falling back to the spec's when there is none, and 
`partiallyClusteredSpec` is gone. Revert-verified -- without it the updated 
subset test throws exactly the measured `PartitioningCollection` invariant. The 
second split for id = 1 makes the test fail on master as well, where the 
stacked re-grouping duplicates rows. 83ed83560db
   



##########
sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala:
##########
@@ -3462,6 +3462,145 @@ class KeyGroupedPartitioningSuite
     }
   }
 
+  test("SPARK-58996: partially clustered join keeps its row count when 
EnsureRequirements " +
+      "re-runs") {
+    // The storage-partitioned join branch has no shuffle of its own, so the 
re-run of
+    // `EnsureRequirements` (triggered by the other branch below) reaches it. 
With
+    // `numRowsPerSplit = 1` the two id = 1 rows end up in two splits, which 
is what makes
+    // partial clustering replicate a side across two expected partitions. 
Regrouping that
+    // replicated layout on the second pass concatenated the replicas and 
replicated again,
+    // duplicating every id = 1 row.
+    val spColumns = Array(Column.create("id", LongType), Column.create("data", 
StringType))
+    createTable("sp1", spColumns, Array(identity("id")))
+    sql("INSERT INTO testcat.ns.sp1 VALUES (1, 'aa'), (1, 'ab'), (2, 'bb')")
+    createTable("sp2", spColumns, Array(identity("id")))
+    sql("INSERT INTO testcat.ns.sp2 VALUES (1, 'p'), (2, 'q')")
+
+    // Unpartitioned, so this branch's join materializes shuffle stages and is 
converted to a
+    // shuffled hash join, which hands the whole plan back to 
`EnsureRequirements`.
+    createTable("np1", spColumns, Array.empty)
+    sql("INSERT INTO testcat.ns.np1 VALUES (7, 'x')")
+    createTable("np2", spColumns, Array.empty)
+    sql("INSERT INTO testcat.ns.np2 VALUES (7, 'y')")
+
+    withSQLConf(
+        SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> "true",
+        SQLConf.V2_BUCKETING_PARTIALLY_CLUSTERED_DISTRIBUTION_ENABLED.key -> 
"true",
+        SQLConf.ADAPTIVE_MAX_SHUFFLE_HASH_JOIN_LOCAL_MAP_THRESHOLD.key -> 
"100m") {
+      val df = sql(
+        """
+          |SELECT /*+ MERGE(a, b) */ a.id AS k
+          |FROM testcat.ns.sp1 a JOIN testcat.ns.sp2 b ON a.id = b.id
+          |UNION ALL
+          |SELECT c.id AS k
+          |FROM testcat.ns.np1 c JOIN testcat.ns.np2 d ON c.id = d.id
+          |""".stripMargin)
+      checkAnswer(df, Seq(Row(1L), Row(1L), Row(2L), Row(7L)))
+
+      // The re-run must leave the storage-partitioned side shuffle-free, with 
the single
+      // grouping per child the first pass built: a grouping stacked over 
another re-derives the
+      // alignment from an already-aligned layout and duplicates rows.
+      assert(collectShuffles(df.queryExecution.executedPlan).isEmpty,
+        "the storage-partitioned join must stay shuffle-free")
+      val groupPartitions = 
collectGroupPartitions(df.queryExecution.executedPlan)
+      assert(groupPartitions.nonEmpty, "the storage-partitioned join must keep 
its groupings")
+      groupPartitions.foreach { g =>
+        assert(collectAllGroupPartitions(g.child).isEmpty,
+          s"a GroupPartitionsExec must not be stacked over 
another:\n${g.treeString}")
+      }
+    }
+  }
+
+  test("SPARK-58996: partially clustered join keeps its replicate-side choice 
when " +
+      "EnsureRequirements re-runs") {
+    // The smaller side is replicated, chosen by plan statistics on the first 
pass. On the re-run
+    // the statistics must be read from the pre-alignment plan again: reading 
them from the
+    // aligned layout skips the statistics branch and deterministically flips 
the choice. The
+    // flipped side then distributes where it used to replicate, and since the 
smaller side holds
+    // more splits for id = 1 than the larger one, its raw splits overflow the 
expected count
+    // (`padTo` never truncates) and the join sides end up with an unequal 
number of partitions.
+    val spColumns = Array(Column.create("id", LongType), Column.create("data", 
StringType))
+    createTable("sp_small", spColumns, Array(identity("id")))
+    sql("INSERT INTO testcat.ns.sp_small VALUES " +
+        "(1, 'a1'), (1, 'a2'), (1, 'a3'), (1, 'a4'), (1, 'a5')")

Review Comment:
   Adopted -- the test now fails on master as well. One adjustment: with the 
single-split shape on its own, reverting the guard stayed self-consistent in my 
arms (the flipped side's one split per key fills the aligned counts), so the 
data keeps the five splits for id = 1 on the smaller side too: with them the 
flipped distribute side overflows the expected count of one, and reverting the 
guard fails the `PartitioningCollection` invariant (measured). 83ed83560db
   



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