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


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -528,22 +554,25 @@ case class CoalescedNullAwareHashPartitioning(
  *                      guaranteed after projection. May contain duplicates 
when ungrouped.
  * @param isGrouped Whether partition keys are unique (no duplicates). 
Computed on first
  *                  creation, then preserved through copy operations to avoid 
recomputation.
- * @param isNarrowed Whether this partitioning was derived from a 
finer-grained one by dropping key
- *                   positions (e.g. via 
`PartitioningPreservingUnaryExecNode`). When true and the
- *                   keys are no longer unique, `GroupPartitionsExec` may 
merge partitions that held
- *                   distinct keys in the original partitioning, carrying the 
same skew risk as
- *                   `allowKeysSubsetOfPartitionKeys`. "May", because the 
condition is a proxy: the
- *                   duplicate keys can also come from a source that reports 
several splits per
- *                   partition key, in which case grouping merges only 
same-key partitions. Such a
- *                   partitioning can only satisfy `ClusteredDistribution` by 
being grouped, and
- *                   `groupedSatisfies` refuses that unless the config is 
enabled, regardless of
- *                   `requireAllClusterKeysForDistribution`.
+ * @param isCollapsed Whether a projection or a reduction mapped keys that 
were distinct in the
+ *                    partitioning this one was derived from onto the same 
key, so one partition
+ *                    here can stand for several of the original ones -- see 
"Key Collapse" above.
+ *                    Dropping key positions does not set it on its own; the 
projected keys have to
+ *                    actually lose distinctness. Sticky, because neither 
grouping nor a further
+ *                    projection can make a partitioning finer again. One case 
sets it without a
+ *                    collapse of its own: the side shuffled onto a collapsed 
partitioning's keys
+ *                    inherits it, because the two are then co-located on that 
key set -- see
+ *                    `KeyedShuffleSpec.createPartitioning`.
+ *                    Together with `!isGrouped` it decides whether 
`groupedSatisfies` may coalesce
+ *                    the duplicate keys without 
`allowKeysSubsetOfPartitionKeys`: `isCollapsed`
+ *                    says the collapse happened, `!isGrouped` says there is 
still something left
+ *                    to merge, and only both together mean there is an 
outstanding risk to gate.
  */
 case class KeyedPartitioning(
     expressions: Seq[Expression],
     @transient partitionKeys: Seq[InternalRowComparableWrapper],
     isGrouped: Boolean,
-    isNarrowed: Boolean = false) extends Expression with Partitioning with 
Unevaluable {
+    isCollapsed: Boolean = false) extends Expression with Partitioning with 
Unevaluable {

Review Comment:
   Done, the default is gone. In main code only the companion `apply` relied on 
it, and it now passes `isCollapsed = false` explicitly with a comment saying 
why a fresh source partitioning is the layout everything else is compared 
against.
   
   It also caught a live instance of exactly the laundering you describe, in 
test code: `DistributionAndOrderingSuiteBase.resolvePartitioning` destructured 
the flag away with `_` and rebuilt the partitioning without it. That is the 
concrete payoff for the compile-time change, so it is in the commit message.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -643,12 +676,17 @@ case class KeyedPartitioning(
       val joinKeyPositions = 
result.keyPositions.map(_.nonEmpty).zipWithIndex.filter(_._1).map(_._2)
       val projectedExpressions = joinKeyPositions.map(expressions)
       val projectedKeys = projectKeys(joinKeyPositions)._2
+      // Projecting onto the operation keys can collapse keys in its own 
right, which is
+      // what the key count comparison catches. The gate in `groupedSatisfies` 
is bypassed while
+      // this config is on, so the flag decides nothing here today, but it 
travels with the
+      // partitioning and leaving a producer to launder it is how the 
protection went missing.
+      val projectedCollapsed = isCollapsed || projectedKeys.distinct.length < 
distinctKeyCount

Review Comment:
   Done. The count now comes off the grouped partitioning that `toGrouped` 
already builds, so there is one `distinct` where there were two, and the 
comparison is skipped entirely when `joinKeyPositions` selects every position 
-- which, as you note, cannot collapse anything.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/AliasAwareOutputExpression.scala:
##########
@@ -137,12 +137,24 @@ trait PartitioningPreservingUnaryExecNode extends 
UnaryExecNode
       if (projectablePositions.length == numPositions) keySource.partitionKeys
       else keySource.projectKeys(projectablePositions)._2
 
-    val isGrouped = sharedKeys.distinct.size == sharedKeys.size
-    // A KP is narrowed if this node drops positions, or if the input KPs were 
already narrowed
-    // (i.e. came from a finer-grained partitioning). The flag must be sticky: 
a subsequent
-    // PartitioningPreservingUnaryExecNode that passes all positions through 
would otherwise
-    // recompute isNarrowed=false, silently dropping the protection.
-    val isNarrowed = projectablePositions.length < numPositions || 
keySource.isNarrowed
+    val distinctSharedKeys = sharedKeys.distinct
+    val isGrouped = distinctSharedKeys.size == sharedKeys.size
+    // This projection collapses keys when it maps keys that were distinct in 
the input
+    // onto the same projected key -- dropping positions is not enough on its 
own, since the
+    // projected keys can stay just as distinct as the originals. The flag is 
sticky: a subsequent
+    // PartitioningPreservingUnaryExecNode that passes all positions through 
must not recompute it
+    // as false and drop the protection, and no projection can make a 
partitioning finer again.
+    //
+    // Both cheap terms come first: an inherited flag or a projection that 
drops no position
+    // settles the question without counting distinct keys. A pass-through 
projection cannot
+    // collapse anything, since it keeps the input's keys as they are.
+    //
+    // The inherited flag is read from all inputs rather than from the key 
source alone. A
+    // `PartitioningCollection` normalizes it across its members, so the two 
agree today; reading
+    // all of them keeps this producer correct without depending on that.
+    val isCollapsed = kps.exists(_.isCollapsed) ||

Review Comment:
   Added, as `KeyedPartitioning.collapsesOnProjection`. One correction to the 
shape you sketched: the `isCollapsed ||` disjunct would be dead at both call 
sites, because each already carries the inherited flag outside it -- and 
`AliasAwareOutputExpression` has to, since it reads the flag from every input 
rather than only from the one whose keys it counts. So the helper is the count 
comparison alone, and its scaladoc says that the callers own the inherited term.
   
   `GroupPartitionsExec` deliberately does not use it: it can answer the 
question exactly from the key groups it keeps, and the scaladoc points at that 
as the reference definition.
   



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