HyukjinKwon commented on code in PR #59317:
URL: https://github.com/apache/spark/pull/59317#discussion_r4280081486


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -667,12 +667,22 @@ private[sql] object GroupPartitionsExec {
       val splits = keyMap.getOrElse(key, Seq.empty)
       numMatchedPartitions += splits.size
       if (distributePartitions) {
+        // `padTo` only pads, never truncates, so a key holding more splits 
than its expected
+        // count would be emitted more times than the count says while 
`isGrouped` still reads as

Review Comment:
   **Nit (P3):** This says an over-counted key is emitted too many times "while 
`isGrouped` still reads as grouped", but two lines up `if (numSplits > 1) 
isGrouped = false` clears the flag as soon as any expected count exceeds 1. So 
an overflow is reported as grouped only when every count is 1. The previous 
revision (aed95ec) had the accurate "can still read as grouped". The closing 
sentence also lost its verb, and "committed-gate shuffle fallback" is a new 
coinage: it reads as if the fallback happens when the gate commits, whereas it 
happens when the `committed` gate in `EnsureRequirements` refuses the pair.
   
   Something like:
   
   ```scala
           // `padTo` only pads, never truncates, so a key holding more splits 
than its expected
           // count would be emitted more times than the count says, while 
`isGrouped` can still
           // read as grouped. No producer does this today: the join split 
reduces its keys into this
           // node's space before counting them (`EnsureRequirements`), and the 
ordering split counts
           // the same keys it groups, so every count matches its splits. The 
assert surfaces any
           // future miscount here instead of as a shuffle fallback at the 
`committed` gate in
           // `EnsureRequirements` or as a bad grouped layout.
   ```
   
   **Verification:**
   - **Inspection:** The guard comment's `isGrouped` clause is conditional, its 
closing sentence names the `committed` gate in `EnsureRequirements`, and the 
comment lines stay ASCII and within 100 characters.



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExecSuite.scala:
##########
@@ -74,6 +74,37 @@ class GroupPartitionsExecSuite extends SharedSparkSession {
     }
   }
 
+  test("SPARK-59234: a distributed key with more splits than its expected 
count fails fast") {
+    // Keys [(1,1), (1,2)] projected onto position 0 both become key 1, so the 
child holds two
+    // splits for it. Declaring an expected count of 1 for key 1 while the 
node is ungrouped for a
+    // join split is the inconsistency this guard catches: `padTo` would emit 
both splits while the
+    // count says one, leaving duplicate keys under a grouped report.
+    val childKp = KeyedPartitioning(Seq(exprA, exprB), Seq(row(1, 1), row(1, 
2)))
+    def keyOf(a: Int): InternalRowComparableWrapper =
+      InternalRowComparableWrapper(row(a), Seq(exprA))
+    val e = intercept[AssertionError] {
+      GroupPartitionsExec(DummySparkPlan(outputPartitioning = childKp),
+        joinKeyPositions = Some(Seq(0)),
+        expectedPartitionKeys = Some(Seq(keyOf(1) -> 1)),
+        ungroupingOrigin = Some(SPLIT_FOR_JOIN))
+    }
+    assert(e.getMessage.contains("more splits"))
+  }
+
+  test("SPARK-59234: the replicate branch emits numSplits entries and does not 
overflow") {
+    // The same over-count input as the distribute case, but ungrouped for a 
replicate rather than a
+    // split. REPLICATED_FOR_JOIN takes the else branch, which emits exactly 
numSplits entries per
+    // key whatever the split count, so it cannot overflow and builds without 
the guard firing.
+    val childKp = KeyedPartitioning(Seq(exprA, exprB), Seq(row(1, 1), row(1, 
2)))
+    def keyOf(a: Int): InternalRowComparableWrapper =
+      InternalRowComparableWrapper(row(a), Seq(exprA))
+    val gpe = GroupPartitionsExec(DummySparkPlan(outputPartitioning = childKp),
+      joinKeyPositions = Some(Seq(0)),
+      expectedPartitionKeys = Some(Seq(keyOf(1) -> 1)),
+      ungroupingOrigin = Some(REPLICATED_FOR_JOIN))
+    assert(gpe.groupedPartitions.size === 1, "the one expected partition 
replicates both splits")

Review Comment:
   **Nit (P3):** This checks only the slot count, but the message says the one 
slot replicates both splits, and that is the property that matters here: the 
replicated side has to keep the whole key group, or partial clustering loses 
join rows. A later "symmetric" guard that truncated the group (e.g. 
`splits.take(numSplits)`) would keep `groupedPartitions.size === 1` and pass. 
Asserting the indices pins it:
   
   ```suggestion
       assert(gpe.groupedPartitions.map(_._2) === Seq(Seq(0, 1)),
         "the one expected partition replicates both splits")
   ```
   
   **Verification:**
   - **Regression:** A replicate-branch change that drops a split for an 
over-counted key (for example, truncating the group to `numSplits` entries) 
makes the fixture fail.
   - **Behavior:** With the current implementation, a REPLICATED_FOR_JOIN node 
over two splits under one key with an expected count of 1 builds and emits one 
slot holding both splits.



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