peter-toth opened a new pull request, #58420:
URL: https://github.com/apache/spark/pull/58420

   ### 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 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.
   
   ### 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 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.
   
   ### 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, so its results change from wrong to 
right.
   
   ### 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 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.
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   Generated-by: Claude Code
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to