This is an automated email from the ASF dual-hosted git repository.
peter-toth pushed a commit to branch branch-4.3
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.3 by this push:
new 14a232b5abb3 [SPARK-59120][SQL] Fix ClassCastException in a
storage-partitioned join whose partition keys were reduced
14a232b5abb3 is described below
commit 14a232b5abb3aab7c18274937aef460bef11b57b
Author: Peter Toth <[email protected]>
AuthorDate: Mon Aug 31 12:03:57 2026 +0200
[SPARK-59120][SQL] Fix ClassCastException in a storage-partitioned join
whose partition keys were reduced
### What changes were proposed in this pull request?
`KeyedPartitioning` gains `keyDataTypes`, the schema each `partitionKeys`
row was actually written under, and everything that reads those rows takes its
types from there instead of from the partition expressions.
The two can disagree. With `v2BucketingAllowCompatibleTransforms` a
storage-partitioned join reduces one or both sides' keys onto a common key
space, and the partitioning keeps reporting the expressions it was built from.
Joining an `identity(ts)`-partitioned table to a `years(ts)`-partitioned one
leaves `IntegerType` year values under a `TimestampType`-declared expression. A
row is only readable at its own schema, so the wrappers are the authority:
transient lazy val keyDataTypes: Seq[DataType] =
partitionKeys.headOption.map(_.dataTypes).getOrElse(expressionDataTypes)
Switched to it: `keyRowOrdering` (and with it `toGrouped` and
`createShuffleSpec`'s subset projection), the instance-level `projectKeys` and
`reduceKeys`, `GroupPartitionsExec`'s projection base types, and the reduce
path in `EnsureRequirements`. `PushDownUtils` changes basis twice, at its
wrapper factory and through `keyRowOrdering` at its sort, and both are no-ops
today: it handles rows a scan has just reported, and a scan's own partitioning
is never reduced. They are switched becau [...]
The expressions' own types stay right in one place. `ShuffleExchangeExec`
builds a `KeyGroupedPartitioner` whose lookup keys come from evaluating the
expressions per row, so both sides of that comparison are declared at
`expressionDataTypes`.
That is also what the second change is about. A reduced partitioning must
not be the layout another child is shuffled onto, whatever types it declares,
so `KeyedShuffleSpec.canCreatePartitioning` refuses the ones it can detect. The
question gets a name next to the two values it relates:
transient lazy val expressionsDescribeKeyShape: Boolean =
keyDataTypes.corresponds(expressionDataTypes)(
DataType.equalsStructurally(_, _, ignoreNullability = true))
Shapes rather than plain equality, which is the test `HashJoin` already
applies to its join key types. `createPartitioning` puts the other child's
expressions over this side's keys, so a struct key whose two sides name the
field differently reaches the gate with no reducer involved, and refusing it
would cost a shuffle for nothing.
The shape test is only a proxy for the real question, and the comment says
so. A reduction that keeps the type, `bucket(12)` and `bucket(8)` both reducing
onto `bucket(4)`, passes it and still misroutes rows. SPARK-59045 and
SPARK-59121 add the real test.
Two of the switched sites are inert today, for the same reason as the
`PushDownUtils` no-op: `reduceKeys` and `GroupPartitionsExec`'s projection only
see a reduced partitioning when a second reduce lands on a first, which still
fails at planning either way, in `KeyedShuffleSpec.reducers`. That reducer is
bound to the partition expression and then reads a stored key, so it is a
fourth reader of these rows that types cannot fix. It is named in the
`keyDataTypes` scaladoc and belongs to [...]
### Why are the changes needed?
Both halves fail a query on the `v2BucketingAllowCompatibleTransforms` path.
Reading a reduced key at the expression's type unboxes an Integer as a
Long, which throws `ClassCastException` at planning. AQE reaches it on its own:
`ValidateRequirements` re-runs `createShuffleSpec` on the already reduced
children, and with the subset opt-in on that projects and sorts the keys. The
first test below is such a query.
The second half is independent of the first, and it fails at execution
rather than at planning. A reduced partitioning is grouped and does satisfy the
distribution, so `EnsureRequirements` offers it as the layout to shuffle
another child onto. `ShuffleExchangeExec` then builds the shuffle's key map by
re-wrapping the stored keys at the expressions' types, and the same unboxing
throws, on the driver as the shuffle is prepared. The gate makes
`EnsureRequirements` fall back to shuffling [...]
The `EnsureRequirements` reduce path is the third behaviour change. Its
`fold` default now reports the keys' own types, so two sides whose reduced key
spaces genuinely differ raise
`STORAGE_PARTITION_JOIN_INCOMPATIBLE_REDUCED_TYPES` where `master` cast one to
the other and threw `ClassCastException` in the merge.
What the gate does not fix is a reduction that keeps the type. There
`KeyGroupedPartitioner` sends a key it cannot find to `hashCode %
numPartitions`, and rows are lost silently, both before and after this change.
`bucket(12)` joined to `bucket(8)`, then joined to an unpartitioned table,
returns 8 of 12 rows on `master` and still does here.
The reduce and the reporting of reduced keys under the original expressions
arrived in SPARK-47094 (4.0.0). The crashes start in 4.2.0, with the
`KeyedPartitioning` / `GroupPartitionsExec` refactor that derives the key
ordering and types from the reported expressions.
### Does this PR introduce _any_ user-facing change?
Yes, on the opt-in `v2BucketingAllowCompatibleTransforms` path. Queries
that failed with `ClassCastException` at planning, as the shuffle was prepared,
or in the shuffle write, now plan and return rows. One error changes: two sides
with genuinely incompatible reduced types now say so instead of throwing
`ClassCastException`. A connector whose transforms reduce onto a
type-compatible key space could previously lose rows to the partitioner's hash
fallback and now gets a shuffle instead, [...]
### How was this patch tested?
Five new tests, all five failing on `master`:
- `KeyGroupedPartitioningSuite`: `reduced partition keys are read at the
types they were built with`, which throws at planning on `master`, in
`toGrouped` under `createShuffleSpec` under `ValidateRequirements`. `another
child is not shuffled onto reducer-rewritten keys`, which throws on `master` as
the shuffle is prepared. And `incompatible reduced key types are reported
instead of cast`, which throws `ClassCastException` on `master` and the named
error here. The first two also assert [...]
- `ShuffleSpecSuite`: `createShuffleSpec sorts the projected keys at their
built-with types`, the unit-level form of the first one, same exception on
`master`. And `canCreatePartitioning: KeyedShuffleSpec requires the declared
key shape`, which asserts both directions, the reduced keys refused and the
differently named struct fields allowed.
The two changes are pinned separately by ablation. Removing the
`canCreatePartitioning` clause leaves the first end-to-end test passing and
makes the second one throw again, so neither test stands in for the other.
Tightening the shape test to plain equality fails the allowed direction alone.
291 tests green across `KeyGroupedPartitioningSuite`,
`KeyGroupedPartitioningCatalystRuntimeFilterSuite`, `EnsureRequirementsSuite`,
`ValidateRequirementsSuite`, `ProjectedOrderingAndPartitioningSuite`,
`PlannerSuite`, `ShuffleSpecSuite` and `DistributionSuite`. `dev/lint-scala`
clean.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code
Closes #58420 from peter-toth/SPARK-59120-keyed-partitioning-key-data-types.
Authored-by: Peter Toth <[email protected]>
Signed-off-by: Peter Toth <[email protected]>
(cherry picked from commit 7f12c2da0b4e419c9fa1bb8a059881ea54c55451)
Signed-off-by: Peter Toth <[email protected]>
---
.../sql/catalyst/plans/physical/partitioning.scala | 55 ++++++++++++-
.../spark/sql/catalyst/ShuffleSpecSuite.scala | 41 +++++++++-
.../datasources/v2/GroupPartitionsExec.scala | 2 +-
.../execution/datasources/v2/PushDownUtils.scala | 2 +-
.../execution/exchange/EnsureRequirements.scala | 9 +-
.../connector/KeyGroupedPartitioningSuite.scala | 95 ++++++++++++++++++++++
6 files changed, 192 insertions(+), 12 deletions(-)
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala
index 688968bda16b..6c585e043432 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala
@@ -554,10 +554,51 @@ case class KeyedPartitioning(
newChildren: IndexedSeq[Expression]): KeyedPartitioning =
copy(expressions = newChildren)
+ /** Need not be what the `partitionKeys` rows hold. See `keyDataTypes`. */
@transient lazy val expressionDataTypes: Seq[DataType] =
expressions.map(_.dataType)
+ /**
+ * The types the `partitionKeys` rows were built with. Anything reading
those rows should take its
+ * types from here. It is a driver-side value, since `partitionKeys` is
`@transient`.
+ *
+ * They are the `expressionDataTypes` unless a reducer has rewritten the
keys. With
+ * `v2BucketingAllowCompatibleTransforms` a storage-partitioned join reduces
one or both sides'
+ * keys onto a common key space, while the partitioning keeps reporting the
expressions it was
+ * built from. Joining an `identity(ts)`-partitioned table to a
`years(ts)`-partitioned one leaves
+ * `IntegerType` year values under a `TimestampType`-declared expression.
With no key at all the
+ * expressions are all there is, and there is no row to read or to place.
+ *
+ * `ShuffleExchangeExec` is the one reader that stays on
`expressionDataTypes`. It evaluates the
+ * expressions to place the other child's rows, and it runs on executors,
where this value is not
+ * available. `expressionsDescribeKeyShape` is what keeps that site sound.
+ *
+ * Only the first key's types are read, and nothing enforces that the rest
match. Reduced keys
+ * also break three readers in ways no choice of types can fix, and
SPARK-59121 covers all three:
+ *
+ * - `EnsureRequirements`' `OrderedDistribution` arm orders these rows by
the partition
+ * attributes.
+ * - the `UnionExec` key merge concatenates several children's keys.
+ * - `KeyedShuffleSpec.reducers` binds its reducer to the partition
expression, then reads a
+ * stored key with it.
+ */
+ @transient lazy val keyDataTypes: Seq[DataType] =
+ partitionKeys.headOption.map(_.dataTypes).getOrElse(expressionDataTypes)
+
+ /**
+ * Whether the `partitionKeys` rows still read the same at
`expressionDataTypes`, which is what
+ * `ShuffleExchangeExec` declares them at.
+ *
+ * Shapes rather than plain equality, the test `HashJoin` applies to its
join key types.
+ * `KeyedShuffleSpec.createPartitioning` puts the other child's expressions
over these keys, so a
+ * struct field name can differ here with no reducer involved.
+ */
+ @transient lazy val expressionsDescribeKeyShape: Boolean =
+ keyDataTypes.corresponds(expressionDataTypes)(
+ DataType.equalsStructurally(_, _, ignoreNullability = true))
+
+ /** Driver-side, like the `keyDataTypes` it comes from. */
@transient lazy val keyRowOrdering =
- KeyedPartitioning.groupedKeyRowOrdering(expressionDataTypes)
+ KeyedPartitioning.groupedKeyRowOrdering(keyDataTypes)
@transient lazy val keyOrdering = keyRowOrdering.on((t:
InternalRowComparableWrapper) => t.row)
@@ -572,7 +613,7 @@ case class KeyedPartitioning(
* Returns the projected expressions and their data types together with the
projected keys.
*/
def projectKeys(positions: Seq[Int]): (Seq[DataType],
Seq[InternalRowComparableWrapper]) =
- KeyedPartitioning.projectKeys(partitionKeys, expressionDataTypes,
positions)
+ KeyedPartitioning.projectKeys(partitionKeys, keyDataTypes, positions)
/**
* Reduces this partitioning's partition keys by applying the given reducers.
@@ -580,7 +621,7 @@ case class KeyedPartitioning(
*/
def reduceKeys(
reducers: Seq[Option[Reducer[_, _]]]): (Seq[DataType],
Seq[InternalRowComparableWrapper]) =
- KeyedPartitioning.reduceKeys(partitionKeys, expressionDataTypes, reducers)
+ KeyedPartitioning.reduceKeys(partitionKeys, keyDataTypes, reducers)
override def satisfies0(required: Distribution): Boolean = {
nonGroupedSatisfies(required) || (isGrouped && groupedSatisfies(required))
@@ -1434,7 +1475,13 @@ case class KeyedShuffleSpec(
partitioning.isGrouped &&
partitioning.expressions.forall { e =>
e.isInstanceOf[AttributeReference] ||
e.isInstanceOf[TransformExpression]
- }
+ } &&
+ // A reducer leaves keys that the expressions no longer describe, and
`ShuffleExchangeExec`
+ // would then compare the two at different types as it builds the
shuffle's key map. Matching
+ // shapes are only a proxy for that. `bucket(12)` and `bucket(8)`
reducing onto `bucket(4)`
+ // keep the type, pass here, and still misroute rows. SPARK-59045 and
SPARK-59121 add the real
+ // test.
+ partitioning.expressionsDescribeKeyShape
override def createPartitioning(clustering: Seq[Expression]): Partitioning =
{
assert(clustering.size == distribution.clustering.size,
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/ShuffleSpecSuite.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/ShuffleSpecSuite.scala
index 35f06a722f19..a76ba44b6803 100644
---
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/ShuffleSpecSuite.scala
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/ShuffleSpecSuite.scala
@@ -19,10 +19,11 @@ package org.apache.spark.sql.catalyst
import org.apache.spark.{SparkFunSuite, SparkUnsupportedOperationException}
import org.apache.spark.sql.catalyst.dsl.expressions._
-import org.apache.spark.sql.catalyst.expressions.DirectShufflePartitionID
+import org.apache.spark.sql.catalyst.expressions.{Attribute,
DirectShufflePartitionID}
import org.apache.spark.sql.catalyst.plans.SQLHelper
import org.apache.spark.sql.catalyst.plans.physical._
import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.types.{IntegerType, StructType}
class ShuffleSpecSuite extends SparkFunSuite with SQLHelper {
private val passThrough_a_10 =
ShufflePartitionIdPassThrough(DirectShufflePartitionID($"a"), 10)
@@ -444,6 +445,44 @@ class ShuffleSpecSuite extends SparkFunSuite with
SQLHelper {
}
}
+ test("SPARK-59120: canCreatePartitioning: KeyedShuffleSpec requires the
declared key shape") {
+ // A partitioning whose keys were built with types the expressions no
longer declare, which is
+ // what a reducer leaves behind.
+ def spec(builtWith: Attribute, declared: Attribute, key: InternalRow):
KeyedShuffleSpec =
+ KeyedShuffleSpec(
+ KeyedPartitioning(Seq(builtWith), Seq(key)).copy(expressions =
Seq(declared)),
+ ClusteredDistribution(Seq(declared)))
+
+ withSQLConf(SQLConf.V2_BUCKETING_SHUFFLE_ENABLED.key -> "true") {
+ assert(!spec($"a".int, $"a".timestamp,
InternalRow(2020)).canCreatePartitioning,
+ "`IntegerType` keys cannot be looked up by evaluating a
`TimestampType` expression")
+
+ // The two sides can name a struct field differently with no reducer
involved, see
+ // `expressionsDescribeKeyShape`.
+ def struct(field: String): Attribute = $"a".struct(new
StructType().add(field, IntegerType))
+ assert(spec(struct("a"), struct("b"),
InternalRow(InternalRow(1))).canCreatePartitioning,
+ "a key row reads the same either way, so the field names must not
matter")
+ }
+ }
+
+ test("SPARK-59120: createShuffleSpec sorts the projected keys at their
built-with types") {
+ val a = $"a".timestamp
+ val b = $"b".int
+ // A reducer left `(year, bucket)` values, both `IntegerType`, under
expressions that still
+ // declare `(TimestampType, IntegerType)`. Projecting to the `a` position
sorts them. An
+ // ordering built from the declared types reads the year as a Long and
throws.
+ val reduced =
+ KeyedPartitioning(Seq($"a".int, b), Seq(InternalRow(2021, 1),
InternalRow(2020, 0)))
+ .copy(expressions = Seq(a, b))
+
+ withSQLConf(SQLConf.V2_BUCKETING_ALLOW_KEYS_SUBSET_OF_PARTITION_KEYS.key
-> "true") {
+ val spec = reduced.createShuffleSpec(ClusteredDistribution(Seq(a)))
+ .asInstanceOf[KeyedShuffleSpec]
+ assert(spec.joinKeyPositions === Some(Seq(0)))
+ assert(spec.partitioning.partitionKeys.map(_.row.getInt(0)) ===
Seq(2020, 2021))
+ }
+ }
+
test("createPartitioning: HashShuffleSpec") {
checkCreatePartitioning(
HashShuffleSpec(HashPartitioning(Seq($"a"), 10),
ClusteredDistribution(Seq($"a", $"b"))),
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala
index 6f671bfcb80b..22e7649886ed 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala
@@ -136,7 +136,7 @@ case class GroupPartitionsExec(
// Project partition keys if join key positions are specified
val (projectedDataTypes, projectedKeys) =
joinKeyPositions.fold(
- (keyedPartitioning.expressionDataTypes,
keyedPartitioning.partitionKeys)
+ (keyedPartitioning.keyDataTypes, keyedPartitioning.partitionKeys)
)(keyedPartitioning.projectKeys)
// Reduce keys if reducers are specified
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/PushDownUtils.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/PushDownUtils.scala
index fa330212370f..9a1d00a96a68 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/PushDownUtils.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/PushDownUtils.scala
@@ -313,7 +313,7 @@ object PushDownUtils extends Logging {
val inputMap =
k.partitionKeys.groupBy(identity).view.mapValues(_.size)
val comparableKeyWrapperFactory = InternalRowComparableWrapper
- .getInternalRowComparableWrapperFactory(k.expressionDataTypes)
+ .getInternalRowComparableWrapperFactory(k.keyDataTypes)
val filteredMap = newPartitions.groupBy(
p =>
comparableKeyWrapperFactory(p.asInstanceOf[HasPartitionKey].partitionKey())
)
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 d55c515ab782..c1e125427ff8 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
@@ -557,10 +557,10 @@ case class EnsureRequirements(
val leftReducers = leftSpec.reducers(rightSpec)
val rightReducers = rightSpec.reducers(leftSpec)
val (leftReducedDataTypes, leftReducedKeys) = leftReducers.fold(
- (leftPartitioning.expressionDataTypes,
leftPartitioning.partitionKeys)
+ (leftPartitioning.keyDataTypes, leftPartitioning.partitionKeys)
)(leftPartitioning.reduceKeys)
val (rightReducedDataTypes, rightReducedKeys) = rightReducers.fold(
- (rightPartitioning.expressionDataTypes,
rightPartitioning.partitionKeys)
+ (rightPartitioning.keyDataTypes, rightPartitioning.partitionKeys)
)(rightPartitioning.reduceKeys)
val reducedDataTypes = if (leftReducedDataTypes ==
rightReducedDataTypes) {
leftReducedDataTypes
@@ -572,9 +572,8 @@ case class EnsureRequirements(
rightReducedDataTypes = rightReducedDataTypes)
}
- val reducedKeyRowOrdering =
RowOrdering.createNaturalAscendingOrdering(reducedDataTypes)
- val reducedKeyOrdering =
- reducedKeyRowOrdering.on((t: InternalRowComparableWrapper) => t.row)
+ val reducedKeyOrdering =
KeyedPartitioning.groupedKeyRowOrdering(reducedDataTypes)
+ .on((t: InternalRowComparableWrapper) => t.row)
// merge values on both sides
var mergedPartitionKeys =
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 c8cc20b5c42d..29141f0b2cd8 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
@@ -4922,4 +4922,99 @@ class KeyGroupedPartitioningSuite extends
DistributionAndOrderingSuiteBase with
}
}
}
+
+ private def createTsTable(name: String, partitions: Array[Transform]): Unit
= {
+ createTable(name, columns, partitions)
+ sql(s"INSERT INTO testcat.ns.$name VALUES " +
+ s"(1, 'aa', cast('2020-01-01' as timestamp)), " +
+ s"(2, 'bb', cast('2021-06-01' as timestamp))")
+ }
+
+ test("SPARK-59120: reduced partition keys are read at the types they were
built with") {
+ // The join reduces the identity side onto the year key space, so its keys
become `IntegerType`
+ // years while the partitioning still reports `identity(ts)`, declaring
`TimestampType`. With
+ // the subset opt-in on, AQE re-runs `createShuffleSpec` on the already
reduced children through
+ // `ValidateRequirements`, which projects and sorts those keys. The
mechanism is in
+ // `ShuffleSpecSuite`'s "createShuffleSpec sorts the projected keys at
their built-with types".
+ withTable("t_identity", "t_years") {
+ createTsTable("t_identity", Array(identity("ts")))
+ createTsTable("t_years", Array(years("ts")))
+
+ val query =
+ s"""${selectWithMergeJoinHint("a", "b")} a.id, b.data
+ |FROM testcat.ns.t_identity a JOIN testcat.ns.t_years b ON a.ts =
b.ts
+ |""".stripMargin
+
+ withSQLConf(
+ SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_ALLOW_COMPATIBLE_TRANSFORMS.key -> "true",
+ SQLConf.V2_BUCKETING_ALLOW_KEYS_SUBSET_OF_PARTITION_KEYS.key ->
"true") {
+ val df = sql(query)
+ checkAnswer(df, Seq(Row(1, "aa"), Row(2, "bb")))
+ assert(collectShuffles(df.queryExecution.executedPlan).isEmpty,
+ "the two sides are reduced onto one key space, so they stay
co-partitioned")
+ }
+ }
+ }
+
+ test("SPARK-59120: incompatible reduced key types are reported instead of
cast") {
+ // The first join reduces the identity side onto the year key space. The
third table does not
+ // reduce at all, so the two key spaces genuinely differ, and reading each
side's keys at their
+ // own types is what lets `EnsureRequirements` say so. On `master` the
merge cast one to the
+ // other and threw `ClassCastException`.
+ withTable("t_identity", "t_years", "t_identity2") {
+ createTsTable("t_identity", Array(identity("ts")))
+ createTsTable("t_years", Array(years("ts")))
+ createTsTable("t_identity2", Array(identity("ts")))
+
+ val query =
+ """SELECT /*+ MERGE(a, b), MERGE(a, c) */ a.id, c.data
+ |FROM testcat.ns.t_identity a JOIN testcat.ns.t_years b ON a.ts =
b.ts
+ |JOIN testcat.ns.t_identity2 c ON a.ts = c.ts
+ |""".stripMargin
+
+ withSQLConf(
+ SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+ SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_ALLOW_COMPATIBLE_TRANSFORMS.key -> "true") {
+ checkError(
+ exception = intercept[SparkException](sql(query).collect()),
+ condition = "STORAGE_PARTITION_JOIN_INCOMPATIBLE_REDUCED_TYPES",
+ parameters = Map(
+ "leftReducers" -> "[]",
+ "leftReducedDataTypes" -> "[\"INT\"]",
+ "rightReducers" -> "[]",
+ "rightReducedDataTypes" -> "[\"TIMESTAMP\"]"))
+ }
+ }
+ }
+
+ test("SPARK-59120: another child is not shuffled onto reducer-rewritten
keys") {
+ // `canCreatePartitioning` refuses the reduced side as a layout for the
second join, so both of
+ // that join's sides are shuffled. Without the gate the driver dies while
assembling that
+ // shuffle's key map, where the stored keys are re-wrapped at the
expressions' types.
+ withTable("t_identity", "t_years", "t_plain") {
+ createTsTable("t_identity", Array(identity("ts")))
+ createTsTable("t_years", Array(years("ts")))
+ createTsTable("t_plain", Array.empty[Transform])
+
+ val query =
+ """SELECT /*+ MERGE(a, b), MERGE(c) */ a.id, c.data
+ |FROM testcat.ns.t_identity a JOIN testcat.ns.t_years b ON a.ts =
b.ts
+ |JOIN testcat.ns.t_plain c ON a.ts = c.ts
+ |""".stripMargin
+
+ withSQLConf(
+ SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+ SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_SHUFFLE_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_ALLOW_COMPATIBLE_TRANSFORMS.key -> "true") {
+ val df = sql(query)
+ checkAnswer(df, Seq(Row(1, "aa"), Row(2, "bb")))
+ assert(collectAllShuffles(df.queryExecution.executedPlan).size == 2,
+ "the second join shuffles both of its sides")
+ }
+ }
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]