This is an automated email from the ASF dual-hosted git repository.

baibaichen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git


The following commit(s) were added to refs/heads/main by this push:
     new d3325b0000 [GLUTEN-13174][VL] Re-enable the Spark 4.2 
storage-partitioned join tests (#13243)
d3325b0000 is described below

commit d3325b000053c48864442e0452af64ef26ac9b61
Author: Akshay Tayal <[email protected]>
AuthorDate: Thu Oct 15 19:32:41 2026 -0700

    [GLUTEN-13174][VL] Re-enable the Spark 4.2 storage-partitioned join tests 
(#13243)
    
    * [GLUTEN-13174][VL] Re-enable the Spark 4.2 storage-partitioned join tests
    
    Remove the 29 GlutenKeyGroupedPartitioningSuite excludes that #13163 added 
for
    Spark 4.2, so the spark42 UT CI shows which of them still fail on current
    main. Fixes for the remaining failures follow in this PR.
    
    Co-authored-by: Copilot <[email protected]>
    Copilot-Session: 3a1d1e30-b32c-45b1-9f0e-d7aa4654eab9
    
    * [GLUTEN-13174][VL] Leave the SPJ sorted-merge coalescing tests to a 
separate PR
    
    Keep the 4 SPARK-55715 / SPARK-56549 tests excluded here. They need a
    different fix and have their own PR.
    
    Co-authored-by: Copilot <[email protected]>
    Copilot-Session: 3a1d1e30-b32c-45b1-9f0e-d7aa4654eab9
    
    * [GLUTEN-13174][VL] Look for GroupPartitionsExec under 
SortMergeJoinExecTransformer
    
    Spark 4.2 collectGroupPartitions only looks under a vanilla 
SortMergeJoinExec,
    which Gluten replaces with SortMergeJoinExecTransformer. Override it the 
same
    way as collectShuffles. This fixes 8 of the re-enabled tests.
    
    Co-authored-by: Copilot <[email protected]>
    Copilot-Session: 3a1d1e30-b32c-45b1-9f0e-d7aa4654eab9
    
    * [GLUTEN-13174][VL] Port the Gluten SPJ test copies to Spark 4.2
    
    Spark 4.2 moved SPJ partition grouping from BatchScanExec into
    GroupPartitionsExec and updated its KeyGroupedPartitioningSuite for it.
    Bring the Gluten copies in GlutenKeyGroupedPartitioningSuite in line:
    
    - SPARK-42038 (8 tests), SPARK-44647 (3 tests) and SPARK-47094: check
      the GroupPartitionsExec output partitions instead of the raw scan
      split counts, with the Spark 4.2 expectations (no GroupPartitionsExec
      when SPJ is not used). The SPARK-42038 checks also require a
      non-empty result.
    - SPARK-53322: Spark 4.2 uses checkpointed scans for SPJ. Replace the
      "aren't used for SPJ" and "can't shuffle other children on SPJ" copies
      with the Spark 4.2 "are used for SPJ" and "can shuffle other children
      on SPJ" tests, and remove "can be shuffled by children on SPJ", which
      Spark 4.2 dropped. Add the Spark 4.2 GroupPartitionsExec checks to
      "avoid shuffles for aggregates".
    - collectAllShuffles returns vanilla and columnar shuffles again, as the
      inherited tests expect. Native-only checks use the new
      collectAllColumnarShuffles.
    - Add Gluten variants of the two SPARK-56241 tests, which look for a
      vanilla SortMergeJoinExec, and of "SPARK-55848: checkpointed
      partially-clustered join with dedup", which looks for a vanilla
      RDDScanExec that Gluten offloads to an RDDScanTransformer. The vanilla
      tests are excluded.
    
    Co-authored-by: Copilot <[email protected]>
    Copilot-Session: 3a1d1e30-b32c-45b1-9f0e-d7aa4654eab9
    
    ---------
    
    Co-authored-by: Akshay Tayal <[email protected]>
    Co-authored-by: Copilot <[email protected]>
    Copilot-Session: 3a1d1e30-b32c-45b1-9f0e-d7aa4654eab9
---
 .../gluten/utils/velox/VeloxTestSettings.scala     |  80 +----
 .../GlutenKeyGroupedPartitioningSuite.scala        | 329 ++++++++++++++++-----
 2 files changed, 253 insertions(+), 156 deletions(-)

diff --git 
a/gluten-ut/spark42/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
 
b/gluten-ut/spark42/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
index 2023bf3236..08fbd9c469 100644
--- 
a/gluten-ut/spark42/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
+++ 
b/gluten-ut/spark42/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
@@ -111,6 +111,7 @@ class VeloxTestSettings extends BackendTestSettings {
     .exclude("partitioned join: number of buckets mismatch should trigger 
shuffle")
     .exclude("partitioned join: only one side reports partitioning")
     .exclude("partitioned join: join with two partition keys and different # 
of partition keys")
+    .exclude("SPARK-55848: checkpointed partially-clustered join with dedup")
     .excludeByPrefix("SPARK-47094")
     .excludeByPrefix("SPARK-48655")
     .excludeByPrefix("SPARK-48012")
@@ -121,58 +122,13 @@ class VeloxTestSettings extends BackendTestSettings {
     // disable due to check for SMJ node
     .excludeByPrefix("SPARK-41413: partitioned join:")
     .excludeByPrefix("SPARK-42038: partially clustered:")
-    .exclude("SPARK-44641: duplicated records when SPJ is not triggered")
-    .exclude(
-      "Gluten - SPARK-42038: partially clustered: full outer join is not 
applicable"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "Gluten - SPARK-42038: partially clustered: left outer join"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "Gluten - SPARK-42038: partially clustered: right outer join"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "Gluten - SPARK-42038: partially clustered: with different partition 
keys and both sides partially clustered"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "Gluten - SPARK-42038: partially clustered: with different partition 
keys and missing keys on left-hand side"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "Gluten - SPARK-42038: partially clustered: with different partition 
keys and missing keys on right-hand side"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "Gluten - SPARK-42038: partially clustered: with same partition keys and 
both sides partially clustered"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "Gluten - SPARK-42038: partially clustered: with same partition keys and 
one side fully clustered"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "Gluten - SPARK-44647: SPJ: test join key is subset of cluster key with 
push values and partially-clustered"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "Gluten - SPARK-44647: test join key is the second partition key and a 
transform"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "Gluten - SPARK-47094: Compatible buckets does not support SPJ with 
push-down values or partially-clustered"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "Gluten - SPARK-53322: checkpointed scans aren't used for SPJ"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
     .exclude(
-      "Gluten - SPARK-53322: checkpointed scans can be shuffled by children on 
SPJ"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "Gluten - SPARK-53322: checkpointed scans can't shuffle other children 
on SPJ"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "SPARK-48065: SPJ: allowJoinKeysSubsetOfPartitionKeys is too strict"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "SPARK-55535: Multi table join granular partition grouping"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
+      "SPARK-56241: GroupPartitionsExec non-coalescing passes through child 
ordering, " +
+        "no pre-join SortExec needed before SortMergeJoin")
     .exclude(
-      "SPARK-55535: Multi table join partial clustering"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
+      "SPARK-56241: GroupPartitionsExec coalescing derives ordering from key 
expressions, " +
+        "no pre-join SortExec needed before SortMergeJoin")
+    .exclude("SPARK-44641: duplicated records when SPJ is not triggered")
     .exclude(
       "SPARK-55715: Custom metrics of sorted-merge coalesced partitions"
     ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
@@ -182,33 +138,9 @@ class VeloxTestSettings extends BackendTestSettings {
     .exclude(
       "SPARK-55715: preserve outputOrdering when coalescing 
transform-partitioned splits"
     ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "SPARK-55848: Window dedup after SPJ with partial clustering"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "SPARK-55848: checkpointed partially-clustered join with dedup"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "SPARK-55848: dropDuplicates after SPJ with partial clustering"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "SPARK-56241: GroupPartitionsExec coalescing derives ordering from key 
expressions, no pre-join SortExec needed before SortMergeJoin"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "SPARK-56241: GroupPartitionsExec non-coalescing passes through child 
ordering, no pre-join SortExec needed before SortMergeJoin"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
     .exclude(
       "SPARK-56549: k-way merge enabled only when parent requires ordering"
     ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "partitioned join: exact distribution (same number of buckets) from both 
sides"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "partitioned join: join with two partition keys and matching & sorted 
partitions"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "partitioned join: join with two partition keys and unsorted partitions"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
   enableSuite[GlutenLocalScanSuite]
   enableSuite[GlutenMetadataColumnSuite]
   enableSuite[GlutenSupportsCatalogOptionsSuite]
diff --git 
a/gluten-ut/spark42/src/test/scala/org/apache/spark/sql/connector/GlutenKeyGroupedPartitioningSuite.scala
 
b/gluten-ut/spark42/src/test/scala/org/apache/spark/sql/connector/GlutenKeyGroupedPartitioningSuite.scala
index 9d03138809..4c665e5dee 100644
--- 
a/gluten-ut/spark42/src/test/scala/org/apache/spark/sql/connector/GlutenKeyGroupedPartitioningSuite.scala
+++ 
b/gluten-ut/spark42/src/test/scala/org/apache/spark/sql/connector/GlutenKeyGroupedPartitioningSuite.scala
@@ -17,7 +17,7 @@
 package org.apache.spark.sql.connector
 
 import org.apache.gluten.config.GlutenConfig
-import org.apache.gluten.execution.SortMergeJoinExecTransformer
+import org.apache.gluten.execution.{SortExecTransformer, 
SortMergeJoinExecTransformer}
 
 import org.apache.spark.SparkConf
 import org.apache.spark.sql.{DataFrame, GlutenSQLTestsBaseTrait, Row}
@@ -26,8 +26,8 @@ import org.apache.spark.sql.connector.catalog.{Column, 
Identifier, InMemoryTable
 import org.apache.spark.sql.connector.distributions.Distributions
 import org.apache.spark.sql.connector.expressions.Expressions.{bucket, days, 
identity, years}
 import org.apache.spark.sql.connector.expressions.Transform
-import org.apache.spark.sql.execution.{ColumnarShuffleExchangeExec, SparkPlan}
-import org.apache.spark.sql.execution.datasources.v2.BatchScanExec
+import org.apache.spark.sql.execution.{ColumnarShuffleExchangeExec, 
RDDScanExec, RDDScanTransformer, SortExec, SparkPlan}
+import org.apache.spark.sql.execution.datasources.v2.GroupPartitionsExec
 import org.apache.spark.sql.execution.exchange.{ShuffleExchangeExec, 
ShuffleExchangeLike}
 import org.apache.spark.sql.execution.joins.SortMergeJoinExec
 import org.apache.spark.sql.functions.{col, max}
@@ -100,7 +100,22 @@ class GlutenKeyGroupedPartitioningSuite
         })
   }
 
-  override protected def collectAllShuffles(plan: SparkPlan): 
Seq[ColumnarShuffleExchangeExec] = {
+  override protected def collectGroupPartitions(plan: SparkPlan): 
Seq[GroupPartitionsExec] = {
+    // here we skip collecting group partitions operators that are not 
associated with SMJ
+    collect(plan) {
+      case s: SortMergeJoinExec => s
+      case s: SortMergeJoinExecTransformer => s
+    }.flatMap(smj => collect(smj) { case g: GroupPartitionsExec => g })
+  }.toSet.toSeq
+
+  override protected def collectAllShuffles(plan: SparkPlan): 
Seq[ShuffleExchangeLike] = {
+    collect(plan) {
+      case s: ShuffleExchangeExec => s
+      case s: ColumnarShuffleExchangeExec => s
+    }
+  }
+
+  private def collectAllColumnarShuffles(plan: SparkPlan): 
Seq[ColumnarShuffleExchangeExec] = {
     collect(plan) { case s: ColumnarShuffleExchangeExec => s }
   }
 
@@ -108,10 +123,6 @@ class GlutenKeyGroupedPartitioningSuite
     collect(plan) { case s: ShuffleExchangeExec => s }
   }
 
-  private def collectScans(plan: SparkPlan): Seq[BatchScanExec] = {
-    collect(plan) { case s: BatchScanExec => s }
-  }
-
   private def selectWithMergeJoinHint(t1: String, t2: String): String = {
     s"SELECT /*+ MERGE($t1, $t2) */ "
   }
@@ -359,8 +370,11 @@ class GlutenKeyGroupedPartitioningSuite
               val shuffles = 
collectColumnarShuffleExchangeExec(df.queryExecution.executedPlan)
               assert(shuffles.isEmpty, "should not contain any shuffle")
               if (pushDownValues) {
-                val scans = collectScans(df.queryExecution.executedPlan)
-                assert(scans.forall(_.inputRDD.partitions.length == expected))
+                val groupPartitions = 
collectGroupPartitions(df.queryExecution.executedPlan)
+                  .map(_.outputPartitioning.numPartitions)
+                assert(
+                  groupPartitions.nonEmpty && groupPartitions.forall(_ == 
expected),
+                  s"Expected $expected but got $groupPartitions")
               }
               checkAnswer(
                 df,
@@ -415,8 +429,11 @@ class GlutenKeyGroupedPartitioningSuite
               val shuffles = 
collectColumnarShuffleExchangeExec(df.queryExecution.executedPlan)
               assert(shuffles.isEmpty, "should not contain any shuffle")
               if (pushDownValues) {
-                val scans = collectScans(df.queryExecution.executedPlan)
-                assert(scans.forall(_.inputRDD.partitions.length == expected))
+                val groupPartitions = 
collectGroupPartitions(df.queryExecution.executedPlan)
+                  .map(_.outputPartitioning.numPartitions)
+                assert(
+                  groupPartitions.nonEmpty && groupPartitions.forall(_ == 
expected),
+                  s"Expected $expected but got $groupPartitions")
               }
               checkAnswer(
                 df,
@@ -481,8 +498,11 @@ class GlutenKeyGroupedPartitioningSuite
               val shuffles = 
collectColumnarShuffleExchangeExec(df.queryExecution.executedPlan)
               if (pushDownValues) {
                 assert(shuffles.isEmpty, "should not contain any shuffle")
-                val scans = collectScans(df.queryExecution.executedPlan)
-                assert(scans.forall(_.inputRDD.partitions.length == expected))
+                val groupPartitions = 
collectGroupPartitions(df.queryExecution.executedPlan)
+                  .map(_.outputPartitioning.numPartitions)
+                assert(
+                  groupPartitions.nonEmpty && groupPartitions.forall(_ == 
expected),
+                  s"Expected $expected but got $groupPartitions")
               } else {
                 assert(
                   shuffles.nonEmpty,
@@ -550,8 +570,11 @@ class GlutenKeyGroupedPartitioningSuite
               val shuffles = 
collectColumnarShuffleExchangeExec(df.queryExecution.executedPlan)
               if (pushDownValues) {
                 assert(shuffles.isEmpty, "should not contain any shuffle")
-                val scans = collectScans(df.queryExecution.executedPlan)
-                assert(scans.forall(_.inputRDD.partitions.length == expected))
+                val groupPartitions = 
collectGroupPartitions(df.queryExecution.executedPlan)
+                  .map(_.outputPartitioning.numPartitions)
+                assert(
+                  groupPartitions.nonEmpty && groupPartitions.forall(_ == 
expected),
+                  s"Expected $expected but got $groupPartitions")
               } else {
                 assert(
                   shuffles.nonEmpty,
@@ -608,8 +631,11 @@ class GlutenKeyGroupedPartitioningSuite
               val shuffles = 
collectColumnarShuffleExchangeExec(df.queryExecution.executedPlan)
               if (pushDownValues) {
                 assert(shuffles.isEmpty, "should not contain any shuffle")
-                val scans = collectScans(df.queryExecution.executedPlan)
-                assert(scans.forall(_.inputRDD.partitions.length == expected))
+                val groupPartitions = 
collectGroupPartitions(df.queryExecution.executedPlan)
+                  .map(_.outputPartitioning.numPartitions)
+                assert(
+                  groupPartitions.nonEmpty && groupPartitions.forall(_ == 
expected),
+                  s"Expected $expected but got $groupPartitions")
               } else {
                 assert(
                   shuffles.nonEmpty,
@@ -663,10 +689,11 @@ class GlutenKeyGroupedPartitioningSuite
               val shuffles = 
collectColumnarShuffleExchangeExec(df.queryExecution.executedPlan)
               if (pushDownValues) {
                 assert(shuffles.isEmpty, "should not contain any shuffle")
-                val scans = collectScans(df.queryExecution.executedPlan)
+                val groupPartitions = 
collectGroupPartitions(df.queryExecution.executedPlan)
+                  .map(_.outputPartitioning.numPartitions)
                 assert(
-                  scans.forall(_.inputRDD.partitions.length == expected),
-                  s"Expected $expected but got 
${scans.head.inputRDD.partitions.length}")
+                  groupPartitions.nonEmpty && groupPartitions.forall(_ == 
expected),
+                  s"Expected $expected but got $groupPartitions")
               } else {
                 assert(
                   shuffles.nonEmpty,
@@ -727,11 +754,11 @@ class GlutenKeyGroupedPartitioningSuite
               val shuffles = 
collectColumnarShuffleExchangeExec(df.queryExecution.executedPlan)
               if (pushDownValues) {
                 assert(shuffles.isEmpty, "should not contain any shuffle")
-                val scans = collectScans(df.queryExecution.executedPlan)
-                assert(scans.map(_.inputRDD.partitions.length).toSet.size == 1)
+                val groupPartitions = 
collectGroupPartitions(df.queryExecution.executedPlan)
+                  .map(_.outputPartitioning.numPartitions)
                 assert(
-                  scans.forall(_.inputRDD.partitions.length == expected),
-                  s"Expected $expected but got 
${scans.head.inputRDD.partitions.length}")
+                  groupPartitions.nonEmpty && groupPartitions.forall(_ == 
expected),
+                  s"Expected $expected but got $groupPartitions")
               } else {
                 assert(
                   shuffles.nonEmpty,
@@ -790,11 +817,11 @@ class GlutenKeyGroupedPartitioningSuite
               val shuffles = 
collectColumnarShuffleExchangeExec(df.queryExecution.executedPlan)
               if (pushDownValues) {
                 assert(shuffles.isEmpty, "should not contain any shuffle")
-                val scans = collectScans(df.queryExecution.executedPlan)
-                assert(scans.map(_.inputRDD.partitions.length).toSet.size == 1)
+                val groupPartitions = 
collectGroupPartitions(df.queryExecution.executedPlan)
+                  .map(_.outputPartitioning.numPartitions)
                 assert(
-                  scans.forall(_.inputRDD.partitions.length == expected),
-                  s"Expected $expected but got 
${scans.head.inputRDD.partitions.length}")
+                  groupPartitions.nonEmpty && groupPartitions.forall(_ == 
expected),
+                  s"Expected $expected but got $groupPartitions")
               } else {
                 assert(
                   shuffles.nonEmpty,
@@ -1113,7 +1140,7 @@ class GlutenKeyGroupedPartitioningSuite
         keyGroupedShuffles.nonEmpty,
         "KeyGroupedPartitioning shuffle should fall back to a vanilla 
ShuffleExchangeExec")
 
-      val columnarKeyGroupedShuffles = collectAllShuffles(plan)
+      val columnarKeyGroupedShuffles = collectAllColumnarShuffles(plan)
         .filter(_.outputPartitioning.isInstanceOf[KeyedPartitioning])
       assert(
         columnarKeyGroupedShuffles.isEmpty,
@@ -1320,23 +1347,23 @@ class GlutenKeyGroupedPartitioningSuite
                         assert(shuffles.nonEmpty, "SPJ should not be 
triggered")
                       }
 
-                      val scannedPartitions = 
collectScans(df.queryExecution.executedPlan)
-                        .map(_.inputRDD.partitions.length)
+                      val groupPartitions = 
collectGroupPartitions(df.queryExecution.executedPlan)
+                        .map(_.outputPartitioning.numPartitions)
                       (allowJoinKeysSubsetOfPartitionKeys, partiallyClustered, 
filter) match {
                         // SPJ, partially-clustered, with filter
-                        case (true, true, true) => assert(scannedPartitions == 
Seq(6, 6))
+                        case (true, true, true) => assert(groupPartitions == 
Seq(6, 6))
 
                         // SPJ, partially-clustered, no filter
-                        case (true, true, false) => assert(scannedPartitions 
== Seq(8, 8))
+                        case (true, true, false) => assert(groupPartitions == 
Seq(8, 8))
 
                         // SPJ and not partially-clustered, with filter
-                        case (true, false, true) => assert(scannedPartitions 
== Seq(2, 2))
+                        case (true, false, true) => assert(groupPartitions == 
Seq(2, 2))
 
                         // SPJ and not partially-clustered, no filter
-                        case (true, false, false) => assert(scannedPartitions 
== Seq(4, 4))
+                        case (true, false, false) => assert(groupPartitions == 
Seq(4, 4))
 
                         // No SPJ
-                        case _ => assert(scannedPartitions == Seq(5, 4))
+                        case _ => assert(groupPartitions.isEmpty)
                       }
 
                       checkAnswer(
@@ -1419,13 +1446,13 @@ class GlutenKeyGroupedPartitioningSuite
                     assert(shuffles.nonEmpty, "SPJ should not be triggered")
                   }
 
-                  val scans = collectScans(df.queryExecution.executedPlan)
-                    .map(_.inputRDD.partitions.length)
+                  val partitions = 
collectGroupPartitions(df.queryExecution.executedPlan)
+                    .map(_.outputPartitioning.numPartitions)
                   (pushDownValues, allowJoinKeysSubsetOfPartitionKeys, 
partiallyClustered) match {
                     // SPJ and partially-clustered
-                    case (true, true, true) => assert(scans == Seq(3, 3))
+                    case (_, true, _) => assert(partitions == Seq(3, 3))
                     // non-SPJ or SPJ/partially-clustered
-                    case _ => assert(scans == Seq(3, 3))
+                    case _ => assert(partitions.isEmpty)
                   }
                 }
             }
@@ -1479,15 +1506,15 @@ class GlutenKeyGroupedPartitioningSuite
                     assert(shuffles.nonEmpty, "SPJ should not be triggered")
                   }
 
-                  val scans = collectScans(df.queryExecution.executedPlan)
-                    .map(_.inputRDD.partitions.length)
+                  val partitions = 
collectGroupPartitions(df.queryExecution.executedPlan)
+                    .map(_.outputPartitioning.numPartitions)
                   (allowJoinKeysSubsetOfPartitionKeys, partiallyClustered) 
match {
                     // SPJ and partially-clustered
-                    case (true, true) => assert(scans == Seq(5, 5))
+                    case (true, true) => assert(partitions == Seq(5, 5))
                     // SPJ and not partially-clustered
-                    case (true, false) => assert(scans == Seq(3, 3))
+                    case (true, false) => assert(partitions == Seq(3, 3))
                     // No SPJ
-                    case _ => assert(scans == Seq(4, 4))
+                    case _ => assert(partitions.isEmpty)
                   }
 
                   checkAnswer(
@@ -1599,16 +1626,16 @@ class GlutenKeyGroupedPartitioningSuite
                               |""".stripMargin)
 
               val shuffles = 
collectColumnarShuffleExchangeExec(df.queryExecution.executedPlan)
-              val scans =
-                
collectScans(df.queryExecution.executedPlan).map(_.inputRDD.partitions.length)
+              val partitions = 
collectGroupPartitions(df.queryExecution.executedPlan)
+                .map(_.outputPartitioning.numPartitions)
 
               (allowPushDown, partiallyClustered) match {
                 case (true, false) =>
                   assert(shuffles.isEmpty, "SPJ should be triggered")
-                  assert(scans == Seq(2, 2))
+                  assert(partitions == Seq(2, 2))
                 case (_, _) =>
                   assert(shuffles.nonEmpty, "SPJ should not be triggered")
-                  assert(scans == Seq(3, 2))
+                  assert(partitions.isEmpty)
               }
 
               checkAnswer(
@@ -1900,10 +1927,13 @@ class GlutenKeyGroupedPartitioningSuite
         assert(
           shuffles.isEmpty,
           "should not contain shuffle when not grouping by partition values")
+        val groupPartitions = 
collectAllGroupPartitions(df.queryExecution.executedPlan)
+        assert(groupPartitions.size === 1)
+        assert(groupPartitions.head.outputPartitioning.numPartitions == 3)
     }
   }
 
-  testGluten("SPARK-53322: checkpointed scans aren't used for SPJ") {
+  testGluten("SPARK-53322: checkpointed scans are used for SPJ") {
     withTempDir {
       dir =>
         spark.sparkContext.setCheckpointDir(dir.getPath)
@@ -1943,14 +1973,22 @@ class GlutenKeyGroupedPartitioningSuite
               .orderBy("id", "purchase_price", "sale_price")
             checkAnswer(df, Seq(Row(1, "aa", 41.0, 40.0), Row(3, "cc", 15.5, 
25.5)))
 
-            // One shuffle for sort and two shuffles for join are expected.
-            assert(collectAllShuffles(df.queryExecution.executedPlan).length 
=== 3)
+            val plan = df.queryExecution.executedPlan
+            if (pushdownValues) {
+              // 1 shuffle for SORT and 2 group partitions for JOIN are 
expected.
+              assert(collectAllShuffles(plan).length === 1)
+              assert(collectAllGroupPartitions(plan).length === 2)
+            } else {
+              // 1 shuffle for SORT and 2 shuffles for JOIN are expected.
+              assert(collectAllShuffles(plan).length === 3)
+              assert(collectAllGroupPartitions(plan).isEmpty)
+            }
           }
         }
     }
   }
 
-  testGluten("SPARK-53322: checkpointed scans can't shuffle other children on 
SPJ") {
+  testGluten("SPARK-53322: checkpointed scans can shuffle other children on 
SPJ") {
     withTempDir {
       dir =>
         spark.sparkContext.setCheckpointDir(dir.getPath)
@@ -1985,51 +2023,76 @@ class GlutenKeyGroupedPartitioningSuite
                 .orderBy("id", "purchase_price", "sale_price")
               checkAnswer(df, Seq(Row(1, "aa", 41.0, 40.0), Row(3, "cc", 15.5, 
25.5)))
 
-              // One shuffle for sort and two shuffles for join are expected.
-              assert(collectAllShuffles(df.queryExecution.executedPlan).length 
=== 3)
+              val plan = df.queryExecution.executedPlan
+              // 1 shuffle for SORT and 1 shuffle for JOIN are expected.
+              assert(collectAllShuffles(plan).length === 2)
+              // The JOIN shuffle has KeyedPartitioning and falls back to 
vanilla Spark
+              // (GLUTEN-10992). The SORT shuffle is native.
+              assert(collectVanillaShuffles(plan).length === 1)
+              assert(collectAllColumnarShuffles(plan).length === 1)
+              // 0 group partitions are expected because both sides of the 
join are clustered
+              assert(collectAllGroupPartitions(plan).isEmpty)
             }
         }
     }
   }
 
-  testGluten("SPARK-53322: checkpointed scans can be shuffled by children on 
SPJ") {
+  testGluten("SPARK-55848: checkpointed partially-clustered join with dedup") {
     withTempDir {
       dir =>
         spark.sparkContext.setCheckpointDir(dir.getPath)
-        val itemsPartitions = Array(identity("id"))
-        createTable(items, itemsColumns, itemsPartitions)
+        val items_partitions = Array(identity("id"))
+        createTable(items, itemsColumns, items_partitions)
         sql(
           s"INSERT INTO testcat.ns.$items VALUES " +
-            "(1, 'aa', 41.0, cast('2020-01-01' as timestamp)), " +
-            "(2, 'bb', 10.0, cast('2020-01-02' as timestamp)), " +
-            "(3, 'cc', 15.5, cast('2020-01-03' as timestamp))")
+            "(1, 'aa', 40.0, cast('2020-01-01' as timestamp)), " +
+            "(1, 'aa', 41.0, cast('2020-01-15' as timestamp)), " +
+            "(2, 'bb', 10.0, cast('2020-01-01' as timestamp)), " +
+            "(3, 'cc', 15.5, cast('2020-02-01' as timestamp))")
 
-        createTable(purchases, purchasesColumns, Array(identity("item_id")))
+        val purchases_partitions = Array(identity("item_id"))
+        createTable(purchases, purchasesColumns, purchases_partitions)
         sql(
           s"INSERT INTO testcat.ns.$purchases VALUES " +
-            "(1, 40.0, cast('2020-01-01' as timestamp)), " +
-            "(3, 25.5, cast('2020-01-03' as timestamp)), " +
-            "(4, 20.0, cast('2020-01-04' as timestamp))")
+            "(1, 42.0, cast('2020-01-01' as timestamp)), " +
+            "(1, 50.0, cast('2020-01-02' as timestamp)), " +
+            "(2, 11.0, cast('2020-01-01' as timestamp)), " +
+            "(3, 19.5, cast('2020-02-01' as timestamp))")
 
         withSQLConf(
+          SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
           SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1",
-          SQLConf.V2_BUCKETING_SHUFFLE_ENABLED.key -> "true",
-          SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> "true"
+          SQLConf.V2_BUCKETING_PARTIALLY_CLUSTERED_DISTRIBUTION_ENABLED.key -> 
true.toString
         ) {
-          val scanDF1 = 
spark.read.table(s"testcat.ns.$items").checkpoint().as("i")
-          val scanDF2 = spark.read.table(s"testcat.ns.$purchases").as("p")
-
-          val df = scanDF1
-            .join(scanDF2, col("id") === col("item_id"))
-            .selectExpr("id", "name", "i.price AS purchase_price", "p.price AS 
sale_price")
-            .orderBy("id", "purchase_price", "sale_price")
-          checkAnswer(df, Seq(Row(1, "aa", 41.0, 40.0), Row(3, "cc", 15.5, 
25.5)))
-
-          // One shuffle for sort and one vanilla shuffle for one side of join 
are expected.
-          val plan = df.queryExecution.executedPlan
-          assert(collectAllShuffles(plan).length === 1)
-          // KeyGroupedPartitioning shuffle falls back to vanilla 
ShuffleExchangeExec
-          assert(collectVanillaShuffles(plan).length === 1)
+          // Checkpoint the JOIN result (not the scan) so the checkpoint node 
carries the
+          // partially-clustered KeyGroupedPartitioning. The dedup on top must 
still insert
+          // the required grouping operator because partially-clustered 
partitioning does not
+          // satisfy ClusteredDistribution.
+          val joinedDf = sql(s"""${selectWithMergeJoinHint("i", "p")} i.id, 
i.name, i.price
+                                |FROM testcat.ns.$items i
+                                |JOIN testcat.ns.$purchases p ON i.id = 
p.item_id""".stripMargin)
+          val checkpointedDf = joinedDf.checkpoint()
+          val df = checkpointedDf.select("id").distinct()
+          checkAnswer(df, Seq(Row(1), Row(2), Row(3)))
+
+          // Gluten offloads the checkpoint's RDDScanExec to an 
RDDScanTransformer, which keeps
+          // the output partitioning of the RDDScanExec.
+          val checkpointScans = collect(df.queryExecution.executedPlan) {
+            case r: RDDScanExec => r
+            case r: RDDScanTransformer => r
+          }
+          assert(
+            checkpointScans.exists(_.outputPartitioning match {
+              case kp: KeyedPartitioning => !kp.isGrouped
+              case _ => false
+            }),
+            "checkpoint scan should have ungrouped KeyedPartitioning"
+          )
+
+          val allGroupPartitions = 
collectAllGroupPartitions(df.queryExecution.executedPlan)
+          assert(
+            allGroupPartitions.size === 1,
+            "expected 1 GroupPartitionsExec above the checkpointed join for 
dedup")
         }
     }
   }
@@ -2085,4 +2148,106 @@ class GlutenKeyGroupedPartitioningSuite
       checkAnswer(df, Seq(Row(1, "aa", 40.0, 42.0)))
     }
   }
+
+  private def assertNoSortBeforeSortMergeJoin(plan: SparkPlan): Unit = {
+    val smjs = collect(plan) { case j: SortMergeJoinExecTransformer => j }
+    assert(smjs.nonEmpty, "expected SortMergeJoinExecTransformer in plan")
+    smjs.foreach {
+      smj =>
+        val sorts = smj.children.flatMap {
+          child =>
+            collect(child) {
+              case s: SortExec => s
+              case s: SortExecTransformer => s
+            }
+        }
+        assert(sorts.isEmpty, s"should not add a sort before SMJ, but found: 
$sorts")
+    }
+  }
+
+  testGluten(
+    "SPARK-56241: GroupPartitionsExec non-coalescing passes through child 
ordering, " +
+      "no pre-join SortExec needed before SortMergeJoin") {
+    // Non-identical key sets force GroupPartitionsExec to be inserted on both 
sides align them,
+    // but each group has exactly one partition — no coalescing.
+    val items_partitions = Array(identity("id"))
+    createTable(items, itemsColumns, items_partitions)
+    sql(
+      s"INSERT INTO testcat.ns.$items VALUES " +
+        "(1, 'aa', 10.0, cast('2021-01-01' as timestamp)), " +
+        "(2, 'bb', 20.0, cast('2021-01-01' as timestamp)), " +
+        "(3, 'cc', 30.0, cast('2021-01-01' as timestamp))")
+
+    val purchases_partitions = Array(identity("item_id"))
+    createTable(purchases, purchasesColumns, purchases_partitions)
+    sql(
+      s"INSERT INTO testcat.ns.$purchases VALUES " +
+        "(1, 100.0, cast('2021-01-01' as timestamp)), " +
+        "(2, 200.0, cast('2021-01-01' as timestamp))")
+
+    // GroupPartitionsExec passes through the child's key-derived 
outputOrdering.
+    // EnsureRequirements checks outputOrdering directly so no sort should be 
inserted before
+    // the SMJ.
+    withSQLConf(SQLConf.V2_BUCKETING_PARTITION_KEY_ORDERING_ENABLED.key -> 
"true") {
+      val df = sql(s"""
+                      |${selectWithMergeJoinHint("i", "p")}
+                      |i.id, i.name
+                      |FROM testcat.ns.$items i JOIN testcat.ns.$purchases p 
ON p.item_id = i.id
+                      |""".stripMargin)
+
+      checkAnswer(df, Seq(Row(1, "aa"), Row(2, "bb")))
+
+      val plan = df.queryExecution.executedPlan
+      val groupPartitions = collectGroupPartitions(plan)
+      assert(groupPartitions.nonEmpty, "expected GroupPartitionsExec in plan")
+      assert(
+        groupPartitions.forall(_.groupedPartitions.forall(_._2.size <= 1)),
+        "expected non-coalescing GroupPartitionsExec")
+      assertNoSortBeforeSortMergeJoin(plan)
+    }
+  }
+
+  testGluten(
+    "SPARK-56241: GroupPartitionsExec coalescing derives ordering from key 
expressions, " +
+      "no pre-join SortExec needed before SortMergeJoin") {
+    // Duplicate key 1 on both sides causes coalescing.
+    val items_partitions = Array(identity("id"))
+    createTable(items, itemsColumns, items_partitions)
+    sql(
+      s"INSERT INTO testcat.ns.$items VALUES " +
+        "(1, 'aa', 10.0, cast('2021-01-01' as timestamp)), " +
+        "(1, 'ab', 11.0, cast('2021-06-01' as timestamp)), " +
+        "(2, 'bb', 20.0, cast('2021-01-01' as timestamp))")
+
+    val purchases_partitions = Array(identity("item_id"))
+    createTable(purchases, purchasesColumns, purchases_partitions)
+    sql(
+      s"INSERT INTO testcat.ns.$purchases VALUES " +
+        "(1, 100.0, cast('2021-01-01' as timestamp)), " +
+        "(1, 110.0, cast('2021-06-01' as timestamp)), " +
+        "(2, 200.0, cast('2021-01-01' as timestamp))")
+
+    // GroupPartitionsExec derives outputOrdering from the key expressions 
after coalescing.
+    // EnsureRequirements checks outputOrdering directly so no sort should be 
inserted before
+    // the SMJ.
+    withSQLConf(
+      SQLConf.V2_BUCKETING_PARTITION_KEY_ORDERING_ENABLED.key -> "true",
+      SQLConf.V2_BUCKETING_PRESERVE_KEY_ORDERING_ON_COALESCE_ENABLED.key -> 
"true") {
+      val df = sql(s"""
+                      |${selectWithMergeJoinHint("i", "p")}
+                      |i.id, i.name
+                      |FROM testcat.ns.$items i JOIN testcat.ns.$purchases p 
ON p.item_id = i.id
+                      |""".stripMargin)
+
+      checkAnswer(df, Seq(Row(1, "aa"), Row(1, "aa"), Row(1, "ab"), Row(1, 
"ab"), Row(2, "bb")))
+
+      val plan = df.queryExecution.executedPlan
+      val groupPartitions = collectGroupPartitions(plan)
+      assert(groupPartitions.nonEmpty, "expected GroupPartitionsExec in plan")
+      assert(
+        groupPartitions.exists(_.groupedPartitions.exists(_._2.size > 1)),
+        "expected coalescing GroupPartitionsExec")
+      assertNoSortBeforeSortMergeJoin(plan)
+    }
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to