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


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -1678,7 +1751,18 @@ case class KeyedShuffleSpec(
     // The shuffled side is laid out on this side's partition keys, so it 
inherits the flag. That
     // is conservative rather than strictly true, and it can only ever add a 
shuffle: a later
     // grouping of the shared key set carries the collapsed side's risk.
-    partitioning.copy(expressions = newExpressions)
+    //
+    // The child re-shuffled onto this layout may hold keys outside the 
declared set, so every
+    // partitioning produced here carries the unknown-keys marker too (see
+    // `KeyedPartitioning.mayContainUnknownPartitionKeys`). 
`createPartitioning` is only reached
+    // from `EnsureRequirements`' shuffle loop to re-shuffle a join child onto 
the best spec's
+    // layout, and the re-shuffled child's keys are never provably a subset of 
the declared keys:
+    // a non-keyed (v1) child's keys are unknown to the planner, and a keyed 
child with an
+    // incompatible partitioning is re-evaluated in this spec's key space, 
which the planner
+    // cannot bound. Marking every such partitioning is therefore
+    // sound (conservative only when the two sides happen to share a transform 
and the re-shuffled
+    // keys are a known subset).
+    partitioning.copy(expressions = newExpressions, 
mayContainUnknownPartitionKeys = true)

Review Comment:
   Thanks @cloud-fan -- fixed in 7464c25 exactly as scoped: the 
`OrderedDistribution` branch keeps the ordering claim of a marked layout only 
with `numPartitions == 1` (one partition holds everything, so local order is 
global order). The repro test `SPARK-59050: SPJ: a global ORDER BY over an 
unknown-keyed layout must range-partition` measures your [1, 4, 2] on the 
pre-gate tree, compares `collect()` directly rather than through `checkAnswer` 
(which sorts both sides), and pins the range exchange.
   



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExecSuite.scala:
##########
@@ -192,6 +192,35 @@ class GroupPartitionsExecSuite extends SharedSparkSession {
     assert(gpe.outputOrdering === Nil)
   }
 
+  test("SPARK-59050: unknown-keyed child with reducers gives up the keyed 
claim at the real " +
+    "count") {
+    // No planner path reaches the give-up branch with the built-in transforms 
--
+    // `createShuffleSpec` refuses a narrowing projection of an unknown-keyed 
partitioning one
+    // hop earlier, and `areKeysCompatible` pairs it only with same-function 
partners that have
+    // no reducer (see `GroupPartitionsExec.outputPartitioning`) -- so pin the 
contract
+    // directly: the node must report `UnknownPartitioning` with its physical 
grouped count.
+    // Reporting zero partitions is what threw once a parent join built a
+    // `PartitioningCollection` over both sides.
+    val partitionKeys = Seq(row(1), row(2), row(1))
+    val child = DummySparkPlan(
+      outputPartitioning = KeyedPartitioning(Seq(exprA), partitionKeys)
+        .copy(mayContainUnknownPartitionKeys = true))
+    val gpe = GroupPartitionsExec(child, reducers = Some(Seq(None)))

Review Comment:
   Good catch, thanks -- done in 7464c25. The pin now builds a real mod-2 
`KeyReducer` ([1, 2, 3] reduced to [1, 0, 1]), asserts 2 grouped partitions 
where the child had 3, and ends with `PartitioningCollection.fromPartitionings` 
over a 2-partition partner: reverting the give-up count to 
`UnknownPartitioning(0)` fails both the count and the collection.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -1545,6 +1574,45 @@ case class KeyedShuffleSpec(
       }
     } && expressions.zip(otherExpressions).forall {
       case (l, r) => isExpressionCompatible(l, r)
+    } && {
+      // An unknown-keyed side (see 
`KeyedPartitioning.mayContainUnknownPartitionKeys`) guarantees
+      // co-location only for its declared keys, and `KeyGroupedPartitioner`'s 
out-of-set routing
+      // is a deterministic hash -- so it can pair only with a side whose keys 
are a subset of the
+      // declared keys.
+      //
+      // The key comparison below must also happen in a single domain: 
`isExpressionCompatible`
+      // admits an `AttributeReference` against a `TransformExpression` (and 
two different-but-
+      // compatible transforms) when `v2BucketingAllowCompatibleTransforms` is 
on, and in those
+      // cases the two sides' `partitionKeys` hold raw values on one side and 
transform outputs on
+      // the other, so the subset test would compare unrelated values. Require 
the partition
+      // expressions to be the same function per position before comparing 
keys.
+      //
+      // Two unknown-keyed sides are compatible only when they agree on the 
declared keys *and*
+      // their order: the out-of-set keys hash to the same-index partition on 
both sides, and a
+      // `GroupPartitionsExec` regrouping re-labels each partition by that 
side's declared key, so
+      // a differing declared order would push the out-of-set keys into 
different output
+      // partitions and lose their matches.
+      if (partitioning.mayContainUnknownPartitionKeys ||
+          other.partitioning.mayContainUnknownPartitionKeys) {
+        expressions.zip(otherExpressions).forall {
+          case (_: AttributeReference, _: AttributeReference) => true
+          case (l: TransformExpression, r: TransformExpression) => 
l.isSameFunction(r)

Review Comment:
   Added in 7464c25, thanks: a local reducible bucket-like function (catalyst 
has no `BucketFunction`) pairs bucket(4) against bucket(8) under 
`allowCompatibleTransforms`, marked in both argument orders (refused) and 
unmarked (still admissible, guarding the pre-existing relaxation), with both 
specs declaring the same key set {0, 1} so only the same-function gate can 
cause the refusal.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -573,12 +577,29 @@ case class CoalescedNullAwareHashPartitioning(
  *                    partitioning this one was derived from onto the same 
key, so one key here can
  *                    stand for several of the original ones. Sticky. See "Key 
Collapse" above for
  *                    what it gates and how it travels.
+ * @param mayContainUnknownPartitionKeys Whether the data may contain rows 
whose partition key is
+ *                                 not among the declared `partitionKeys`. 
`KeyGroupedPartitioner`
+ *                                 routes such rows to arbitrary partitions 
when a side is
+ *                                 re-shuffled onto this partitioning (see
+ *                                 `KeyedShuffleSpec.createPartitioning`), so 
only the declared

Review Comment:
   Documented in 7464c25, thanks -- the `@param` now states the exception the 
`areKeysCompatible` branch relies on: two marked partitionings declaring the 
same keys in the same order still pair, since equal undeclared keys hash to the 
same partition.
   



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