peter-toth commented on code in PR #58339:
URL: https://github.com/apache/spark/pull/58339#discussion_r3924348492


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -219,6 +236,21 @@ case class GroupPartitionsExec(
     PartitionGrouping(partitions, isGrouped, isCollapsed)
   }
 
+  /**
+   * Whether this node's grouping leaves every partition where it was and 
keeps all of them, i.e.
+   * output partition i holds exactly input partition i and there is one 
output per input. That is
+   * the only grouping that keeps a marked layout's undeclared rows at 
hash(key) % numPartitions:
+   * a grouping that drops trailing declared keys still reads identity for 
every group it keeps,
+   * but the partition count shrinks and the hash modulus with it. The 
`forall` stops at the first
+   * moved partition, so a reorder or coalesce is rejected without a full scan.
+   */
+  @transient private lazy val identityGrouping: Boolean =

Review Comment:
   **Finding 26.** `identityGrouping` compares partition indices and counts, 
and never asks whether the grouping rewrote the keys. The keys the node goes on 
to declare are `grouping.partitions.map(_._1)`, which are the projected and 
reduced ones, so a reduction that is injective and order-preserving on the 
declared keys passes the predicate.
   
   Take a marked child with keys `[0, 1]` and `reducers = Some(Seq(Some(r)))` 
where `r` maps `0 -> 0` and `1 -> 1`. That is two groups, one input partition 
each, in the same order, count unchanged, so `identityGrouping` is `true`. The 
node then reports a marked `KeyedPartitioning` whose declared keys live in the 
reducer's key space, while its undeclared rows still sit at `hash(originalKey) 
% 2`. The claim's routing is about a different key than the one it declares.
   
   That contradicts two statements this PR adds. Rule (1) of the `@param` at 
`sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:601`
 lists `reducer` among the coarsening sites that must drop the keyed 
partitioning. The comment at line 85 above says a reduction moves the rows. 
Only a collapsing reduction is caught, and it is the count clause that catches 
it, not the reduction. `GroupPartitionsExecSuite`'s `mod 2` over `[1, 2, 3]` 
collapses, so it passes either way.
   
   Not reachable today. I said the opposite at round 6, where I wrote that the 
give-up catches a contract-violating self-reducer, and it does not. 
`areKeysCompatible`'s marked path demands `isSameFunction`, and 
`BucketFunction.reducer` returns null for equal bucket counts, so 
`reducersBothWays` gives `(None, None)`. Only a connector whose `reducer` is 
non-identity for its own function at its own parameters gets here. The 
projection half is closed the same way, since none of the three producers of 
`joinKeyPositions` can narrow a marked partitioning.
   
   So this is defence in depth, and it is one clause. `grouping` already 
computes exactly the predicate:
   
   ```scala
   val keysChanged =
     joinKeyPositions.exists(_.length < childKp.expressions.length) || 
reducers.isDefined
   ```
   
   Carry it on `PartitionGrouping` and add `!grouping.keysChanged` to 
`identityGrouping`. Tightening the two comments instead would close it too, but 
the clause is cheaper than the prose.
   



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