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


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/EnsureRequirements.scala:
##########
@@ -867,42 +867,234 @@ case class EnsureRequirements(
   }
 
   /**
-   * Splits a partitioning into three categories:
-   * 1. Non-KeyedPartitioning (HashPartitioning, RangePartitioning, etc.)
-   * 2. Grouped KeyedPartitioning (isGrouped = true)
-   * 3. Non-grouped KeyedPartitioning (isGrouped = false)
+   * The positions of `kp`'s partition expressions that are operation keys of 
`distribution`, and so
+   * have to survive a projection. All of them when nothing needs projecting.
+   *
+   * Under `v2BucketingAllowKeysSubsetOfPartitionKeys` a [[KeyedPartitioning]] 
may be grouped on
+   * more keys than the operation requires, in which case partitions sharing 
an operation key are
+   * still separate. A partition expression is an operation key in two ways: 
one of its *references*
+   * is a cluster key - the form `groupedSatisfies` and 
`KeyedShuffleSpec.keyPositions` both use,
+   * where a `bucket(4, a)` transform covers the cluster key `a` - or the 
expression *itself* is a
+   * cluster key. The second is never decisive in practice, because 
`IdentityTransform` resolves to
+   * the attribute itself and then the reference-level test matches the same 
position anyway; it is
+   * kept so that a partition expression which is a cluster key can never be 
projected away.
+   *
+   * Returns every position for a co-partitioned operator: there the 
multi-child block owns the
+   * projection, and doing it here as well would leave that block deriving 
positions from an already
+   * projected partitioning and applying them to the unprojected partition 
expressions.
+   *
+   * Only call this for a `kp` that can satisfy `distribution`. For a `kp` 
that cannot, no position
+   * would be covered, and an empty result means something else here.
+   *
+   * Keeping a position is only sound because `groupedSatisfies`' subset 
branch also requires
+   * `expressions.forall(_.references.size == 1)`: a kept expression is then a 
function of a single
+   * cluster key, so coalescing on the projected keys cannot put rows that 
share an operation key on
+   * different partitions.
+   */
+  private def clusterKeyPositions(
+      kp: KeyedPartitioning,
+      distribution: Distribution,
+      isCoPartitioned: Boolean): BitSet = distribution match {
+    case c: ClusteredDistribution if !isCoPartitioned =>
+      val positions = kp.expressions.indices.filter { i =>
+        val e = kp.expressions(i)
+        c.clustering.exists(_.semanticEquals(e)) ||
+          e.references.exists(ref => 
c.clustering.exists(_.semanticEquals(ref)))
+      }.to(BitSet)
+      // A member that can satisfy covers at least one position, unless it 
declares no partition
+      // expression at all. `supportsExpressions` rules out a reference-free 
expression, so a
+      // non-empty expression list has non-empty `references`, and every 
`groupedSatisfies` branch
+      // then needs a cluster key among them. A connector can declare 
`KeyGroupedPartitioning` with
+      // no keys though, and nothing rejects it: there the empty result is 
harmless, because it
+      // makes the projection a no-op and the node only coalesces, exactly as 
before this change.
+      assert(positions.nonEmpty || kp.expressions.isEmpty,
+        s"no partition expression of ${kp.expressions} covers an operation 
key")
+      positions
+    case _ => kp.expressions.indices.to(BitSet)
+  }
+
+  /**
+   * Splits a partitioning into three categories with respect to 
`distribution`:
+   * 1. Every non-KeyedPartitioning (HashPartitioning, RangePartitioning, etc.)
+   * 2. The KeyedPartitioning that needs no [[GroupPartitionsExec]] at all: it 
satisfies the
+   *    distribution and such a node would leave nothing changed
+   * 3. Or the one that satisfies it only after a [[GroupPartitionsExec]], 
paired with the partition
+   *    expression positions that node has to project to (`None` when it only 
has to coalesce
+   *    duplicate partition keys)
+   *
+   * Categories 2 and 3 hold at most one partitioning each and never both, 
because the caller acts
+   * on a single one of them: whichever it takes, the child then satisfies the 
distribution and the
+   * rest of the child's partitioning is irrelevant. A partitioning that 
satisfies the distribution
+   * can still land in 3, because `satisfies` over-claims under
+   * `v2BucketingAllowKeysSubsetOfPartitionKeys`.
+   *
+   * Categories 2 and 3 classify by what still has to happen to the data, not 
by how the
+   * partitioning was built. An already grouped `KeyedPartitioning` can still 
need a
+   * `GroupPartitionsExec`, because 
`v2BucketingAllowKeysSubsetOfPartitionKeys` lets it be grouped
+   * on more keys than the operation requires -- `isGrouped` only tells 
whether the *full*
+   * partition keys are unique. Keeping both reasons in one category leaves 
the caller a single
+   * `ClusteredDistribution` arm that inserts the node, and one place that 
decides the projection.
+   *
+   * KeyedPartitionings that cannot satisfy the distribution at all are 
dropped.
    *
    * @param partitioning The partitioning to split
-   * @return A tuple of (other, grouped, nonGrouped) where:
-   *         - other: Option containing non-KeyedPartitioning(s)
-   *         - grouped: Seq of grouped KeyedPartitionings
-   *         - nonGrouped: Seq of non-grouped KeyedPartitionings
+   * @param distribution The distribution to satisfy
+   * @param isCoPartitioned Whether the parent operator co-partitions more 
than one child, in which
+   *                        case the projection is not done here (see 
`clusterKeyPositions`)
+   * @return A tuple of (other, satisfying, needsGrouping)
    */
-  private def splitKeyedPartitionings(partitioning: Partitioning) = {
+  private def splitKeyedPartitionings(
+      partitioning: Partitioning,
+      distribution: Distribution,
+      isCoPartitioned: Boolean) = {
     val otherPartitionings = ArrayBuffer.empty[Partitioning]
-    val groupedKeyedPartitionings = ArrayBuffer.empty[KeyedPartitioning]
-    val nonGroupedKeyedPartitionings = ArrayBuffer.empty[KeyedPartitioning]
+    // A member that needs no node at all settles the whole child, so it is 
kept apart from the
+    // candidates that would need one.
+    var satisfiedAsIs: Option[KeyedPartitioning] = None
+    // The candidates that would need a node, keyed by the positions the node 
would project them to.
+    // One entry per distinct position set is enough, and the first member 
wins: the same set
+    // projects to the same keys whichever member applies it, because 
`PartitioningCollection`
+    // guarantees its members share the `partitionKeys` reference and their 
arity, so position `i`
+    // addresses the same key column in all of them.
+    //
+    // Insertion-ordered so that when two sets leave the same number of 
partitions, the one from the
+    // member the child reports first wins. That tie is the only thing the 
order decides, and either

Review Comment:
   Agreed, and thanks for tracing it across the files. I have added a sentence 
at the `candidates` declaration naming `checkKeyedPartitioningInvariant` and 
the interning in `fromPartitionings` as the actual guarantee, and saying that 
relaxing it means changing `GroupPartitionsExec`'s `collectFirst` at the same 
time rather than this side alone.
   
   Worth adding that the assumption is already only partly guaranteed: the 
`partitionKeys` reference and the arity are enforced, the per-position 
`expressionDataTypes` are not. Two members of one collection really can declare 
different types over the same keys - `pushPartValues` plus 
`allowCompatibleTransforms`, an `identity(ts)`-partitioned table joined to a 
`years(ts)`-partitioned one - which is why the projected-count memo in this 
method is keyed on `(BitSet, Seq[DataType])` rather than on the position set 
alone.
   
   I would rather not add a `require` on the reference identity here: 
`PartitioningCollection` already asserts it on construction, so a second check 
would go stale the day that one moves.
   



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