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


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/EnsureRequirements.scala:
##########
@@ -867,42 +879,259 @@ 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.
+   *
+   * An empty result means no partition expression covers an operation key, so 
there is nothing to
+   * project onto. That is the answer for a member that cannot satisfy 
`distribution`, which is why
+   * the caller only asks for members that can. It also happens for a member 
that can: one whose
+   * expressions have no references at all makes every `groupedSatisfies` 
branch vacuously true, and
+   * nothing at `KeyedPartitioning` construction rejects that. The caller 
skips such a member rather
+   * than project it to no position, which would collapse every partition into 
one.
+   *
+   * 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)
+      positions

Review Comment:
   Redundant local val



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -544,6 +550,21 @@ case class KeyedPartitioning(
 
   @transient lazy val expressionDataTypes: Seq[DataType] = 
expressions.map(_.dataType)
 
+  /**
+   * The data types of the `partitionKeys` rows: the types each key was built 
with, and the ones it
+   * is hashed and compared under (`InternalRowComparableWrapper.dataTypes`).
+   *
+   * These are the `expressionDataTypes` unless a reducer has rewritten the 
keys. With
+   * `v2BucketingAllowCompatibleTransforms` a storage-partitioned join reduces 
one or both sides'
+   * keys onto a common key space, while the partitioning keeps reporting the 
expressions it was
+   * built from: joining an `identity(ts)`-partitioned table to a 
`years(ts)`-partitioned one leaves
+   * `IntegerType` year values under a `TimestampType`-declared expression. 
Reading a key row is
+   * only sound at the types the row was written with, which is what these are 
for; use
+   * `expressionDataTypes` only where the question is about the expressions 
themselves.
+   */
+  @transient lazy val keyDataTypes: Seq[DataType] =
+    partitionKeys.headOption.map(_.dataTypes).getOrElse(expressionDataTypes)

Review Comment:
   the key-type axis is fixed in `projectKeys` but not in the ordering 
consumers — `toGrouped`, `keyRowOrdering`, and the OrderedDistribution arm 
still read at `expressionDataTypes`
   
     - **Where:** `partitioning.scala:547-556` 
(`keyRowOrdering`/`keyOrdering`/`toGrouped`), reached
       via `KeyedPartitioning.createShuffleSpec` subset branch at 
`partitioning.scala:620-629`; also
       `EnsureRequirements.scala:93-94`.
     - **Defect:** `toGrouped` sorts with 
`groupedKeyRowOrdering(expressionDataTypes)` — the declared
       expression types — while this PR establishes that keys must be read at 
`keyDataTypes`. For a
       reducer-rewritten partitioning (Integer year values under a 
`TimestampType`-declared `ts`
       expression), sorting compares `GenericInternalRow.getLong` against a 
boxed `Integer`
       (`rows.scala`: `getLong(ordinal) = getAs(ordinal)`) ->
       `ClassCastException: java.lang.Integer cannot be cast to java.lang.Long` 
— the exact error this
       PR fixes elsewhere.
     - **Concrete failure:** `v2BucketingAllowCompatibleTransforms` +
       `v2BucketingAllowKeysSubsetOfPartitionKeys` on; 
`identity(ts)`/`years(ts)` SPJ join (the PR's
       own test shape) -> any `PartitioningPreservingUnaryExecNode` above it 
(final `HashAggregate`)
       preserving the stale-typed KP -> a join above *that* calls 
`createShuffleSpec` -> subset branch
       runs `projectKeys` (now fixed) then `.toGrouped` -> CCE at planning with 
>=2 distinct projected
       keys. Same mismatch in the `OrderedDistribution` arm 
(`EnsureRequirements.scala:93`):
       `RowOrdering.create(o.ordering, attrs)` compares reduced keys with an 
ordering bound to the
       stale expression types, under `v2BucketingAllowSorting`.
     - **Peer code / invariant:** the PR's own `keyDataTypes` scaladoc 
(`partitioning.scala:550-564`)
       states "the types each key was built with, and the ones it is hashed and 
compared under" — the
       hash/compare path (`InternalRowComparableWrapper.equals/hashCode`) uses 
the wrapper's own types
       and is safe; `toGrouped`'s sort does not go through the wrapper's 
ordering, so it violates the
       stated invariant.
     - **Severity honesty:** *not a regression* — on master these plans CCE'd 
even earlier (in
       `projectKeys`). The PR widens the set of plannable queries past the 
first crash and lands on
       the next instance of the same bug. Recommend either deriving 
`keyRowOrdering` from
       `keyDataTypes` (one line, consistent with the new scaladoc), or 
explicitly listing the
       remaining consumers in the follow-up JIRA alongside the 
`ValidateRequirements` gap.
     - **Verdict: CONFIRMED** (line-by-line trace; cast mechanics verified in 
source). Runnable repro,
       works on master *without* the PR, and would still fail after it — 
appendable to
       `EnsureRequirementsSuite`:
   
     ```scala
     test("SPARK-58968: createShuffleSpec must sort keys at the types they were 
built with") {
       val exprTs = AttributeReference("ts", TimestampType)()
       val exprId = AttributeReference("id", IntegerType)()
       val factory = InternalRowComparableWrapper
         .getInternalRowComparableWrapperFactory(Seq(IntegerType, IntegerType))
       // Reduced keys (year, bucket) under stale expressions (ts: 
TimestampType), as
       // GroupPartitionsExec.outputPartitioning reports after applying 
reducers.
       val keys = Seq(InternalRow(2020, 0), InternalRow(2021, 1)).map(factory)
       val kp = new KeyedPartitioning(Seq(exprTs, exprId), keys, isGrouped = 
true)
       withSQLConf(SQLConf.V2_BUCKETING_ALLOW_KEYS_SUBSET_OF_PARTITION_KEYS.key 
-> "true") {
         val e = 
intercept[ClassCastException](kp.createShuffleSpec(ClusteredDistribution(Seq(exprTs))))
         assert(e.getMessage.contains("java.lang.Integer cannot be cast to 
java.lang.Long"))
       }
     }
     ```
   
     Run:
     `build/sbt 'sql/testOnly 
org.apache.spark.sql.execution.exchange.EnsureRequirementsSuite -- -z 
"createShuffleSpec must sort keys"'`



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