This is an automated email from the ASF dual-hosted git repository.
peter-toth pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new 87c75f1c6a5a [SPARK-58783][4.1][SQL] Fix SPJ with runtime filtering
when join keys are a subset of partition keys
87c75f1c6a5a is described below
commit 87c75f1c6a5a15b8546f3149183d50450a35a8e6
Author: Peter Toth <[email protected]>
AuthorDate: Sat Aug 15 06:21:03 2026 -0700
[SPARK-58783][4.1][SQL] Fix SPJ with runtime filtering when join keys are a
subset of partition keys
### What changes were proposed in this pull request?
This PR makes `BatchScanExec.filteredPartitions` validate the partitions a
data source reports after runtime filtering against the **unprojected**
partitioning (`super.outputPartitioning`) instead of `outputPartitioning`.
`BatchScanExec.outputPartitioning` projects the partition expressions and
values down to the join keys when SPJ params were pushed down
(`spjParams.joinKeyPositions`), and it also replaces the partition values with
the common partition values of the join. But `filteredPartitions` wraps the
re-planned partitions with their full, unprojected
`HasPartitionKey.partitionKey()`, so the two sides of the comparison are in
different spaces.
Nothing else needs to change, because everything after the check already
works in the full partition key space: `groupPartitions` groups by the
unprojected keys, and the projection, the reducers, the common partition values
and the empty-partition padding all happen later, in `inputRDD`.
One further consequence worth calling out: when `commonPartitionValues` was
pushed down, the values the check compared against were the *merged* set from
both sides of the join, so it also accepted keys that only the other side had
ever reported. It now compares against this scan's own pre-filter keys, which
is what the error message claims. Every data source that only prunes during
filtering still passes, since its post-filter keys are a subset of its own
pre-filter keys.
This PR targets `branch-4.1` (and later `branch-4.0`) only. `master` and
`branch-4.2` are not affected: SPARK-55535 / SPARK-55092 moved the join key
projection out of the scan into `GroupPartitionsExec`, so there both sides of
the comparison are already unprojected. That refactor is too large for a
maintenance branch, hence this targeted fix.
### Why are the changes needed?
A storage-partitioned join and runtime filtering cannot coexist on the same
scan when the join keys are a subset of the partition keys. The query fails on
the driver, in `BatchScanExec.filteredPartitions`, before a single task runs:
```
org.apache.spark.SparkException: During runtime filtering, data source must
not report new partition values that are not present in the original
partitioning.
```
The general trigger is that `outputPartitioning`'s partition values are not
a superset of the keys the scan itself reported, so a post-filter key looks new
when it isn't. The practically reachable way to get there is a projected
partitioning:
1. SPJ with
`spark.sql.sources.v2.bucketing.allowJoinKeysSubsetOfPartitionKeys.enabled=true`
(off by default), so Spark projects the scan partitioning down to the join
keys.
2. A scan reporting `KeyGroupedPartitioning` where the join keys are **not
a leading prefix** of the partition keys. `InternalRowComparableWrapper` builds
its `StructType` and its ordering from the projected data types, and
`InterpretedOrdering.compare` walks `ordering.size` positions, so the
comparison is positional over the first k fields of the raw key. When the
projection is a leading prefix (`Seq(0)`, `Seq(0, 1)`, …) it happens to compare
the same columns and the mismatch goes un [...]
3. A runtime filter on that same scan.
`spark.sql.sources.v2.bucketing.allowCompatibleTransforms.enabled` (the
values are reduced into the smaller bucket space while the keys the scan
reports are not) and `spark.sql.sources.v2.bucketing.partition.filter.enabled`
(the values become the intersection of the two sides, or one side's for an
outer join) are two further ways to make the value set not a superset, and this
change repairs those as well. All three have tests, see below.
With `allowJoinKeysSubsetOfPartitionKeys` enabled, a copy-on-write `MERGE
INTO` hits the other two conditions by default, because the row-level operation
group filter
(`spark.sql.optimizer.runtime.rowLevelOperationGroupFilter.enabled`) is on: the
target is partitioned by e.g. `months(created), bucket(4, id)` and the merge
joins on `id`, which is the second partition key. This makes SPJ unusable for
`MERGE`, which is the case it is most worth having. The only workaround is to
turn the [...]
### Does this PR introduce _any_ user-facing change?
Yes, it fixes the bug above: a query that failed with the exception now
runs. There is no change for queries that don't combine SPJ, join keys that are
a non-prefix subset of the partition keys, and a runtime filter on the same
scan. This is a change compared to released 4.0.x and 4.1.x; `master` and
`branch-4.2` already behave this way.
### How was this patch tested?
Six new tests in `KeyGroupedPartitioningSuite`, each asserting that the
dynamic filter really is pushed to the scan the bug needs it on and that SPJ is
triggered:
| shape | failure on `branch-4.1` without the source change |
|---|---|
| join key is the second of two partition keys, projection `Seq(1)` |
`SparkException`, must not report new partition values |
| join keys are the first and third of three, projection `Seq(0, 2)` | same
|
| projection changes the key types, `Seq(1)` over `(int, string)` |
`ClassCastException: class java.lang.Integer cannot be cast to class
org.apache.spark.unsafe.types.UTF8String` in
`InternalRowComparableWrapper.equals` |
| `partition.filter.enabled`, join keys cover all partition keys, **no
projection** | `SparkException`, "Before: 2 partition values. After: 3
partition values" |
| `partition.filter.enabled` on a `LEFT OUTER` join, so the values become
one side's | `SparkException`, must not report new partition values |
| `allowCompatibleTransforms.enabled` with `bucket(4, ...)` against
`bucket(2, ...)`, so the values are reduced | same |
All six pass with the source change. The last three carry no projection at
all, which is why the fix is about the value set rather than about
`joinKeyPositions`.
Note that `InMemoryBatchScan.filter` only prunes for a single-transform
table, so these tests cover the driver-side validation rather than the pruning
that follows it; the existing single-key dynamic-filtering tests in the suite
cover the pruning and padding path.
Also ran the full `KeyGroupedPartitioningSuite` (76 tests, covering the
partially-clustered and dynamic-filtering combinations that exercise the common
partition values path), plus `GroupBasedMergeIntoTableSuite`,
`GroupBasedDeleteFromTableSuite`, `GroupBasedUpdateTableSuite` and
`DataSourceV2Suite` — 322 tests in total.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 5)
Closes #58013 from peter-toth/SPARK-58783-spj-runtime-filtering-4.1.
Authored-by: Peter Toth <[email protected]>
Signed-off-by: Peter Toth <[email protected]>
---
.../execution/datasources/v2/BatchScanExec.scala | 7 +-
.../connector/KeyGroupedPartitioningSuite.scala | 279 +++++++++++++++++++++
2 files changed, 285 insertions(+), 1 deletion(-)
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/BatchScanExec.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/BatchScanExec.scala
index 55866cc85840..a7f8b3640204 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/BatchScanExec.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/BatchScanExec.scala
@@ -68,7 +68,12 @@ case class BatchScanExec(
}
if (dataSourceFilters.nonEmpty) {
- val originalPartitioning = outputPartitioning
+ // The partitions reported by the data source after filtering are
compared below by their
+ // full partition keys, so the partitioning they are compared against
must not be the
+ // projected one: `outputPartitioning` projects the partition keys down
to the join keys and
+ // replaces the partition values with the common ones of the join when
SPJ params were pushed
+ // down. Projecting and re-grouping the filtered partitions happens
later, in `inputRDD`.
+ val originalPartitioning = super.outputPartitioning
// the cast is safe as runtime filters are only assigned if the scan can
be filtered
val filterableScan = scan.asInstanceOf[SupportsRuntimeV2Filtering]
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 e8d51dedcda3..77324eb3b77c 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
@@ -2334,6 +2334,285 @@ class KeyGroupedPartitioningSuite extends
DistributionAndOrderingSuiteBase {
}
}
+ test("SPARK-58783: SPJ with runtime filtering when the join key is a
trailing partition key") {
+ val table1 = "table1"
+ val table2 = "table2"
+ // the join key is the second partition key of `table1`, so its scan
partitioning is projected
+ // down to `dept_id` when SPJ is applied
+ createTable(table1, columns2, Array(identity("store_id"),
identity("dept_id")))
+ sql(s"INSERT INTO testcat.ns.$table1 VALUES " +
+ "(100, 1, 'aa'), " +
+ "(100, 2, 'ab'), " +
+ "(200, 1, 'ac'), " +
+ "(200, 3, 'ad')")
+
+ createTable(table2, columns2, Array(identity("dept_id")))
+ sql(s"INSERT INTO testcat.ns.$table2 VALUES " +
+ "(100, 1, 'x'), " +
+ "(100, 2, 'y'), " +
+ "(100, 3, 'x')")
+
+ Seq(true, false).foreach { partiallyClustered =>
+ withSQLConf(
+ SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1",
+ SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_ENABLED.key -> "true",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_REUSE_BROADCAST_ONLY.key -> "false",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_FALLBACK_FILTER_RATIO.key -> "10",
+ 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_PARTIALLY_CLUSTERED_DISTRIBUTION_ENABLED.key ->
+ partiallyClustered.toString) {
+ val df = sql(
+ s"""
+ |${selectWithMergeJoinHint("t1", "t2")}
+ |t1.store_id, t1.dept_id, t1.data AS t1data, t2.data AS t2data
+ |FROM testcat.ns.$table1 t1 JOIN testcat.ns.$table2 t2
+ |ON t1.dept_id = t2.dept_id WHERE t2.data = 'x'
+ |ORDER BY t1.store_id, t1.dept_id, t1data
+ |""".stripMargin)
+ // the dynamic filter must land on the `table1` scan, the side whose
partitioning is
+ // projected down to a non-prefix of its partition keys
+ val scans = collectScans(df.queryExecution.executedPlan)
+ assert(
+ scans.exists(s =>
+ s.runtimeFilters.nonEmpty &&
s.spjParams.joinKeyPositions.contains(Seq(1))),
+ "the dynamic filter should be pushed to the scan with the projected
partitioning")
+ val shuffles = collectShuffles(df.queryExecution.executedPlan)
+ assert(shuffles.isEmpty, "SPJ should be triggered")
+ checkAnswer(df, Seq(
+ Row(100, 1, "aa", "x"),
+ Row(200, 1, "ac", "x"),
+ Row(200, 3, "ad", "x")))
+ }
+ }
+ }
+
+ test("SPARK-58783: SPJ with runtime filtering when the join keys have a gap
in the " +
+ "partition keys") {
+ val table1 = "table1"
+ val table2 = "table2"
+ // the join keys are the first and the third partition key of `table1`, so
its scan partitioning
+ // is projected down to `data, dept_id` when SPJ is applied. The partition
key the projection
+ // drops, `store_id`, sits between them, which is what makes the values it
is compared against
+ // line up with the wrong column.
+ createTable(table1, columns2,
+ Array(identity("data"), identity("store_id"), identity("dept_id")))
+ sql(s"INSERT INTO testcat.ns.$table1 VALUES " +
+ "(100, 1, 'aa'), " +
+ "(200, 2, 'aa'), " +
+ "(100, 3, 'bb')")
+
+ createTable(table2, columns2, Array(identity("data"), identity("dept_id")))
+ sql(s"INSERT INTO testcat.ns.$table2 VALUES " +
+ "(500, 1, 'aa'), " +
+ "(500, 2, 'aa'), " +
+ "(500, 3, 'bb')")
+
+ Seq(true, false).foreach { partiallyClustered =>
+ withSQLConf(
+ SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1",
+ SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_ENABLED.key -> "true",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_REUSE_BROADCAST_ONLY.key -> "false",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_FALLBACK_FILTER_RATIO.key -> "10",
+ 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_PARTIALLY_CLUSTERED_DISTRIBUTION_ENABLED.key ->
+ partiallyClustered.toString) {
+ val df = sql(
+ s"""
+ |${selectWithMergeJoinHint("t1", "t2")}
+ |t1.store_id, t1.dept_id, t1.data, t2.store_id AS t2store_id
+ |FROM testcat.ns.$table1 t1 JOIN testcat.ns.$table2 t2
+ |ON t1.data = t2.data AND t1.dept_id = t2.dept_id WHERE t2.data =
'aa'
+ |ORDER BY t1.store_id, t1.dept_id
+ |""".stripMargin)
+ val scans = collectScans(df.queryExecution.executedPlan)
+ assert(
+ scans.exists(s =>
+ s.runtimeFilters.nonEmpty &&
s.spjParams.joinKeyPositions.contains(Seq(0, 2))),
+ "the dynamic filter should be pushed to the scan with the projected
partitioning")
+ val shuffles = collectShuffles(df.queryExecution.executedPlan)
+ assert(shuffles.isEmpty, "SPJ should be triggered")
+ checkAnswer(df, Seq(
+ Row(100, 1, "aa", 500),
+ Row(200, 2, "aa", 500)))
+ }
+ }
+ }
+
+ test("SPARK-58783: SPJ with runtime filtering when the projection changes
the partition key " +
+ "types") {
+ val table1 = "table1"
+ val table2 = "table2"
+ // `data` is the second partition key, so the projection down to it leaves
the wrapper with a
+ // single `StringType`, which it then reads off the first field of the raw
key - an
+ // `IntegerType`
+ createTable(table1, columns2, Array(identity("dept_id"), identity("data")))
+ sql(s"INSERT INTO testcat.ns.$table1 VALUES " +
+ "(100, 1, 'aa'), " +
+ "(200, 2, 'aa'), " +
+ "(300, 3, 'bb')")
+
+ createTable(table2, columns2, Array(identity("data")))
+ sql(s"INSERT INTO testcat.ns.$table2 VALUES " +
+ "(500, 1, 'aa'), " +
+ "(500, 2, 'bb')")
+
+ Seq(true, false).foreach { partiallyClustered =>
+ withSQLConf(
+ SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_ENABLED.key -> "true",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_REUSE_BROADCAST_ONLY.key -> "false",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_FALLBACK_FILTER_RATIO.key -> "10",
+ 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_PARTIALLY_CLUSTERED_DISTRIBUTION_ENABLED.key ->
+ partiallyClustered.toString) {
+ val df = sql(
+ s"""
+ |${selectWithMergeJoinHint("t1", "t2")}
+ |t1.store_id, t1.dept_id, t1.data, t2.store_id AS t2store_id
+ |FROM testcat.ns.$table1 t1 JOIN testcat.ns.$table2 t2
+ |ON t1.data = t2.data WHERE t2.dept_id = 1
+ |ORDER BY t1.store_id, t1.dept_id
+ |""".stripMargin)
+ val scans = collectScans(df.queryExecution.executedPlan)
+ assert(
+ scans.exists(s =>
+ s.runtimeFilters.nonEmpty &&
s.spjParams.joinKeyPositions.contains(Seq(1))),
+ "the dynamic filter should be pushed to the scan with the projected
partitioning")
+ val shuffles = collectShuffles(df.queryExecution.executedPlan)
+ assert(shuffles.isEmpty, "SPJ should be triggered")
+ checkAnswer(df, Seq(
+ Row(100, 1, "aa", 500),
+ Row(200, 2, "aa", 500)))
+ }
+ }
+ }
+
+ test("SPARK-58783: SPJ with runtime filtering and partition filtering,
without a projection") {
+ val table1 = "table1"
+ val table2 = "table2"
+ // no projection here - the join keys are all of the partition keys. The
partition values are
+ // replaced by the intersection of the two sides instead, and `table1` has
a partition key that
+ // `table2` does not, so the keys it reports back after filtering are not
all in that
+ // intersection.
+ createTable(table1, columns2, Array(identity("store_id"),
identity("dept_id")))
+ sql(s"INSERT INTO testcat.ns.$table1 VALUES " +
+ "(100, 1, 'aa'), " +
+ "(100, 2, 'ab'), " +
+ "(200, 3, 'ac')")
+
+ createTable(table2, columns2, Array(identity("store_id"),
identity("dept_id")))
+ sql(s"INSERT INTO testcat.ns.$table2 VALUES " +
+ "(100, 1, 'x'), " +
+ "(100, 2, 'y')")
+
+ withSQLConf(
+ SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_ENABLED.key -> "true",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_REUSE_BROADCAST_ONLY.key -> "false",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_FALLBACK_FILTER_RATIO.key -> "10",
+ SQLConf.V2_BUCKETING_PARTITION_FILTER_ENABLED.key -> "true") {
+ val df = sql(
+ s"""
+ |${selectWithMergeJoinHint("t1", "t2")}
+ |t1.store_id, t1.dept_id, t1.data AS t1data, t2.data AS t2data
+ |FROM testcat.ns.$table1 t1 JOIN testcat.ns.$table2 t2
+ |ON t1.store_id = t2.store_id AND t1.dept_id = t2.dept_id WHERE
t2.data = 'x'
+ |ORDER BY t1.store_id, t1.dept_id
+ |""".stripMargin)
+ val scans = collectScans(df.queryExecution.executedPlan)
+ assert(
+ scans.exists(s => s.runtimeFilters.nonEmpty &&
s.table.name().endsWith(table1) &&
+ s.spjParams.joinKeyPositions.isEmpty),
+ s"the dynamic filter should be pushed to the unprojected $table1 scan")
+ val shuffles = collectShuffles(df.queryExecution.executedPlan)
+ assert(shuffles.isEmpty, "SPJ should be triggered")
+ checkAnswer(df, Seq(Row(100, 1, "aa", "x")))
+ }
+ }
+
+ test("SPARK-58783: SPJ with runtime filtering and partition filtering on an
outer join") {
+ val table1 = "table1"
+ val table2 = "table2"
+ // for an outer join the partition values become one side's, so here they
are `table1`'s and the
+ // scan that reports keys outside them is `table2`, the side an outer join
can prune
+ createTable(table1, columns2, Array(identity("store_id"),
identity("dept_id")))
+ sql(s"INSERT INTO testcat.ns.$table1 VALUES " +
+ "(100, 1, 'aa'), " +
+ "(300, 5, 'ab')")
+
+ createTable(table2, columns2, Array(identity("store_id"),
identity("dept_id")))
+ sql(s"INSERT INTO testcat.ns.$table2 VALUES " +
+ "(100, 1, 'x'), " +
+ "(200, 2, 'y')")
+
+ withSQLConf(
+ SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_ENABLED.key -> "true",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_REUSE_BROADCAST_ONLY.key -> "false",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_FALLBACK_FILTER_RATIO.key -> "10",
+ SQLConf.V2_BUCKETING_PARTITION_FILTER_ENABLED.key -> "true") {
+ val df = sql(
+ s"""
+ |${selectWithMergeJoinHint("t1", "t2")}
+ |t1.store_id, t1.dept_id, t1.data AS t1data, t2.data AS t2data
+ |FROM testcat.ns.$table1 t1 LEFT OUTER JOIN testcat.ns.$table2 t2
+ |ON t1.store_id = t2.store_id AND t1.dept_id = t2.dept_id WHERE
t1.data = 'aa'
+ |ORDER BY t1.store_id, t1.dept_id
+ |""".stripMargin)
+ val scans = collectScans(df.queryExecution.executedPlan)
+ assert(
+ scans.exists(s => s.runtimeFilters.nonEmpty &&
s.table.name().endsWith(table2)),
+ s"the dynamic filter should be pushed to the $table2 scan")
+ val shuffles = collectShuffles(df.queryExecution.executedPlan)
+ assert(shuffles.isEmpty, "SPJ should be triggered")
+ checkAnswer(df, Seq(Row(100, 1, "aa", "x")))
+ }
+ }
+
+ test("SPARK-58783: SPJ with runtime filtering and compatible bucket
transforms") {
+ val table1 = "table1"
+ val table2 = "table2"
+ // the two sides bucket into different numbers of buckets, so the
partition values pushed down
+ // are reduced into the smaller bucket space while the keys the scan
reports stay in its own
+ createTable(table1, columns2, Array(bucket(4, "store_id"), bucket(4,
"dept_id")))
+ sql(s"INSERT INTO testcat.ns.$table1 VALUES " +
+ "(0, 0, 'aa'), " +
+ "(2, 3, 'ab')")
+
+ createTable(table2, columns2, Array(bucket(2, "store_id"), bucket(2,
"dept_id")))
+ sql(s"INSERT INTO testcat.ns.$table2 VALUES " +
+ "(0, 0, 'x'), " +
+ "(1, 1, 'y')")
+
+ withSQLConf(
+ SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_ENABLED.key -> "true",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_REUSE_BROADCAST_ONLY.key -> "false",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_FALLBACK_FILTER_RATIO.key -> "10",
+ SQLConf.V2_BUCKETING_ALLOW_COMPATIBLE_TRANSFORMS.key -> "true") {
+ val df = sql(
+ s"""
+ |${selectWithMergeJoinHint("t1", "t2")}
+ |t1.store_id, t1.dept_id, t1.data AS t1data, t2.data AS t2data
+ |FROM testcat.ns.$table1 t1 JOIN testcat.ns.$table2 t2
+ |ON t1.store_id = t2.store_id AND t1.dept_id = t2.dept_id WHERE
t2.data = 'x'
+ |ORDER BY t1.store_id, t1.dept_id
+ |""".stripMargin)
+ val scans = collectScans(df.queryExecution.executedPlan)
+ assert(
+ scans.exists(s => s.runtimeFilters.nonEmpty &&
s.spjParams.reducers.nonEmpty),
+ "the dynamic filter should be pushed to the scan whose partition keys
are reduced")
+ val shuffles = collectShuffles(df.queryExecution.executedPlan)
+ assert(shuffles.isEmpty, "SPJ should be triggered")
+ checkAnswer(df, Seq(Row(0, 0, "aa", "x")))
+ }
+ }
+
test("SPARK-48012: one-side shuffle with partition transforms") {
val items_partitions = Array(bucket(2, "id"), identity("arrive_time"))
val items_partitions2 = Array(identity("arrive_time"), bucket(2, "id"))
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]