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 39d80db554 [GLUTEN-13174][VL] Add Gluten versions of the Spark 4.2 SPJ 
sorted-merge tests (#13246)
39d80db554 is described below

commit 39d80db5545113d5ca38c01eb7d3fceb9300d0fb
Author: Akshay Tayal <[email protected]>
AuthorDate: Wed Oct 21 00:14:00 2026 -0700

    [GLUTEN-13174][VL] Add Gluten versions of the Spark 4.2 SPJ sorted-merge 
tests (#13246)
    
    Spark 4.2's GroupPartitionsExec can coalesce partitions with a k-way
    sorted merge (SortedMergeCoalescedRDD) that keeps the scan ordering, but
    only when every node below it is SafeForKWayMerge, a short list of
    vanilla nodes. Gluten offloads the Filter/Project between the scan and
    GroupPartitionsExec, so the sorted merge is not used: GroupPartitionsExec
    runs columnar, concatenates the coalesced splits and reports only the
    ordering that survives that. Gluten's EnsureLocalSortRequirements then
    adds the sort the sort-merge join needs, so the results stay correct.
    
    The 4 Spark tests check the sorted-merge path and look for vanilla
    SortMergeJoinExec / ShuffledHashJoinExec nodes. Keep them excluded and
    add Gluten versions that:
    - check the Gluten join transformers and SortExec / SortExecTransformer,
    - run the sorted-merge checks with filter and project offload turned
      off, so the subtree under GroupPartitionsExec stays vanilla, and
    - for the two preserve-ordering tests, also run with the Gluten defaults
      and check that a sort is added before the join.
    
    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     |  18 +-
 .../GlutenKeyGroupedPartitioningSuite.scala        | 374 ++++++++++++++++++++-
 2 files changed, 376 insertions(+), 16 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 91ec7d1137..ac13d5f467 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
@@ -103,18 +103,12 @@ class VeloxTestSettings extends BackendTestSettings {
       "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)
-    .exclude(
-      "SPARK-55715: preserve outputOrdering when coalescing partitions with 
sorted merge"
-    ) // GLUTEN-12569 Spark 4.2 UT enablement (CI-confirmed)
-    .exclude(
-      "SPARK-55715: preserve outputOrdering when coalescing 
transform-partitioned splits"
-    ) // 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)
+    // Rewritten: the sorted merge needs the GroupPartitionsExec child to stay 
vanilla, and the
+    // tests check vanilla SortMergeJoinExec / ShuffledHashJoinExec nodes.
+    .exclude("SPARK-55715: Custom metrics of sorted-merge coalesced 
partitions")
+    .exclude("SPARK-55715: preserve outputOrdering when coalescing partitions 
with sorted merge")
+    .exclude("SPARK-55715: preserve outputOrdering when coalescing 
transform-partitioned splits")
+    .exclude("SPARK-56549: k-way merge enabled only when parent requires 
ordering")
   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 4c665e5dee..0647f71730 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,15 +17,16 @@
 package org.apache.spark.sql.connector
 
 import org.apache.gluten.config.GlutenConfig
-import org.apache.gluten.execution.{SortExecTransformer, 
SortMergeJoinExecTransformer}
+import org.apache.gluten.execution.{ShuffledHashJoinExecTransformerBase, 
SortExecTransformer, SortMergeJoinExecTransformer}
 
 import org.apache.spark.SparkConf
+import org.apache.spark.rdd.SortedMergeCoalescedRDD
 import org.apache.spark.sql.{DataFrame, GlutenSQLTestsBaseTrait, Row}
 import org.apache.spark.sql.catalyst.plans.physical.KeyedPartitioning
 import org.apache.spark.sql.connector.catalog.{Column, Identifier, 
InMemoryTableCatalog}
 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.connector.expressions.{FieldReference, 
NullOrdering, SortDirection, SortOrder, Transform}
+import org.apache.spark.sql.connector.expressions.Expressions.{bucket, days, 
identity, sort, years}
 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}
@@ -65,6 +66,7 @@ class GlutenKeyGroupedPartitioningSuite
       table: String,
       columns: Array[Column],
       partitions: Array[Transform],
+      ordering: Array[SortOrder] = Array.empty,
       catalog: InMemoryTableCatalog = catalog): Unit = {
     catalog.createTable(
       Identifier.of(Array("ns"), table),
@@ -72,7 +74,7 @@ class GlutenKeyGroupedPartitioningSuite
       partitions,
       emptyProps,
       Distributions.unspecified(),
-      Array.empty,
+      ordering,
       None,
       None,
       numRowsPerSplit = 1)
@@ -2250,4 +2252,368 @@ class GlutenKeyGroupedPartitioningSuite
       assertNoSortBeforeSortMergeJoin(plan)
     }
   }
+
+  // Gluten offloads the Filter/Project between the scan and a 
GroupPartitionsExec. Gluten
+  // operators are not SafeForKWayMerge, so the GroupPartitionsExec then can't 
use the sorted merge
+  // (SortedMergeCoalescedRDD): it concatenates the coalesced splits and 
reports only the ordering
+  // that survives that, and Gluten's EnsureLocalSortRequirements adds a sort 
before the sort-merge
+  // join. Turning the offload off keeps that subtree vanilla, so that the 
sorted merge is used.
+  private def groupPartitionsChildConfs(offload: Boolean): Seq[(String, 
String)] = Seq(
+    GlutenConfig.COLUMNAR_FILTER_ENABLED.key -> offload.toString,
+    GlutenConfig.COLUMNAR_PROJECT_ENABLED.key -> offload.toString)
+
+  testGluten("SPARK-55715: Custom metrics of sorted-merge coalesced 
partitions") {
+    // items has id=1 on three splits with interleaved arrive_times -- out of 
order across splits.
+    // purchases has item_id=1 on two splits, also out of order. Both sides 
coalesce under SMJ,
+    // using SortedMergeCoalescedRDD with multiple concurrent readers per 
task. This test verifies
+    // that all rows from both tables (5 + 4 = 9) are accounted for in the 
per-scan metrics.
+    val itemOrdering = Array(
+      sort(FieldReference("id"), SortDirection.ASCENDING, 
NullOrdering.NULLS_FIRST),
+      sort(FieldReference("arrive_time"), SortDirection.ASCENDING, 
NullOrdering.NULLS_FIRST)
+    )
+    createTable(items, itemsColumns, Array(identity("id")), itemOrdering)
+    // Rows inserted out of order: id=1 lands on partitions 1, 3, 4 with 
arrive_times
+    // [2022-03-10, 2021-05-20, 2025-09-01] -- out of order.
+    sql(
+      s"INSERT INTO testcat.ns.$items VALUES " +
+        "(3, 'dd', 40.0, cast('2024-01-01' as timestamp)), " +
+        "(1, 'bb', 20.0, cast('2022-03-10' as timestamp)), " +
+        "(2, 'cc', 30.0, cast('2023-06-15' as timestamp)), " +
+        "(1, 'aa', 10.0, cast('2021-05-20' as timestamp)), " +
+        "(1, 'ee', 50.0, cast('2025-09-01' as timestamp))")
+
+    val purchaseOrdering = Array(
+      sort(FieldReference("item_id"), SortDirection.ASCENDING, 
NullOrdering.NULLS_FIRST),
+      sort(FieldReference("time"), SortDirection.ASCENDING, 
NullOrdering.NULLS_FIRST)
+    )
+    createTable(purchases, purchasesColumns, Array(identity("item_id")), 
purchaseOrdering)
+    // item_id=1 lands on partitions 1 and 3 with times [2022-03-10, 
2021-05-20] -- out of order.
+    sql(
+      s"INSERT INTO testcat.ns.$purchases VALUES " +
+        "(2, 30.0, cast('2023-06-15' as timestamp)), " +
+        "(1, 20.0, cast('2022-03-10' as timestamp)), " +
+        "(3, 40.0, cast('2024-01-01' as timestamp)), " +
+        "(1, 10.0, cast('2021-05-20' as timestamp))")
+
+    withSQLConf(
+      (groupPartitionsChildConfs(offload = false) ++ Seq(
+        SQLConf.REQUIRE_ALL_CLUSTER_KEYS_FOR_CO_PARTITION.key -> "false",
+        SQLConf.V2_BUCKETING_ALLOW_JOIN_KEYS_SUBSET_OF_PARTITION_KEYS.key -> 
"true",
+        SQLConf.V2_BUCKETING_PRESERVE_ORDERING_ON_COALESCE_ENABLED.key -> 
"true"
+      )): _*) {
+      val metrics = runAndFetchMetrics {
+        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 AND 
p.time = i.arrive_time
+                        |""".stripMargin)
+        checkAnswer(df, Seq(Row(1, "aa"), Row(1, "bb"), Row(2, "cc"), Row(3, 
"dd")))
+        val plan = df.queryExecution.executedPlan
+        assert(
+          collect(plan) { case j: SortMergeJoinExecTransformer => j }.nonEmpty,
+          "expected SortMergeJoinExecTransformer")
+        val groupPartitions = collectAllGroupPartitions(plan)
+        val coalescingGP = 
groupPartitions.filter(_.groupedPartitions.exists(_._2.size > 1))
+        assert(coalescingGP.nonEmpty, "expected a coalescing 
GroupPartitionsExec")
+        coalescingGP.foreach {
+          gp =>
+            assert(
+              gp.execute().isInstanceOf[SortedMergeCoalescedRDD[_]],
+              "should use SortedMergeCoalescedRDD when preserve-ordering 
config is enabled")
+        }
+      }
+      assert(metrics.collect {
+        case ((_, "BatchScan testcat.ns.items", "number of rows read"), v) => v
+      } === Seq("5"))
+      assert(metrics.collect {
+        case ((_, "BatchScan testcat.ns.purchases", "number of rows read"), v) 
=> v
+      } === Seq("4"))
+    }
+  }
+
+  testGluten("SPARK-55715: preserve outputOrdering when coalescing partitions 
with sorted merge") {
+    // Both tables are partitioned by their id column and report ordering [id 
ASC, arrive_time ASC]
+    // via SupportsReportOrdering. Each has two rows with id=1 (two splits), 
so GroupPartitionsExec
+    // must coalesce them. We join on (id, arrive_time) = (item_id, time) 
using SMJ.
+    //
+    // With config enabled:  SortedMergeCoalescedRDD performs a k-way merge 
preserving the full
+    //   [id ASC, arrive_time ASC] ordering -> EnsureRequirements is satisfied 
-> no SortExec added.
+    // With config disabled: simple CoalescedRDD concatenates the splits and 
only the key-derived
+    //   [id ASC] ordering survives -> arrive_time ordering is lost -> 
SortExec is added.
+    // With the GroupPartitionsExec child offloaded, Gluten always falls back 
to the latter.
+    val itemOrdering = Array(
+      sort(FieldReference("id"), SortDirection.ASCENDING, 
NullOrdering.NULLS_FIRST),
+      sort(FieldReference("arrive_time"), SortDirection.ASCENDING, 
NullOrdering.NULLS_FIRST)
+    )
+    createTable(items, itemsColumns, Array(identity("id")), itemOrdering)
+    // Rows inserted out of order: id values are interleaved and arrive_time 
is not monotone
+    // within each id group, so ordering by [id ASC, arrive_time ASC] is 
non-trivial.
+    sql(
+      s"INSERT INTO testcat.ns.$items VALUES " +
+        "(2, 'cc', 30.0, cast('2023-06-15' as timestamp)), " +
+        "(1, 'bb', 20.0, cast('2022-03-10' as timestamp)), " +
+        "(3, 'dd', 40.0, cast('2024-01-01' as timestamp)), " +
+        "(1, 'aa', 10.0, cast('2021-05-20' as timestamp)), " +
+        "(2, 'ee', 50.0, cast('2025-09-01' as timestamp))")
+
+    val purchaseOrdering = Array(
+      sort(FieldReference("item_id"), SortDirection.ASCENDING, 
NullOrdering.NULLS_FIRST),
+      sort(FieldReference("time"), SortDirection.ASCENDING, 
NullOrdering.NULLS_FIRST)
+    )
+    createTable(purchases, purchasesColumns, Array(identity("item_id")), 
purchaseOrdering)
+    // Also inserted out of order
+    sql(
+      s"INSERT INTO testcat.ns.$purchases VALUES " +
+        "(2, 50.0, cast('2025-09-01' as timestamp)), " +
+        "(1, 10.0, cast('2021-05-20' as timestamp)), " +
+        "(3, 40.0, cast('2024-01-01' as timestamp)), " +
+        "(2, 30.0, cast('2023-06-15' as timestamp)), " +
+        "(1, 20.0, cast('2022-03-10' as timestamp))")
+
+    for (offloadChild <- Seq(false, true); preserveOrdering <- Seq(true, 
false)) {
+      withSQLConf(
+        (groupPartitionsChildConfs(offloadChild) ++ Seq(
+          SQLConf.REQUIRE_ALL_CLUSTER_KEYS_FOR_CO_PARTITION.key -> "false",
+          SQLConf.V2_BUCKETING_ALLOW_JOIN_KEYS_SUBSET_OF_PARTITION_KEYS.key -> 
"true",
+          SQLConf.V2_BUCKETING_PRESERVE_ORDERING_ON_COALESCE_ENABLED.key ->
+            preserveOrdering.toString
+        )): _*) {
+        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 AND 
p.time = i.arrive_time
+                        |""".stripMargin)
+        checkAnswer(df, Seq(Row(1, "aa"), Row(1, "bb"), Row(2, "cc"), Row(2, 
"ee"), Row(3, "dd")))
+
+        val plan = df.queryExecution.executedPlan
+        assert(
+          collect(plan) { case s: ShuffleExchangeLike => s }.isEmpty,
+          "should not contain any shuffle")
+
+        val groupPartitions = collectAllGroupPartitions(plan)
+        assert(groupPartitions.nonEmpty, "should contain GroupPartitionsExec 
for coalescing")
+        assert(
+          groupPartitions.exists(_.groupedPartitions.exists(_._2.size > 1)),
+          "expected coalescing GroupPartitionsExec")
+
+        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
+                })
+            if (preserveOrdering && !offloadChild) {
+              assert(
+                sorts.isEmpty,
+                "config enabled: SortedMergeCoalescedRDD preserves [id ASC, 
arrive_time ASC], " +
+                  "no sort should be added before SMJ")
+
+              // Also verify the k-way merge RDD is actually used
+              val coalescingGP = 
groupPartitions.filter(_.groupedPartitions.exists(_._2.size > 1))
+              coalescingGP.foreach {
+                gp =>
+                  assert(
+                    gp.execute().isInstanceOf[SortedMergeCoalescedRDD[_]],
+                    "config enabled: should use SortedMergeCoalescedRDD")
+              }
+            } else {
+              assert(
+                sorts.nonEmpty,
+                "config disabled or child offloaded: simple coalescing loses 
arrive_time " +
+                  "ordering, a sort should be added before SMJ")
+            }
+        }
+      }
+    }
+  }
+
+  testGluten("SPARK-55715: preserve outputOrdering when coalescing 
transform-partitioned splits") {
+    // Both tables are partitioned by years("arrive_time") / years("time") and 
report ordering
+    // [arrive_time ASC] / [time ASC]. Two rows share the same year bucket 
(2022 and 2023), so
+    // GroupPartitionsExec coalesces two splits per year. We join solely on
+    // p.time = i.arrive_time (the partition key expression) using SMJ.
+    //
+    // With config enabled:  SortedMergeCoalescedRDD k-way merge preserves 
[arrive_time ASC]
+    //   ordering -> EnsureRequirements is satisfied -> no SortExec added.
+    // With config disabled: simple CoalescedRDD only preserves the 
key-derived year ordering ->
+    //   arrive_time ordering within a year is lost -> SortExec is added.
+    // With the GroupPartitionsExec child offloaded, Gluten always falls back 
to the latter.
+    val itemOrdering = Array(
+      sort(FieldReference("arrive_time"), SortDirection.ASCENDING, 
NullOrdering.NULLS_FIRST))
+    createTable(items, itemsColumns, Array(years("arrive_time")), itemOrdering)
+    // Inserted out of order: within year 2022, September is before March in 
insertion order
+    sql(
+      s"INSERT INTO testcat.ns.$items VALUES " +
+        "(2, 'bb', 20.0, cast('2022-09-20' as timestamp)), " +
+        "(4, 'dd', 40.0, cast('2023-11-05' as timestamp)), " +
+        "(1, 'aa', 10.0, cast('2022-03-15' as timestamp)), " +
+        "(3, 'cc', 30.0, cast('2023-01-10' as timestamp))")
+
+    val purchaseOrdering = Array(
+      sort(FieldReference("time"), SortDirection.ASCENDING, 
NullOrdering.NULLS_FIRST))
+    createTable(purchases, purchasesColumns, Array(years("time")), 
purchaseOrdering)
+    // Also inserted out of order
+    sql(
+      s"INSERT INTO testcat.ns.$purchases VALUES " +
+        "(2, 20.0, cast('2022-09-20' as timestamp)), " +
+        "(4, 40.0, cast('2023-11-05' as timestamp)), " +
+        "(1, 10.0, cast('2022-03-15' as timestamp)), " +
+        "(3, 30.0, cast('2023-01-10' as timestamp))")
+
+    for (offloadChild <- Seq(false, true); preserveOrdering <- Seq(true, 
false)) {
+      withSQLConf(
+        (groupPartitionsChildConfs(offloadChild) :+
+          (SQLConf.V2_BUCKETING_PRESERVE_ORDERING_ON_COALESCE_ENABLED.key ->
+            preserveOrdering.toString)): _*) {
+        val df = sql(s"""
+                        |${selectWithMergeJoinHint("i", "p")}
+                        |i.id, i.name
+                        |FROM testcat.ns.$items i
+                        |JOIN testcat.ns.$purchases p ON p.time = i.arrive_time
+                        |""".stripMargin)
+        checkAnswer(df, Seq(Row(1, "aa"), Row(2, "bb"), Row(3, "cc"), Row(4, 
"dd")))
+
+        val plan = df.queryExecution.executedPlan
+        assert(
+          collect(plan) { case s: ShuffleExchangeLike => s }.isEmpty,
+          "should not contain any shuffle")
+
+        val groupPartitions = collectAllGroupPartitions(plan)
+        assert(groupPartitions.nonEmpty, "should contain GroupPartitionsExec 
for coalescing")
+        assert(
+          groupPartitions.exists(_.groupedPartitions.exists(_._2.size > 1)),
+          "expected coalescing GroupPartitionsExec")
+
+        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
+                })
+            if (preserveOrdering && !offloadChild) {
+              assert(
+                sorts.isEmpty,
+                "config enabled: SortedMergeCoalescedRDD preserves 
[arrive_time ASC], " +
+                  "no sort should be added before SMJ")
+
+              val coalescingGP = 
groupPartitions.filter(_.groupedPartitions.exists(_._2.size > 1))
+              coalescingGP.foreach {
+                gp =>
+                  assert(
+                    gp.execute().isInstanceOf[SortedMergeCoalescedRDD[_]],
+                    "config enabled: should use SortedMergeCoalescedRDD")
+              }
+            } else {
+              assert(
+                sorts.nonEmpty,
+                "config disabled or child offloaded: simple coalescing loses 
arrive_time " +
+                  "ordering within a year, a sort should be added before SMJ"
+              )
+            }
+        }
+      }
+    }
+  }
+
+  testGluten("SPARK-56549: k-way merge enabled only when parent requires 
ordering") {
+    // Both tables are partitioned by id/item_id and report a two-column 
ordering.
+    // Key 1 appears on two splits on each side, so GroupPartitionsExec must 
coalesce.
+    //
+    // Dynamic gate: with the config enabled, k-way merge must be activated 
only when the parent
+    // actually requires ordering (SMJ), and must stay off when the parent 
does not (hash join).
+    val itemOrdering = Array(
+      sort(FieldReference("id"), SortDirection.ASCENDING, 
NullOrdering.NULLS_FIRST),
+      sort(FieldReference("arrive_time"), SortDirection.ASCENDING, 
NullOrdering.NULLS_FIRST)
+    )
+    createTable(items, itemsColumns, Array(identity("id")), itemOrdering)
+    sql(
+      s"INSERT INTO testcat.ns.$items VALUES " +
+        "(2, 'cc', 30.0, cast('2023-06-15' as timestamp)), " +
+        "(1, 'bb', 20.0, cast('2022-03-10' as timestamp)), " +
+        "(3, 'dd', 40.0, cast('2024-01-01' as timestamp)), " +
+        "(1, 'aa', 10.0, cast('2021-05-20' as timestamp)), " +
+        "(2, 'ee', 50.0, cast('2025-09-01' as timestamp))")
+
+    val purchaseOrdering = Array(
+      sort(FieldReference("item_id"), SortDirection.ASCENDING, 
NullOrdering.NULLS_FIRST),
+      sort(FieldReference("time"), SortDirection.ASCENDING, 
NullOrdering.NULLS_FIRST)
+    )
+    createTable(purchases, purchasesColumns, Array(identity("item_id")), 
purchaseOrdering)
+    sql(
+      s"INSERT INTO testcat.ns.$purchases VALUES " +
+        "(2, 50.0, cast('2025-09-01' as timestamp)), " +
+        "(1, 10.0, cast('2021-05-20' as timestamp)), " +
+        "(3, 40.0, cast('2024-01-01' as timestamp)), " +
+        "(2, 30.0, cast('2023-06-15' as timestamp)), " +
+        "(1, 20.0, cast('2022-03-10' as timestamp))")
+
+    withSQLConf(
+      (groupPartitionsChildConfs(offload = false) ++ Seq(
+        SQLConf.REQUIRE_ALL_CLUSTER_KEYS_FOR_CO_PARTITION.key -> "false",
+        SQLConf.V2_BUCKETING_PRESERVE_ORDERING_ON_COALESCE_ENABLED.key -> 
"true")): _*) {
+      val hashDf =
+        sql(s"""
+               |SELECT /*+ SHUFFLE_HASH(i, p) */ i.id, i.name
+               |FROM testcat.ns.$items i
+               |JOIN testcat.ns.$purchases p ON p.item_id = i.id AND p.time = 
i.arrive_time
+               |""".stripMargin)
+      checkAnswer(hashDf, Seq(Row(1, "aa"), Row(1, "bb"), Row(2, "cc"), Row(2, 
"ee"), Row(3, "dd")))
+      val hashPlan = hashDf.queryExecution.executedPlan
+      assert(
+        collect(hashPlan) { case j: ShuffledHashJoinExecTransformerBase => j 
}.nonEmpty,
+        "expected ShuffledHashJoinExecTransformer")
+      assert(
+        collect(hashPlan) { case s: ShuffleExchangeLike => s }.isEmpty,
+        "should not shuffle for compatible partitioning")
+      val hashCoalescing =
+        
collectAllGroupPartitions(hashPlan).filter(_.groupedPartitions.exists(_._2.size 
> 1))
+      assert(hashCoalescing.nonEmpty, "expected coalescing 
GroupPartitionsExec")
+      hashCoalescing.foreach {
+        gp =>
+          assert(
+            !gp.enableSortedMerge,
+            "hash join does not require ordering: enableSortedMerge must stay 
false")
+          assert(
+            !gp.execute().isInstanceOf[SortedMergeCoalescedRDD[_]],
+            "hash join does not require ordering: must use simple 
CoalescedRDD")
+      }
+
+      val smjDf =
+        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 AND p.time = 
i.arrive_time
+               |""".stripMargin)
+      checkAnswer(smjDf, Seq(Row(1, "aa"), Row(1, "bb"), Row(2, "cc"), Row(2, 
"ee"), Row(3, "dd")))
+      val smjPlan = smjDf.queryExecution.executedPlan
+      assert(
+        collect(smjPlan) { case j: SortMergeJoinExecTransformer => j 
}.nonEmpty,
+        "expected SortMergeJoinExecTransformer")
+      assert(
+        collect(smjPlan) { case s: ShuffleExchangeLike => s }.isEmpty,
+        "should not shuffle for compatible partitioning")
+      val smjCoalescing =
+        
collectAllGroupPartitions(smjPlan).filter(_.groupedPartitions.exists(_._2.size 
> 1))
+      assert(smjCoalescing.nonEmpty, "expected coalescing GroupPartitionsExec")
+      smjCoalescing.foreach {
+        gp =>
+          assert(
+            gp.enableSortedMerge,
+            "sort-merge join requires ordering: enableSortedMerge must be 
true")
+          assert(
+            gp.execute().isInstanceOf[SortedMergeCoalescedRDD[_]],
+            "sort-merge join requires ordering: must use 
SortedMergeCoalescedRDD")
+      }
+    }
+  }
 }


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

Reply via email to