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


##########
sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala:
##########
@@ -4207,6 +4207,40 @@ class KeyGroupedPartitioningSuite extends 
DistributionAndOrderingSuiteBase with
     }
   }
 
+  test("SPARK-56877: v2 bucketed table with subset join keys joining v1 
table") {
+    // The v2 table is partitioned by an extra identity key `dt` plus 
`bucket(16, c1)`, while the
+    // join is only on `c1`. allowKeysSubsetOfPartitionKeys lets the operation 
key `c1` be a subset
+    // of the partition keys `[dt, bucket(16, c1)]`, so EnsureRequirements 
projects the keyed side
+    // to `[bucket(16, c1)]`. v2BucketingShuffleEnabled then re-shuffles only 
the v1 side using that
+    // projected KeyedPartitioning. ShuffledJoin wraps the two output 
partitionings into a
+    // PartitioningCollection, which requires all KeyedPartitionings to share 
equal partitionKeys.
+    // The v2 side's keys are sorted by GroupPartitionsExec, while the keys 
re-used for the v1 side
+    // keep their first-occurrence order from createShuffleSpec, so the two 
sequences disagree and
+    // the collection construction used to fail.
+    val cols = Array(
+      Column.create("c1", LongType),
+      Column.create("c2", StringType),
+      Column.create("dt", StringType))
+    val partitions = Array(identity("dt"), bucket(16, "c1"))
+
+    createTable("iceberg_t2", cols, partitions)
+    sql("INSERT INTO testcat.ns.iceberg_t2 VALUES (2, 'cc', '2020'), (1, 'aa', 
'2021')")
+
+    withTable("t1") {
+      sql("CREATE TABLE t1 (c1 BIGINT, c2 STRING) USING parquet")
+      sql("INSERT INTO t1 VALUES (1, 'aa'), (2, 'cc')")
+
+      withSQLConf(
+          SQLConf.V2_BUCKETING_ALLOW_KEYS_SUBSET_OF_PARTITION_KEYS.key -> 
"true",
+          SQLConf.V2_BUCKETING_SHUFFLE_ENABLED.key -> "true") {
+        val df = sql("SELECT * FROM testcat.ns.iceberg_t2 t0 JOIN t1 ON t0.c1 
= t1.c1")
+        checkAnswer(df, Seq(

Review Comment:
   **Finding 2.** This is a regression test for a *planning* failure, so it 
should pin the plan shape. `checkAnswer` alone stays green if planning later 
stops taking the storage-partitioned-join path, and at that point the test 
silently no longer reaches `createShuffleSpec` at all. That is not hypothetical 
here - the path needs `dt` to survive column pruning into the scan, which is 
the exact fragility the two `SPARK-46367` tests above call out in their own 
comments.
   
   I measured the plan with AQE off: one shuffle (the v1 side) and one 
`GroupPartitionsExec` (the v2 side). The suite's helpers already express that:
   
   ```scala
         withSQLConf(
             SQLConf.V2_BUCKETING_ALLOW_KEYS_SUBSET_OF_PARTITION_KEYS.key -> 
"true",
             SQLConf.V2_BUCKETING_SHUFFLE_ENABLED.key -> "true",
             SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false") {
           val df = sql("SELECT * FROM testcat.ns.iceberg_t2 t0 JOIN t1 ON 
t0.c1 = t1.c1")
           val plan = df.queryExecution.executedPlan
           // Only the v1 side is re-shuffled; the v2 side is regrouped onto 
the join key instead.
           assert(collectShuffles(plan).length == 1)
           assert(collectGroupPartitions(plan).length == 1)
           checkAnswer(df, Seq(
             Row(1L, "aa", "2021", 1L, "aa"),
             Row(2L, "cc", "2020", 2L, "cc")))
         }
   ```
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -613,9 +613,12 @@ case class KeyedPartitioning(
       val joinKeyPositions = 
result.keyPositions.map(_.nonEmpty).zipWithIndex.filter(_._1).map(_._2)
       val projectedExpressions = joinKeyPositions.map(expressions)
       val projectedKeys = projectKeys(joinKeyPositions)._2
-      val distinctProjectedKeys = projectedKeys.distinct
+      // Sort the distinct projected keys the same way `GroupPartitionsExec` 
does. Otherwise, when
+      // only the keyed side is grouped and the other side is re-shuffled 
using this spec, the two
+      // `KeyedPartitioning`s carry the same keys in a different order and
+      // `PartitioningCollection.fromPartitionings` rejects them.
       val projectedPartitioning =
-        KeyedPartitioning(projectedExpressions, distinctProjectedKeys, 
isGrouped = true)
+        new KeyedPartitioning(projectedExpressions, projectedKeys, isGrouped = 
false).toGrouped

Review Comment:
   **Finding 1.** The projected branch is consistent now, but the `else` branch 
below still hands the child's `KeyedPartitioning` back verbatim, and 
`KeyedShuffleSpec.createPartitioning` (`partitioning.scala:1393`) then reuses 
its keys as-is:
   
   ```scala
   KeyedPartitioning(newExpressions, partitioning.partitionKeys, 
partitioning.isGrouped)
   ```
   
   The physical partitioner does not: `ShuffleExchangeExec.getPartitioner` 
builds `KeyGroupedPartitioner` from `k.toGrouped`, i.e. always from the 
*sorted* keys (`ShuffleExchangeExec.scala:402`). So whenever the keyed side's 
own keys are not sorted, the two sides disagree exactly as they did here - 
except nothing throws, because `createPartitioning` passes the same 
`partitionKeys` *reference* through, so 
`PartitioningCollection.fromPartitionings` interns it on `eq` and the `require` 
never runs.
   
   Unsorted keys are reachable without `allowKeysSubsetOfPartitionKeys`: a 
narrowing `PartitioningPreservingUnaryExecNode` projects sorted `[dt, id]` keys 
down to `id` in first-occurrence order 
(`AliasAwareOutputExpression.scala:138`), and `UnionExec` concatenates its 
children's keys (`basicPhysicalOperators.scala:989`). The class doc at 
`partitioning.scala:449-452` states this outright.
   
   Measured on this branch. Same table, same query, same configs - only the 
inserted row values differ, which flips the narrowed key sequence:
   
   | narrowed keys | `v2BucketingShuffleEnabled` | shuffles | rows |
   |---|---|---|---|
   | `[2, 1]` | `false` | 2 | `[1,2021,x], [2,2020,y]` |
   | `[2, 1]` | `true` | 1 | *(empty)* |
   | `[1, 2]` | `false` | 2 | `[1,2020,x], [2,2021,y]` |
   | `[1, 2]` | `true` | 1 | `[1,2020,x], [2,2021,y]` |
   
   ```sql
   -- testcat.ns.nt partitioned by [identity(dt), identity(id)], one row per 
split,
   -- rows (1,'2021'),(2,'2020') for the unsorted case and 
(1,'2020'),(2,'2021') for the sorted one
   SELECT a.id, a.m, t1.data
   FROM (SELECT id, MAX(dt) AS m FROM testcat.ns.nt GROUP BY id) a
   JOIN t1 ON a.id = t1.id   -- t1 is a plain parquet table
   ```
   
   Both `true` rows have no `GroupPartitionsExec`, so nothing re-sorts the 
keyed side. This behaves identically on `master`, so it is pre-existing and not 
something this PR introduced - a separate ticket is fine. I raise it here 
because it is the same root cause, and because both halves of a fix live next 
to this diff.
   
   The sort cannot move into the `else` branch: there `partitioning` is the 
child's own declared layout, and sorting it would misreport the child, whereas 
the projected branch is honest only because `EnsureRequirements` inserts the 
`GroupPartitionsExec` that realises it. So the minimum is `.toGrouped` in 
`createPartitioning`, which makes the reported layout match what the 
partitioner builds - that alone re-arms the SPARK-56877 `require` and turns the 
silent wrong answer into a loud failure. To keep the query working you then 
need either `canCreatePartitioning` to refuse a spec whose `partitionKeys` are 
not sorted (falls back to hash shuffles on both sides, loses the 
storage-partitioned join), or a `GroupPartitionsExec` on the keyed side with 
`expectedPartitionKeys` set to the sorted keys, the way the 
`OrderedDistribution` branch already does at `EnsureRequirements.scala:99-107`.
   
   Do you want to take this as a follow-up, or shall I file the ticket and put 
up the fix? Either works for me.
   



##########
sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala:
##########
@@ -4207,6 +4207,40 @@ class KeyGroupedPartitioningSuite extends 
DistributionAndOrderingSuiteBase with
     }
   }
 
+  test("SPARK-56877: v2 bucketed table with subset join keys joining v1 
table") {

Review Comment:
   **Finding 3.** `SPARK-56877` is the ticket that added the invariant; this 
change is `SPARK-58988`, so anyone grepping for the new ticket's coverage will 
not find it.
   
   ```suggestion
     test("SPARK-58988: v2 bucketed table with subset join keys joining v1 
table") {
   ```
   
   The `SPARK-56877` reference is worth keeping in the comment body, which 
already has it.
   



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