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

peter-toth pushed a commit to branch branch-4.0
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.0 by this push:
     new c0633e31d625 [SPARK-53322][4.0][SQL] Select a KeyGroupedShuffleSpec 
only when join key positions can be fully pushed down
c0633e31d625 is described below

commit c0633e31d62599efc70732355f40777cbf96bc3e
Author: Peter Toth <[email protected]>
AuthorDate: Mon Oct 5 17:12:44 2026 +0200

    [SPARK-53322][4.0][SQL] Select a KeyGroupedShuffleSpec only when join key 
positions can be fully pushed down
    
    ### Differences from #53098
    
    This is the branch-4.0 backport of #53098. It reached master and branch-4.1 
(#53132), but not branch-4.0. The description below is the original one. On 
this branch:
    - `KeyGroupedPartitionedScan` does not exist here. 
`canPushDownSPJParamsToScan` asks for a `BatchScanExec` instead, the node 
`populateCommonPartitionInfo` pushes the SPJ parameters into. On branch-4.1, 
`BatchScanExec` is the only implementer of that trait.
    - The tests are the branch-4.1 ones. They conflict only at the end of 
`KeyGroupedPartitioningSuite` and `EnsureRequirementsSuite`. 
`DummySparkPlanWithBatchScanChild` is already on this branch, copied in by the 
SPARK-59080 backport, so its comment that pointed at this ticket is dropped.
    - Three SPARK-59050 tests now match branch-4.1:
      - Without this change, this branch storage-partitions a keyed side above 
a one-side-shuffled join of a parquet table. When the partition counts differ, 
the query fails with `Can't zip RDDs with unequal numbers of partitions` or 
with the `PartitioningCollection` error below. So the SPARK-59050 backport 
(#58572) gave two of these tests equal key sets, and kept the third at 1 
shuffle.
      - With this change, the first join's output reads a parquet table, so the 
second join shuffles one side instead, as on branch-4.1. The rows are the same.
      - Measured without this change: two of them fail with those two errors, 
and the third gets 1 shuffle instead of 2.
    - As on branch-4.1, every leaf under a join side must now be a 
`BatchScanExec`. So some joins that storage-partitioned here before now shuffle:
      - a keyed side that also reads another kind of source, for example 
through a broadcast join with a parquet build side. Measured with every conf at 
its default, such a three-table join goes from 0 to 2 shuffles;
      - a cached or checkpointed keyed side, even when its partition values 
match the other side's.
      - Master and branch-4.2 dropped this check later, with the SPARK-55535 
refactor.
    - The bug reproduces on branch-4.0 without this change, with 
`spark.sql.sources.v2.bucketing.enabled` on:
      - both sides checkpointed: the join returns 1 of 2 rows;
      - one side checkpointed: planning fails with `PartitioningCollection 
requires all of its partitionings have the same numPartitions`.
    - New tests: three of the four fail on branch-4.0 without this change. 
"checkpointed scans avoid shuffles for aggregates" passes, since it needs no 
storage-partitioned join. "checkpointed scans can't shuffle other children on 
SPJ" fails on the shuffle count only, 2 instead of 3.
    - Each half was removed on its own. Without the `candidateSpecs` filter, 
"checkpointed scans can't shuffle other children on SPJ" fails. Without the 
check in `checkKeyGroupCompatible`, "checkpointed scans aren't used for SPJ" 
and "checkpointed scans can be shuffled by children on SPJ" fail.
    - Ran the `KeyGroupedPartitioning*` suites, `EnsureRequirementsSuite`, 
`PlannerSuite`, `WriteDistributionAndOrderingSuite`, 
`BucketedReadWithoutHiveSupportSuite`, the plan stability suites, the 
`*JoinSuite` suites, `AdaptiveQueryExecSuite`, `DataSourceV2Suite` and 
`CheckpointSuite` on branch-4.0, 1313 tests in all, plus `dev/lint-scala`.
    - The backport was prepared with Claude Code. Generated-by: Claude Code 
(Claude Opus 5.5)
    
    ### What changes were proposed in this pull request?
    When a KeyGroupedShuffleSpec is used to shuffle another child of a JOIN, we 
must be able to push down JOIN keys or partition values to be able to ensure 
that both children have matching partitioning. If one child reports a 
KeyGroupedPartitioning but we can't push down these values (for example, if the 
child was a key-grouped scan that was checkpointed), then this information 
cannot be pushed down to the child scan and we should avoid using this shuffle 
spec to shuffle other children.
    
    ### Why are the changes needed?
    Prevents potential correctness issue when key-grouped partitioning is used 
on a checkpointed RDD.
    
    ### Does this PR introduce _any_ user-facing change?
    No.
    
    ### How was this patch tested?
    See test changes.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    No.
    
    Closes #59234 from peter-toth/SPARK-53322-checkpoint-pushdown-4.0.
    
    Lead-authored-by: Peter Toth <[email protected]>
    Co-authored-by: Chirag Singh <[email protected]>
    Signed-off-by: Peter Toth <[email protected]>
---
 .../execution/exchange/EnsureRequirements.scala    |  31 ++++
 .../connector/KeyGroupedPartitioningSuite.scala    | 193 ++++++++++++++++++---
 .../exchange/EnsureRequirementsSuite.scala         |  68 ++++----
 3 files changed, 232 insertions(+), 60 deletions(-)

diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/EnsureRequirements.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/EnsureRequirements.scala
index e581b78c066d..289f6438d374 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/EnsureRequirements.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/EnsureRequirements.scala
@@ -140,6 +140,13 @@ case class EnsureRequirements(
       // Choose all the specs that can be used to shuffle other children
       val candidateSpecs = specs
           .filter(_._2.canCreatePartitioning)
+          .filter {
+            // To choose a KeyGroupedShuffleSpec, we must be able to push down 
SPJ parameters into
+            // the scan (for join key positions). If these parameters can't be 
pushed down, this
+            // spec can't be used to shuffle other children.
+            case (idx, _: KeyGroupedShuffleSpec) => 
canPushDownSPJParamsToScan(children(idx))
+            case _ => true
+          }
           .filter(p => !shouldConsiderMinParallelism ||
               children(p._1).outputPartitioning.numPartitions >= 
conf.defaultNumShufflePartitions)
       val bestSpecOpt = if (candidateSpecs.isEmpty) {
@@ -425,6 +432,24 @@ case class EnsureRequirements(
     }
   }
 
+  /**
+   * Whether SPJ params can be pushed down to the leaf nodes of a physical 
plan. For a plan to be
+   * eligible for SPJ parameter pushdown, all leaf nodes must be a 
KeyGroupedPartitioning-aware
+   * scan, i.e. a [[BatchScanExec]], the node `populateCommonPartitionInfo` 
pushes them into.
+   *
+   * Notably, if the leaf of `plan` is an [[RDDScanExec]] created by 
checkpointing a DSv2 scan, the
+   * reported partitioning will be a [[KeyGroupedPartitioning]], but this plan 
will _not_ be
+   * eligible for SPJ parameter pushdown (as the partitioning is static and 
can't be easily
+   * re-grouped or padded with empty partitions according to the partition 
values on the other side
+   * of the join).
+   */
+  private def canPushDownSPJParamsToScan(plan: SparkPlan): Boolean = {
+    plan.collectLeaves().forall {
+      case _: BatchScanExec => true
+      case _ => false
+    }
+  }
+
   /**
    * Checks whether two children, `left` and `right`, of a join operator have 
compatible
    * `KeyGroupedPartitioning`, and can benefit from storage-partitioned join.
@@ -436,6 +461,12 @@ case class EnsureRequirements(
       left: SparkPlan,
       right: SparkPlan,
       requiredChildDistribution: Seq[Distribution]): Option[Seq[SparkPlan]] = {
+    // If SPJ params can't be pushed down to either the left or right side, 
it's unsafe to do an
+    // SPJ.
+    if (!canPushDownSPJParamsToScan(left) || 
!canPushDownSPJParamsToScan(right)) {
+      return None
+    }
+
     parent match {
       case smj: SortMergeJoinExec =>
         checkKeyGroupCompatible(left, right, smj.joinType, 
requiredChildDistribution)
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala
index dafb04923058..c76056fb8cc9 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala
@@ -35,6 +35,7 @@ import 
org.apache.spark.sql.execution.datasources.v2.BatchScanExec
 import org.apache.spark.sql.execution.datasources.v2.DataSourceV2ScanRelation
 import org.apache.spark.sql.execution.exchange.ShuffleExchangeExec
 import org.apache.spark.sql.execution.joins.SortMergeJoinExec
+import org.apache.spark.sql.functions.{col, max}
 import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.internal.SQLConf._
 import org.apache.spark.sql.types._
@@ -3625,16 +3626,15 @@ class KeyGroupedPartitioningSuite extends 
DistributionAndOrderingSuiteBase {
 
   test("SPARK-59050: SPJ: keyed preserved side of outer join still uses the 
one-side shuffle") {
     // a (keyed) preserved on the left, t (non-keyed) nullable on the right: t 
is shuffled onto
-    // a's keys (its partitioning is marked as having unknown keys), but the 
LEFT OUTER join exposes
-    // only a's accurate partitioning, so the one-side shuffle stays sound and 
the downstream SPJ
-    // still runs (no shuffle for the second join). u declares a's key set 
rather than a superset:
-    // this branch's value-merging path cannot reconcile differing partition 
counts over a join
-    // subtree (a pre-existing limitation independent of the marker), and the 
equal-count shape
-    // carries the property this test is about.
+    // a's keys (its partitioning is marked as having unknown keys), but the 
LEFT OUTER join
+    // exposes only a's accurate partitioning, so the one-side shuffle stays 
sound. The join
+    // output also reads the parquet table t, so SPJ parameters cannot be 
pushed into it
+    // (SPARK-53322), and the second join shuffles one side instead. Both 
exchanges carry the
+    // marker and the answer is complete.
     createTable("a", columns, Array(identity("id")))
     createTable("u", columns, Array(identity("id")))
     sql("INSERT INTO testcat.ns.a VALUES (1, 'a1', NULL), (2, 'a2', NULL)")
-    sql("INSERT INTO testcat.ns.u VALUES (1, 'u1', NULL), (2, 'u2', NULL)")
+    sql("INSERT INTO testcat.ns.u VALUES (1, 'u1', NULL), (2, 'u2', NULL), (3, 
'u3', NULL)")
 
     withTable("t") {
       sql("CREATE TABLE t (id INT, data STRING) USING parquet")
@@ -3652,7 +3652,7 @@ class KeyGroupedPartitioningSuite extends 
DistributionAndOrderingSuiteBase {
         val df = sql(query)
         checkAnswer(df, Seq(Row(1, "u1"), Row(2, "u2")))
         
assertShuffleMayContainUnknownPartitionKeys(df.queryExecution.executedPlan,
-          Seq(true))
+          Seq(true, true))
       }
     }
   }
@@ -3698,9 +3698,10 @@ class KeyGroupedPartitioningSuite extends 
DistributionAndOrderingSuiteBase {
   }
 
   test("SPARK-59050: SPJ: unknown-keyed partitioning still joins a 
subset-keyed partner") {
-    // r (from a RIGHT OUTER JOIN t) has unknown partition keys {1, 2}, but 
the downstream u is
-    // keyed on a subset {1}, so the storage-partitioned join stays compatible 
and works: every
-    // key u can have is co-located on r's declared layout.
+    // r (from a RIGHT OUTER JOIN t) has unknown partition keys {1, 2}, and 
the downstream u is
+    // keyed on a subset {1}, so the marker's subset rule would accept the 
pair. But r also reads
+    // the parquet table t, so SPARK-53322 refuses to push SPJ parameters into 
it. The second join
+    // re-shuffles r onto u's layout instead, which is sound for any keys r 
holds.
     createTable("a", columns, Array(identity("id")))
     createTable("u", columns, Array(identity("id")))
     sql("INSERT INTO testcat.ns.a VALUES (1, 'a1', NULL), (2, 'a2', NULL)")
@@ -3721,9 +3722,8 @@ class KeyGroupedPartitioningSuite extends 
DistributionAndOrderingSuiteBase {
           SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false") {
         val df = sql(query)
         checkAnswer(df, Seq(Row(1, "u1")))
-        // Only the first join's one-side shuffle remains; the second join 
storage-partitions.
         
assertShuffleMayContainUnknownPartitionKeys(df.queryExecution.executedPlan,
-          Seq(true))
+          Seq(true, true))
       }
     }
   }
@@ -3828,16 +3828,15 @@ class KeyGroupedPartitioningSuite extends 
DistributionAndOrderingSuiteBase {
 
   test("SPARK-59050: SPJ: spurious marker of an inner join costs no extra 
shuffle") {
     // The spurious marker on the inner join's collection must cost nothing: 
consumers read a
-    // joined collection with `exists`, so the accurate member still answers, 
and the second
-    // storage-partitioned join runs without a re-shuffle. u declares a's 
bucket values rather
-    // than a finer bucket count: this branch's value-merging path cannot 
reconcile differing
-    // partition counts over a join subtree (a pre-existing limitation 
independent of the
-    // marker), and the equal-count shape carries the property this test is 
about.
+    // joined collection with `exists`, so the accurate member still answers. 
The join output
+    // also reads the parquet table t, so SPJ parameters cannot be pushed into 
it (SPARK-53322),
+    // and the second join shuffles one side instead, with and without the 
marker: two exchanges
+    // either way.
     val cols = Array(Column.create("id", LongType), Column.create("data", 
StringType))
     createTable("a", cols, Array(bucket(4, "id")))
-    createTable("u", cols, Array(bucket(4, "id")))
-    sql("INSERT INTO testcat.ns.a VALUES (0, 'a0'), (1, 'a1'), (2, 'a2')")
-    sql("INSERT INTO testcat.ns.u VALUES (0, 'u0'), (1, 'u1'), (2, 'u2')")
+    createTable("u", cols, Array(bucket(2, "id")))
+    sql("INSERT INTO testcat.ns.a VALUES (0, 'a0'), (1, 'a1'), (2, 'a2'), (3, 
'a3')")
+    sql("INSERT INTO testcat.ns.u VALUES (0, 'u0'), (1, 'u1'), (2, 'u2'), (3, 
'u3')")
 
     withTable("t") {
       sql("CREATE TABLE t (id BIGINT, data STRING) USING parquet")
@@ -3849,7 +3848,7 @@ class KeyGroupedPartitioningSuite extends 
DistributionAndOrderingSuiteBase {
           |FROM testcat.ns.a a JOIN t ON a.id = t.id
           |JOIN testcat.ns.u u ON a.id = u.id
           |""".stripMargin
-      val expected = Seq(Row(0L, "u0"), Row(1L, "u1"), Row(2L, "u2"))
+      val expected = Seq(Row(0L, "u0"), Row(1L, "u1"), Row(2L, "u2"), Row(3L, 
"u3"))
 
       withSQLConf(
           SQLConf.V2_BUCKETING_SHUFFLE_ENABLED.key -> "true",
@@ -3857,9 +3856,10 @@ class KeyGroupedPartitioningSuite extends 
DistributionAndOrderingSuiteBase {
           SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false") {
         val df = sql(query)
         checkAnswer(df, expected)
-        // The spurious marker adds no exchange: the first join's one-side 
shuffle is the only
-        // shuffle and the second join storage-partitions on the accurate 
member.
-        
assertShuffleMayContainUnknownPartitionKeys(df.queryExecution.executedPlan, 
Seq(true))
+        // The first join's one-side shuffle plus the second join's 
re-shuffle; the spurious
+        // marker adds no third exchange.
+        
assertShuffleMayContainUnknownPartitionKeys(df.queryExecution.executedPlan,
+          Seq(true, true))
       }
     }
   }
@@ -4036,4 +4036,147 @@ class KeyGroupedPartitioningSuite extends 
DistributionAndOrderingSuiteBase {
     }
   }
 
+  test("SPARK-53322: checkpointed scans avoid shuffles for aggregates") {
+    withTempDir { dir =>
+      spark.sparkContext.setCheckpointDir(dir.getPath)
+      val itemsPartitions = Array(identity("id"))
+      createTable(items, itemsColumns, itemsPartitions)
+      sql(s"INSERT INTO testcat.ns.$items VALUES " +
+        s"(1, 'aa', 40.0, cast('2020-01-01' as timestamp)), " +
+        s"(1, 'aa', 41.0, cast('2020-01-02' as timestamp)), " +
+        s"(2, 'bb', 10.0, cast('2020-01-01' as timestamp)), " +
+        s"(3, 'cc', 15.5, cast('2020-02-01' as timestamp))")
+
+      val scanDF = spark.read.table(s"testcat.ns.$items").checkpoint()
+      val df = scanDF.groupBy("id").agg(max("price").as("res")).select("res")
+      checkAnswer(df.sort("res"), Seq(Row(10.0), Row(15.5), Row(41.0)))
+
+      val shuffles = collectAllShuffles(df.queryExecution.executedPlan)
+      assert(shuffles.isEmpty,
+        "should not contain shuffle when not grouping by partition values")
+    }
+  }
+
+  test("SPARK-53322: checkpointed scans aren't used for SPJ") {
+    withTempDir { dir =>
+      spark.sparkContext.setCheckpointDir(dir.getPath)
+      val itemsPartitions = Array(identity("id"))
+      createTable(items, itemsColumns, itemsPartitions)
+      sql(s"INSERT INTO testcat.ns.$items VALUES " +
+        s"(1, 'aa', 41.0, cast('2020-01-01' as timestamp)), " +
+        s"(2, 'bb', 10.0, cast('2020-01-02' as timestamp)), " +
+        s"(3, 'cc', 15.5, cast('2020-01-03' as timestamp))")
+
+      val purchase_partitions = Array(identity("item_id"))
+      createTable(purchases, purchasesColumns, purchase_partitions)
+      sql(s"INSERT INTO testcat.ns.$purchases VALUES " +
+        s"(1, 40.0, cast('2020-01-01' as timestamp)), " +
+        s"(3, 25.5, cast('2020-01-03' as timestamp)), " +
+        s"(4, 20.0, cast('2020-01-04' as timestamp))")
+
+      for {
+        pushdownValues <- Seq(true, false)
+        checkpointBothScans <- Seq(true, false)
+      } {
+        withSQLConf(
+            SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1",
+            SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> 
pushdownValues.toString) {
+          val scanDF1 = 
spark.read.table(s"testcat.ns.$items").checkpoint().as("i")
+          val scanDF2 = if (checkpointBothScans) {
+            spark.read.table(s"testcat.ns.$purchases").checkpoint().as("p")
+          } else {
+            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))
+          )
+          // 1 shuffle for SORT and 2 shuffles for JOIN are expected.
+          assert(collectAllShuffles(df.queryExecution.executedPlan).length === 
3)
+        }
+      }
+    }
+  }
+
+  test("SPARK-53322: checkpointed scans can't shuffle other children on SPJ") {
+    withTempDir { dir =>
+      spark.sparkContext.setCheckpointDir(dir.getPath)
+      val itemsPartitions = Array(identity("id"))
+      createTable(items, itemsColumns, itemsPartitions)
+      sql(s"INSERT INTO testcat.ns.$items VALUES " +
+        s"(1, 'aa', 41.0, cast('2020-01-01' as timestamp)), " +
+        s"(2, 'bb', 10.0, cast('2020-01-02' as timestamp)), " +
+        s"(3, 'cc', 15.5, cast('2020-01-03' as timestamp))")
+
+      createTable(purchases, purchasesColumns, Array.empty)
+      sql(s"INSERT INTO testcat.ns.$purchases VALUES " +
+        s"(1, 40.0, cast('2020-01-01' as timestamp)), " +
+        s"(3, 25.5, cast('2020-01-03' as timestamp)), " +
+        s"(4, 20.0, cast('2020-01-04' as timestamp))")
+
+      Seq(true, false).foreach { pushdownValues =>
+        withSQLConf(
+            SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1",
+            SQLConf.V2_BUCKETING_SHUFFLE_ENABLED.key -> "true",
+            SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> 
pushdownValues.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))
+          )
+          // 1 shuffle for SORT and 2 shuffles for JOIN are expected.
+          assert(collectAllShuffles(df.queryExecution.executedPlan).length === 
3)
+        }
+      }
+    }
+  }
+
+  test("SPARK-53322: checkpointed scans can be shuffled by children on SPJ") {
+    withTempDir { dir =>
+      spark.sparkContext.setCheckpointDir(dir.getPath)
+      val itemsPartitions = Array(identity("id"))
+      createTable(items, itemsColumns, itemsPartitions)
+      sql(s"INSERT INTO testcat.ns.$items VALUES " +
+        s"(1, 'aa', 41.0, cast('2020-01-01' as timestamp)), " +
+        s"(2, 'bb', 10.0, cast('2020-01-02' as timestamp)), " +
+        s"(3, 'cc', 15.5, cast('2020-01-03' as timestamp))")
+
+      createTable(purchases, purchasesColumns, Array(identity("item_id")))
+      sql(s"INSERT INTO testcat.ns.$purchases VALUES " +
+        s"(1, 40.0, cast('2020-01-01' as timestamp)), " +
+        s"(3, 25.5, cast('2020-01-03' as timestamp)), " +
+        s"(4, 20.0, cast('2020-01-04' as timestamp))")
+
+      withSQLConf(
+          SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1",
+          SQLConf.V2_BUCKETING_SHUFFLE_ENABLED.key -> "true",
+          SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> "true") {
+        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 the sort and one shuffle for one side of the JOIN 
are expected.
+        assert(collectAllShuffles(df.queryExecution.executedPlan).length === 2)
+      }
+    }
+  }
 }
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/exchange/EnsureRequirementsSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/exchange/EnsureRequirementsSuite.scala
index 4bd5c08b5dee..9ecc94368b9e 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/exchange/EnsureRequirementsSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/exchange/EnsureRequirementsSuite.scala
@@ -92,11 +92,11 @@ class EnsureRequirementsSuite extends SharedSparkSession {
 
   test("reorder should handle KeyGroupedPartitioning") {
     // partitioning on the left
-    val plan1 = DummySparkPlan(
+    val plan1 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(Seq(
         years(exprA), bucket(4, exprB), days(exprC)), 4)
     )
-    val plan2 = DummySparkPlan(
+    val plan2 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(Seq(
         years(exprB), bucket(4, exprA), days(exprD)), 4)
     )
@@ -114,7 +114,7 @@ class EnsureRequirementsSuite extends SharedSparkSession {
     }
 
     // partitioning on the right
-    val plan3 = DummySparkPlan(
+    val plan3 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(Seq(
         bucket(4, exprD), days(exprA), years(exprC)), 4)
     )
@@ -778,9 +778,9 @@ class EnsureRequirementsSuite extends SharedSparkSession {
 
   test("Check with KeyGroupedPartitioning") {
     // simplest case: identity transforms
-    var plan1 = DummySparkPlan(
-      outputPartitioning = KeyGroupedPartitioning(exprA :: exprB :: Nil, 5))
-    var plan2 = DummySparkPlan(
+    var plan1 = new DummySparkPlanWithBatchScanChild(
+      KeyGroupedPartitioning(exprA :: exprB :: Nil, 5))
+    var plan2 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(exprA :: exprC :: Nil, 5))
     var smjExec = SortMergeJoinExec(
       exprA :: exprB :: Nil, exprA :: exprC :: Nil, Inner, None, plan1, plan2)
@@ -794,11 +794,11 @@ class EnsureRequirementsSuite extends SharedSparkSession {
     }
 
     // matching bucket transforms from both sides
-    plan1 = DummySparkPlan(
+    plan1 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(
         bucket(4, exprA) :: bucket(16, exprB) :: Nil, 4)
     )
-    plan2 = DummySparkPlan(
+    plan2 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(
         bucket(4, exprA) :: bucket(16, exprC) :: Nil, 4)
     )
@@ -814,11 +814,11 @@ class EnsureRequirementsSuite extends SharedSparkSession {
     }
 
     // partition collections
-    plan1 = DummySparkPlan(
+    plan1 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(
         bucket(4, exprA) :: bucket(16, exprB) :: Nil, 4)
     )
-    plan2 = DummySparkPlan(
+    plan2 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = PartitioningCollection(Seq(
         KeyGroupedPartitioning(bucket(4, exprA) :: bucket(16, exprC) :: Nil, 
4),
         KeyGroupedPartitioning(bucket(4, exprA) :: bucket(16, exprC) :: Nil, 
4))
@@ -844,10 +844,10 @@ class EnsureRequirementsSuite extends SharedSparkSession {
     }
 
     // bucket + years transforms from both sides
-    plan1 = DummySparkPlan(
+    plan1 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(bucket(4, exprA) :: 
years(exprB) :: Nil, 4)
     )
-    plan2 = DummySparkPlan(
+    plan2 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(bucket(4, exprA) :: 
years(exprC) :: Nil, 4)
     )
     smjExec = SortMergeJoinExec(
@@ -863,11 +863,11 @@ class EnsureRequirementsSuite extends SharedSparkSession {
 
     // by default spark.sql.requireAllClusterKeysForCoPartition is true, so 
when there isn't
     // exact match on all partition keys, Spark will fallback to shuffle.
-    plan1 = DummySparkPlan(
+    plan1 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(
         bucket(4, exprA) :: bucket(4, exprB) :: Nil, 4)
     )
-    plan2 = DummySparkPlan(
+    plan2 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(
         bucket(4, exprA) :: bucket(4, exprC) :: Nil, 4)
     )
@@ -884,11 +884,11 @@ class EnsureRequirementsSuite extends SharedSparkSession {
   }
 
   test(s"KeyGroupedPartitioning with 
${REQUIRE_ALL_CLUSTER_KEYS_FOR_CO_PARTITION.key} = false") {
-    var plan1 = DummySparkPlan(
+    var plan1 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(
         bucket(4, exprB) :: years(exprC) :: Nil, 4)
     )
-    var plan2 = DummySparkPlan(
+    var plan2 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(
         bucket(4, exprC) :: years(exprB) :: Nil, 4)
     )
@@ -906,11 +906,11 @@ class EnsureRequirementsSuite extends SharedSparkSession {
     }
 
     // should also work with distributions with duplicated keys
-    plan1 = DummySparkPlan(
+    plan1 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(
         bucket(4, exprA) :: years(exprB) :: Nil, 4)
     )
-    plan2 = DummySparkPlan(
+    plan2 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(
         bucket(4, exprA) :: years(exprC) :: Nil, 4)
     )
@@ -926,10 +926,10 @@ class EnsureRequirementsSuite extends SharedSparkSession {
     }
 
     // both partitioning and distribution have duplicated keys
-    plan1 = DummySparkPlan(
+    plan1 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(
         years(exprA) :: bucket(4, exprB) :: days(exprA) :: Nil, 5))
-    plan2 = DummySparkPlan(
+    plan2 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(
         years(exprA) :: bucket(4, exprC) :: days(exprA) :: Nil, 5))
     smjExec = SortMergeJoinExec(
@@ -944,11 +944,11 @@ class EnsureRequirementsSuite extends SharedSparkSession {
     }
 
     // invalid case: partitioning key positions don't match
-    plan1 = DummySparkPlan(
+    plan1 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(
         bucket(4, exprA) :: bucket(4, exprB) :: Nil, 4)
     )
-    plan2 = DummySparkPlan(
+    plan2 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(
         bucket(4, exprB) :: bucket(4, exprC) :: Nil, 4)
     )
@@ -965,11 +965,11 @@ class EnsureRequirementsSuite extends SharedSparkSession {
     }
 
     // invalid case: different number of buckets (we don't support 
coalescing/repartitioning yet)
-    plan1 = DummySparkPlan(
+    plan1 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(
         bucket(4, exprA) :: bucket(4, exprB) :: Nil, 4)
     )
-    plan2 = DummySparkPlan(
+    plan2 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(
         bucket(4, exprA) :: bucket(8, exprC) :: Nil, 4)
     )
@@ -985,10 +985,10 @@ class EnsureRequirementsSuite extends SharedSparkSession {
     }
 
     // invalid case: partition key positions match but with different 
transforms
-    plan1 = DummySparkPlan(
+    plan1 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(years(exprA) :: bucket(4, 
exprB) :: Nil, 4)
     )
-    plan2 = DummySparkPlan(
+    plan2 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(days(exprA) :: bucket(4, 
exprC) :: Nil, 4)
     )
     smjExec = SortMergeJoinExec(
@@ -1004,11 +1004,11 @@ class EnsureRequirementsSuite extends 
SharedSparkSession {
 
 
     // invalid case: multiple references in transform
-    plan1 = DummySparkPlan(
+    plan1 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(
         years(exprA) :: buckets(4, Seq(exprB, exprC)) :: Nil, 4)
     )
-    plan2 = DummySparkPlan(
+    plan2 = new DummySparkPlanWithBatchScanChild(
       outputPartitioning = KeyGroupedPartitioning(
         years(exprA) :: buckets(4, Seq(exprB, exprC)) :: Nil, 4)
     )
@@ -1030,11 +1030,11 @@ class EnsureRequirementsSuite extends 
SharedSparkSession {
       val rightPartValues = Seq(Array[Any](1, 1), Array[Any](2, 2), 
Array[Any](3, 3))
           .map(new GenericInternalRow(_))
 
-      var plan1 = DummySparkPlan(
+      var plan1 = new DummySparkPlanWithBatchScanChild(
         outputPartitioning = KeyGroupedPartitioning(bucket(4, exprB) :: 
bucket(8, exprC) :: Nil,
           leftPartValues.length, leftPartValues)
       )
-      var plan2 = DummySparkPlan(
+      var plan2 = new DummySparkPlanWithBatchScanChild(
         outputPartitioning = KeyGroupedPartitioning(bucket(4, exprC) :: 
bucket(8, exprB) :: Nil,
           rightPartValues.length, rightPartValues)
       )
@@ -1052,7 +1052,7 @@ class EnsureRequirementsSuite extends SharedSparkSession {
       }
 
       // With partition collections
-      plan1 = DummySparkPlan(outputPartitioning =
+      plan1 = new DummySparkPlanWithBatchScanChild(outputPartitioning =
         PartitioningCollection(
           Seq(KeyGroupedPartitioning(bucket(4, exprB) :: bucket(8, exprC) :: 
Nil,
             leftPartValues.length, leftPartValues),
@@ -1076,7 +1076,7 @@ class EnsureRequirementsSuite extends SharedSparkSession {
       }
 
       // Nested partition collections
-      plan2 = DummySparkPlan(outputPartitioning =
+      plan2 = new DummySparkPlanWithBatchScanChild(outputPartitioning =
         PartitioningCollection(
           Seq(
             PartitioningCollection(
@@ -1119,7 +1119,7 @@ class EnsureRequirementsSuite extends SharedSparkSession {
       val a1 = AttributeReference("a1", IntegerType)()
 
       val partitionValue = Seq(50, 51, 52).map(v => 
InternalRow.fromSeq(Seq(v)))
-      val plan1 = DummySparkPlan(outputPartitioning = KeyGroupedPartitioning(
+      val plan1 = new DummySparkPlanWithBatchScanChild(outputPartitioning = 
KeyGroupedPartitioning(
           identity(a1) :: Nil, 4, partitionValue))
       val plan2 = DummySparkPlan(outputPartitioning = SinglePartition)
 
@@ -1205,8 +1205,6 @@ class EnsureRequirementsSuite extends SharedSparkSession {
     TransformExpression(DaysFunction, Seq(expr))
   }
 
-  // Backported from SPARK-53322 together with the test below, which needs a 
keyed plan whose
-  // subtree holds a `BatchScanExec` for the join key positions to be pushed 
into.
   private class DummySparkPlanWithBatchScanChild(outputPartitioning: 
Partitioning)
     extends DummySparkPlan(
       children = Seq(BatchScanExec(Seq.empty, null, Seq.empty, table = null)),


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

Reply via email to