Skip to content

[SPARK-59120][SQL][4.2] Fix ClassCastException in a storage-partitioned join whose partition keys were reduced - #58431

Closed
peter-toth wants to merge 1 commit into
apache:branch-4.2from
peter-toth:SPARK-59120-keyed-partitioning-key-data-types-4.2
Closed

[SPARK-59120][SQL][4.2] Fix ClassCastException in a storage-partitioned join whose partition keys were reduced#58431
peter-toth wants to merge 1 commit into
apache:branch-4.2from
peter-toth:SPARK-59120-keyed-partitioning-key-data-types-4.2

Conversation

@peter-toth

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Backport of #58420 to branch-4.2.

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. KeyedShuffleSpec.canCreatePartitioning additionally refuses a partitioning whose expressions no longer describe the shape of its keys, so another child is not shuffled onto a reducer-rewritten layout.

Tailored for this branch in four places:

  • KeyedPartitioning.keyRowOrdering and the reduce path in EnsureRequirements keep building their ordering with RowOrdering.createNaturalAscendingOrdering. KeyedPartitioning.groupedKeyRowOrdering, which master calls there, arrived with SPARK-59027 and is not on this branch.
  • The PushDownUtils hunk is dropped. replanWithRuntimeFilters and the runtime-filter SPJ path it belongs to are not on this branch, and on master that hunk was a no-op anyway.
  • The config is spelled V2_BUCKETING_ALLOW_JOIN_KEYS_SUBSET_OF_PARTITION_KEYS here, the JOIN_ was dropped from the name later.
  • Two of the five tests are omitted, see the testing section.

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. A reduced partitioning is also 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 both sides, which returns the right rows.

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 this branch 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. That half is tracked in SPARK-59121.

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, which is why this branch is affected.

Does this PR introduce any user-facing change?

Yes, on the opt-in v2BucketingAllowCompatibleTransforms path. Queries that failed with ClassCastException as the shuffle was prepared now plan and return rows. One error changes: two sides with genuinely incompatible reduced types now say so instead of throwing ClassCastException.

How was this patch tested?

Three tests, each measured failing on branch-4.2 at 149b29aac1d and passing here:

  • SPARK-59120: canCreatePartitioning: KeyedShuffleSpec requires the declared key shape
  • SPARK-59120: incompatible reduced key types are reported instead of cast, ClassCastException on the base
  • SPARK-59120: another child is not shuffled onto reducer-rewritten keys, ClassCastException on the base

Two tests from #58420 are omitted rather than carried over green. Both were measured passing on this branch's base, so they would pin nothing here: createShuffleSpec sorts the projected keys at their built-with types and reduced partition keys are read at the types they were built with. Both reach the crash through the sort that createShuffleSpec applies on master, and that sort arrived with SPARK-59022, which is not on this branch.

171 tests green across ShuffleSpecSuite, KeyGroupedPartitioningSuite, GroupPartitionsExecSuite, EnsureRequirementsSuite, ValidateRequirementsSuite and ProjectedOrderingAndPartitioningSuite. dev/lint-scala clean.

Was this patch authored or co-authored using generative AI tooling?

Yes. Generated-by: Claude Code.

…in whose partition keys were reduced
`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 because those wrappers are compared against the stored keys, which the switch keeps them comparable with. One drive-by in the same reduce path: it built its ordering with `RowOrdering.createNaturalAscendingOrdering` directly, and now calls `KeyedPartitioning.groupedKeyRowOrdering`, which is where that order is defined.
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 SPARK-59121.
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 both sides, which returns the right rows.
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.
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, so its results change from wrong to right.
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 the plan the fix is about: no shuffle for the co-partitioned pair, two shuffles for the child that must not be laid out on reduced keys.
- `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.
Generated-by: Claude Code
Closesapache#58420 from peter-toth/SPARK-59120-keyed-partitioning-key-data-types.
Authored-by: Peter Toth <peter.toth@gmail.com>
Signed-off-by: Peter Toth <peter.toth@gmail.com>
(cherry picked from commit 7f12c2d)
@peter-toth

Copy link
Copy Markdown
ContributorAuthor

cc @ulysses-you , @dongjoon-hyun

peter-toth added a commit that referenced this pull request Aug 31, 2026
…ed join whose partition keys were reduced
### What changes were proposed in this pull request?
Backport of #58420 to `branch-4.2`.
`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. `KeyedShuffleSpec.canCreatePartitioning` additionally refuses a partitioning whose expressions no longer describe the shape of its keys, so another child is not shuffled onto a reducer-rewritten layout.
Tailored for this branch in four places:
- `KeyedPartitioning.keyRowOrdering` and the reduce path in `EnsureRequirements` keep building their ordering with `RowOrdering.createNaturalAscendingOrdering`. `KeyedPartitioning.groupedKeyRowOrdering`, which `master` calls there, arrived with SPARK-59027 and is not on this branch.
- The `PushDownUtils` hunk is dropped. `replanWithRuntimeFilters` and the runtime-filter SPJ path it belongs to are not on this branch, and on `master` that hunk was a no-op anyway.
- The config is spelled `V2_BUCKETING_ALLOW_JOIN_KEYS_SUBSET_OF_PARTITION_KEYS` here, the `JOIN_` was dropped from the name later.
- Two of the five tests are omitted, see the testing section.
### 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`. A reduced partitioning is also 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 both sides, which returns the right rows.
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 this branch 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. That half is tracked in SPARK-59121.
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, which is why this branch is affected.
### Does this PR introduce _any_ user-facing change?
Yes, on the opt-in `v2BucketingAllowCompatibleTransforms` path. Queries that failed with `ClassCastException` as the shuffle was prepared now plan and return rows. One error changes: two sides with genuinely incompatible reduced types now say so instead of throwing `ClassCastException`.
### How was this patch tested?
Three tests, each measured failing on `branch-4.2` at `149b29aac1d` and passing here:
- `SPARK-59120: canCreatePartitioning: KeyedShuffleSpec requires the declared key shape`
- `SPARK-59120: incompatible reduced key types are reported instead of cast`, `ClassCastException` on the base
- `SPARK-59120: another child is not shuffled onto reducer-rewritten keys`, `ClassCastException` on the base
Two tests from #58420 are omitted rather than carried over green. Both were measured **passing** on this branch's base, so they would pin nothing here: `createShuffleSpec sorts the projected keys at their built-with types` and `reduced partition keys are read at the types they were built with`. Both reach the crash through the sort that `createShuffleSpec` applies on `master`, and that sort arrived with SPARK-59022, which is not on this branch.
171 tests green across `ShuffleSpecSuite`, `KeyGroupedPartitioningSuite`, `GroupPartitionsExecSuite`, `EnsureRequirementsSuite`, `ValidateRequirementsSuite` and `ProjectedOrderingAndPartitioningSuite`. `dev/lint-scala` clean.
### Was this patch authored or co-authored using generative AI tooling?
Yes. Generated-by: Claude Code.
Closes#58431 from peter-toth/SPARK-59120-keyed-partitioning-key-data-types-4.2.
Authored-by: Peter Toth <peter.toth@gmail.com>
Signed-off-by: Peter Toth <peter.toth@gmail.com>
@peter-toth

Copy link
Copy Markdown
ContributorAuthor

Merge Summary:

Posted by merge_spark_pr.py

@peter-toth

Copy link
Copy Markdown
ContributorAuthor

Thank you @ulysses-you for the review.

ulysses-you added a commit that referenced this pull request Sep 2, 2026
…es partition key data type
### What changes were proposed in this pull request?
Backport of #58335 to `branch-4.2`.
`KeyedShuffleSpec.reducersBothWays` now pairs each `Reducer` with the reduced partition expression it produces (`KeyReducer`), and `GroupPartitionsExec.outputPartitioning` reports the reduced expression instead of the original partition expressions when reducers are applied. The stored expression is re-targeted at each `KeyedPartitioning`'s own key attribute at the use site via the new `TransformExpression.withReference`, so a chained storage-partitioned join keeps every side's partitioning intact.
`GroupPartitionsExec.doCanonicalize` additionally normalizes the exprIds inside `KeyReducer` - plan canonicalization does not reach into the plain case class - and the reducer applied for an identity-vs-transform pair is a named `IdentityReducer` case class, so structurally identical SPJ subtrees with value-equal reducers still compare equal and exchange/subquery reuse keeps deduplicating them.
Tailored for this branch in four places:
- `GroupPartitionsExec.outputPartitioning` keeps this branch's `groupedPartitions`/`isGrouped` reporting and its multi-`KeyedPartitioning` partition-keys assertion. The `PartitionGrouping`/`isCollapsed` refactor, which replaced both on `master`, is not on this branch.
- The config is spelled `V2_BUCKETING_ALLOW_JOIN_KEYS_SUBSET_OF_PARTITION_KEYS` here, the `JOIN_` was dropped from the name later.
- The canonicalization test builds `LocalTableScanExec` with this branch's three-argument constructor.
- The subset-join-key test additionally sets `REQUIRE_ALL_CLUSTER_KEYS_FOR_CO_PARTITION` to false. SPARK-58558, which relaxed `createKeyedShuffleSpec` from an exact key match to a key-coverage check so the subset shape plans under the default, is not on this branch; the branch's other subset tests set the config the same way.
One of the three `SPARK-59120` tests from #58335 is not carried over: `SPARK-59120: reduced partition keys are read at the types they were built with` was already omitted from the #58420 backport (#58431), because its failure mode runs through the sort that `createShuffleSpec` applies, which arrived with SPARK-59022 and is not on this branch. The other two `SPARK-59120` tests are carried over verbatim; like on `master`, they replace the two #58431 tests and pin the combined behavior.
### Why are the changes needed?
In a storage-partitioned join with compatible transforms whose result types differ (e.g. `identity(id)` on one side and `bucket(N, id)` on the other), the reducer maps the partition keys to the other side's value type. `GroupPartitionsExec.outputPartitioning` used to report the original expressions with the reduced keys, so the two had different data types and computing the key ordering threw:
```
java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.Long
```
This fix covers the reducers where the reduced keys equal a single transform applied to one side (identity-vs-transform and single-side-transform). When both sides of a compatible-transform join reduce their keys, the reduced keys are not expressible as a single transform; that shape is a known gap tracked in SPARK-59121.
### Does this PR introduce _any_ user-facing change?
No by default. Under `spark.sql.sources.v2.bucketing.allowCompatibleTransforms.enabled`, a storage-partitioned join whose reducer changes the partition key data type previously threw `ClassCastException` and now succeeds.
### How was this patch tested?
Added regression tests in `KeyGroupedPartitioningSuite`, mirroring #58335. Measured failing on this branch at `0e37b68e742` (with #58431 on it):
- `SPARK-59045: compatible transforms reduce multiple times` fails with `ClassCastException: class java.lang.Integer cannot be cast to class java.lang.Long` - the second reduce's reducer is derived from the stale reported expression, which no type fix reaches.
- The two carried-over `SPARK-59120` tests fail: the second-join one raises `STORAGE_PARTITION_JOIN_INCOMPATIBLE_REDUCED_TYPES` (the behavior #58431 pinned on this branch), and the another-child one plans two shuffles instead of one. They now pin the combined behavior.
- The `shipments` extension of `SPARK-56046: Reducers with same result types` fails when the third table reduces onto the first join's stale reported expression and that reduce evaluates the expression (`SCALAR_FUNCTION_NOT_FULLY_IMPLEMENTED`).
Measured passing on that base, as on `master`: the identity-vs-bucket reducer, the subset-join-key, and the per-`KeyedPartitioning` retargeting tests - they pin this change's reporting, and the retargeting test regresses together with the multi-reduce test if the use-site re-targeting is dropped. The canonicalization test arrived with `KeyReducer` in this change. The first three also assert the storage-partitioned join introduces no shuffle. All tests pass here.
Ran `KeyGroupedPartitioningSuite`, `GroupPartitionsExecSuite`, `EnsureRequirementsSuite`, `ProjectedOrderingAndPartitioningSuite`, `ShuffleSpecSuite`, `ValidateRequirementsSuite`, and `DistributionSuite`.
### Was this patch authored or co-authored using generative AI tooling?
Yes. Generated-by: Claude Code.
Closes#58451 from ulysses-you/spj-reducer-4.2.
Authored-by: Xiduo You <ulyssesyou18@gmail.com>
Signed-off-by: Xiduo You <ulyssesyou@apache.org>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@peter-toth@ulysses-you