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


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -1100,6 +1153,15 @@ object PartitioningCollection {
     case _ => None
   }
 
+  /**
+   * Whether `p` or any nested [[KeyedPartitioning]] may contain unknown 
partition keys, read
+   * from its first keyed member, the same one 
`checkKeyedPartitioningInvariant` compares
+   * against. Keyless inputs answer false: `EnsureRequirements` normalizes a 
shuffled join's
+   * children onto one template, so they never meet a keyed sibling here.
+   */
+  private[sql] def mayContainUnknownPartitionKeys(p: Partitioning): Boolean =

Review Comment:
   **Finding 19.** This answers `false` for two different inputs: a 
partitioning with no `KeyedPartitioning` in it, and one whose keyed members are 
unmarked. `clearUnknownPartitionKeys` folds over exactly this value 
(`ShuffledJoin.scala:101`), so a keyless sibling reads as "unmarked", 
`markers.forall(_ == markers.head)` is false, and the keyed input's marker is 
cleared — a genuine claim dropped because the other side has no keys at all.
   
   Round 4's `members.forall(_.mayContainUnknownPartitionKeys)` over the 
flattened keyed members could not fail that way, since a keyless input 
contributed no member.
   
   I could not construct the shape at this head, so this is latent, and your 
doc sentence is right about why:
   
   - `KeyedShuffleSpec.isCompatibleWith` ends in `case _ => false` 
(`partitioning.scala:1629`), so a keyed spec never keeps a keyless partner in 
place.
   - `SinglePartitionShuffleSpec.canCreatePartitioning` is `false` 
(`partitioning.scala:1326`), and `RangeShuffleSpec`'s is too, so a keyless spec 
cannot become `bestSpecOpt` and leave the keyed child untouched. With the keyed 
spec best the keyless child is re-shuffled by `createPartitioning`; with 
neither in `candidateSpecs` both children get `dist.createPartitioning(...)`.
   - `checkKeyGroupCompatible` returns `None` unless both children yield a 
`KeyedShuffleSpec`.
   
   What makes it worth closing is that the argument is a property of one of 
this helper's two call sites. `GroupPartitionsExec.scala:91` passes a single 
child's partitioning, where "meets a keyed sibling" is not a question at all, 
and the keyless `false` is right there for an unrelated reason: `p.transform` 
has no `KeyedPartitioning` to rewrite either way. So the sentence documents the 
caller that needs it, on the helper that does not.
   
   Letting the helper say "no keyed member" separates the two. It keeps the 
short-circuit and the zero-allocation path, and needs no presence filter:
   
   ```scala
     private[sql] def keyedMarkerOf(p: Partitioning): Option[Boolean] =
       representativeOf(p).map(_.mayContainUnknownPartitionKeys)
   ```
   
   Then in `ShuffledJoin`:
   
   ```scala
       val markers = partitionings.flatMap(PartitioningCollection.keyedMarkerOf)
       if (markers.isEmpty || markers.forall(_ == markers.head)) {
   ```
   
   with the per-input pattern guard becoming 
`keyedMarkerOf(partitioning).contains(true)`, and `GroupPartitionsExec` reading 
`PartitioningCollection.keyedMarkerOf(p).contains(true)` — the same value it 
reads today.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/AliasAwareOutputExpression.scala:
##########
@@ -131,9 +131,20 @@ trait PartitioningPreservingUnaryExecNode extends 
UnaryExecNode
 
     if (projectablePositions.isEmpty) return LazyList.empty
 
-    // All input KPs share the same partitionKeys and isCollapsed flag by 
invariant, so the first
-    // one projects the keys and both flags for every combination below. Only 
the expressions
-    // differ.
+    // Collection members carry a uniform marker (mixed collections are 
cleared at

Review Comment:
   **Finding 20.** "mixed collections are cleared at construction by 
`ShuffledJoin`" stopped being the reason the head represents them all. Since 
6021d83 the reason is `PartitioningCollection`'s `require` 
(`partitioning.scala:1105`) plus the OR in `fromPartitionings` 
(`partitioning.scala:1178`): a mixed collection is now unrepresentable, whether 
or not anything cleared. And without the clearing the OR would not leave a 
mixed collection either — it would mark every member.
   
   That matters for whoever reads this line next. It points at the wrong 
invariant, so deleting the clearing looks like it would break `kps.head`. It 
would not; it would spread a spurious marker instead.
   
   ```scala
       // `PartitioningCollection` requires its members to agree on the marker, 
so the head
       // represents them all.
   ```
   
   Two more comments carry the same attribution:
   
   - `ShuffledJoin.scala:92` — "when marked and unmarked members meet". They 
cannot meet inside one collection any more. What meet are the two *inputs*.
   - `partitioning.scala:604` — rule (2) reads "the only site that mixes them, 
clears it at construction; `PartitioningCollection` *additionally* normalizes 
the marker by OR". With the OR in place the normalization is the guarantee and 
the clearing is the precision, not the other way round.
   



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/DistributionSuite.scala:
##########
@@ -447,6 +448,59 @@ class DistributionSuite extends SparkFunSuite {
     assert(interned.partitionKeys eq kpX.partitionKeys)
   }
 
+  test("SPARK-59050: a marked one-partition layout keeps the global ordering 
claim") {
+    // The one-partition exemption inside `keysSatisfy`'s ordered branch: a 
single partition
+    // holds every row, so an out-of-set key cannot break the cross-partition 
sequence, while
+    // two partitions can (the e2e `ORDER BY` repro measures that). Positive 
control: an
+    // always-false gate would shuffle these plans for nothing.
+    val a = AttributeReference("a", IntegerType)()
+    val ordered = OrderedDistribution(
+      Seq(org.apache.spark.sql.catalyst.expressions.SortOrder(a,

Review Comment:
   **Finding 21.** Four names are fully qualified inline where this file 
imports: `SortOrder` and `Ascending` here, `InternalRow` on 460/463/464, and 
`SQLConf` on 467. `InternalRow` needs nothing at all — the suite is in 
`org.apache.spark.sql.catalyst`, and the sibling test added in the same commit 
writes `InternalRow(1)`. The other three want two import lines: `Ascending, 
SortOrder` added to the existing `catalyst.expressions` import, and `import 
org.apache.spark.sql.internal.SQLConf`.
   
   The suite also has `checkSatisfied` for exactly this assertion, and it 
prints the partitioning and the distribution on failure:
   
   ```scala
       val a = AttributeReference("a", IntegerType)()
       val ordered = OrderedDistribution(Seq(SortOrder(a, Ascending)))
       val markedOne = KeyedPartitioning(Seq(a), Seq(InternalRow(1)))
         .copy(mayContainUnknownPartitionKeys = true)
       val markedTwo = KeyedPartitioning(Seq(a), Seq(InternalRow(1), 
InternalRow(2)))
         .copy(mayContainUnknownPartitionKeys = true)
       withSQLConf(SQLConf.V2_BUCKETING_SORTING_ENABLED.key -> "true") {
         checkSatisfied(markedOne, ordered, true)
         checkSatisfied(markedTwo, ordered, false)
       }
   ```
   
   That drops your two assertion messages, so keep the plain `assert`s if you 
prefer them. The imports are the substance.
   



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