Skip to content

Commit 0b913a8

Browse files
committed
[SPARK-46367][SQL] Support narrowing projection of KeyedPartitioning in PartitioningPreservingUnaryExecNode
### What changes were proposed in this pull request? When a `KeyedPartitioning` passes through a `PartitioningPreservingUnaryExecNode` (e.g. `ProjectExec`), the previous implementation projected the partitioning as a whole expression via `multiTransformDown`. If any expression position could not be mapped to an output attribute, the entire `KeyedPartitioning` was silently dropped, resulting in `UnknownPartitioning`. This PR replaces that approach with a per-position projection algorithm implemented in two new private helpers (`projectKeyedPartitionings` and `projectOtherPartitionings`), with the main `outputPartitioning` reduced to a simple split, project, and combine: 1. For each expression position (0..N-1), collect the unique expressions at that position across all input `KeyedPartitioning`s (using `ExpressionSet` to deduplicate semantically equal expressions), then project each through the output aliases via `projectExpression`. 2. Positions with at least one projected alternative are *projectable*; they define the maximum achievable granularity. Positions that cannot be expressed in the output are dropped (narrowing). 3. The shared `partitionKeys` are projected to the subset of projectable positions via `KeyedPartitioning.projectKeys`. 4. The final `KeyedPartitioning`s are the cross-product of per-position alternatives, computed lazily via `MultiTransform.generateCartesianProduct`, deduplicated, and bounded by a single outer `take(aliasCandidateLimit)`. All resulting `KeyedPartitioning`s at the same granularity share the same `partitionKeys` object, preserving the invariant required by `GroupPartitionsExec`. A new `isNarrowed: Boolean` flag is added to `KeyedPartitioning` and set to `true` when the projection drops one or more key positions. The flag is sticky: a subsequent `PartitioningPreservingUnaryExecNode` that passes all positions through does not reset it. When `isNarrowed=true` and `isGrouped=false`, `GroupPartitionsExec` would merge original partitions that held distinct keys, carrying the same data-skew risk as `allowKeysSubsetOfPartitionKeys`. `groupedSatisfies` therefore gates such narrowed partitionings behind that config. When `isGrouped=true` after narrowing, the projected keys are still distinct so no merging happens and no config is required. The config `spark.sql.sources.v2.bucketing.allowJoinKeysSubsetOfPartitionKeys.enabled` is renamed to `spark.sql.sources.v2.bucketing.allowKeysSubsetOfPartitionKeys.enabled` to reflect that it now covers both joins and aggregates. The old key is retained as an alternative for backwards compatibility. Note: this PR does not handle the case where a partition key column is pruned from the scan output by column pruning. `V2ScanPartitioningAndOrdering` drops the `KeyedPartitioning` entirely when any partition key expression references a column absent from the scan's output set, so there is nothing for `PartitioningPreservingUnaryExecNode` to project. Addressing that case requires changes at the logical planning level and is left for a follow-up. ### Why are the changes needed? Without this fix, a `ProjectExec` that drops any column of a multi-column partition key causes the entire `KeyedPartitioning` to be lost. This breaks storage-partitioned join optimisations (SPJ) that rely on the partitioning surviving projection (e.g. a subquery that renames or projects away a partition key column). ### Does this PR introduce _any_ user-facing change? Yes. SPJ is now preserved through `ProjectExec` nodes: - Alias projections (e.g. `SELECT id AS pk FROM t`) no longer break SPJ. - Narrowing projections (e.g. `SELECT id FROM t` where `t` is partitioned by `(id, name)`) enable SPJ when the projected keys remain distinct, or when `spark.sql.sources.v2.bucketing.allowKeysSubsetOfPartitionKeys.enabled` is set and the keys become non-unique. The config key `spark.sql.sources.v2.bucketing.allowJoinKeysSubsetOfPartitionKeys.enabled` is renamed to `spark.sql.sources.v2.bucketing.allowKeysSubsetOfPartitionKeys.enabled`; the old key continues to work via `.withAlternative`. ### How was this patch tested? Unit tests added/updated in `ProjectedOrderingAndPartitioningSuite`: - Full-granularity alias substitution - 2->1 and 3->2 narrowing with and without aliases - `PartitioningCollection` with mixed projectability - `isNarrowed=true, isGrouped=false`: `groupedSatisfies` blocked without config, allowed with `allowKeysSubsetOfPartitionKeys` - `isNarrowed=true, isGrouped=true`: `satisfies` succeeds without config - `isNarrowed` stickiness: a second `PartitioningPreservingUnaryExecNode` hop preserves the flag End-to-end tests added in `KeyGroupedPartitioningSuite`: - Alias in subquery does not break SPJ - Narrowing projection with duplicate projected keys requires `allowKeysSubsetOfPartitionKeys` - Narrowing projection with distinct projected keys triggers SPJ without config - Aggregate with GROUP BY on a subset of partition keys: shuffle by default, `GroupPartitionsExec` with `allowKeysSubsetOfPartitionKeys` ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Sonnet 4.6
1 parent 68456a6 commit 0b913a8

9 files changed

Lines changed: 567 additions & 83 deletions

File tree

sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -353,16 +353,18 @@ case class CoalescedHashPartitioning(from: HashPartitioning, partitions: Seq[Coa
353353
* `KeyedPartitioning` is used in two distinct forms:
354354
*
355355
* 1. '''As outputPartitioning''': When used as a node's output partitioning (e.g., in
356-
* `BatchScanExec` or `GroupPartitionsExec`), the `partitionKeys` are always in sorted order.
357-
* This is how leaf data source nodes produce partition keys originally, and this ordering is
358-
* preserved through `GroupPartitionsExec`. The sorted order is critical for storage-partitioned
359-
* join compatibility.
356+
* `BatchScanExec` or `GroupPartitionsExec`), the `partitionKeys` are typically in sorted order
357+
* because data sources produce them that way and `GroupPartitionsExec` sorts while grouping.
358+
* Sorted order is not a hard requirement, but it is a useful property: when both sides of a
359+
* storage-partitioned join report sorted keys, `EnsureRequirements` can often match them
360+
* without inserting an additional `GroupPartitionsExec`. After a narrowing projection through
361+
* `PartitioningPreservingUnaryExecNode`, the projected keys may no longer be sorted; this is
362+
* acceptable because `EnsureRequirements` can always reconcile both sides via
363+
* `GroupPartitionsExec` with `expectedPartitionKeys`.
360364
*
361-
* 2. '''In KeyedShuffleSpec''': When used within `KeyedShuffleSpec`, the `partitionKeys` may not be
362-
* in sorted order. This occurs because `KeyedShuffleSpec` can project the partition keys by join
363-
* key positions. The `EnsureRequirements` rule ensures that either the unordered keys from both
364-
* sides of a join match exactly, or it builds a common ordered set of keys and pushes them down
365-
* to `GroupPartitionsExec` on both sides to establish a compatible ordering.
365+
* 2. '''In KeyedShuffleSpec''': When used within `KeyedShuffleSpec`, the `partitionKeys` may not
366+
* be in sorted order. `EnsureRequirements` handles this by building a common ordered set of
367+
* keys and pushing them down to `GroupPartitionsExec` on both sides.
366368
*
367369
* == Partition Keys ==
368370
* - `partitionKeys`: The partition keys, one per partition. May contain duplicates initially
@@ -417,16 +419,23 @@ case class CoalescedHashPartitioning(from: HashPartitioning, partitions: Seq[Coa
417419
*
418420
* @param expressions Partition transform expressions (e.g., `years(col)`, `bucket(10, col)`).
419421
* @param partitionKeys Partition keys wrapped in InternalRowComparableWrapper for efficient
420-
* comparison and grouping. One per partition. When used as outputPartitioning,
421-
* always in sorted order. When used in `KeyedShuffleSpec`, may be unsorted
422-
* after projection. May contain duplicates when ungrouped.
422+
* comparison and grouping. One per partition. Typically in sorted order when
423+
* produced by a data source or `GroupPartitionsExec`, but this is not
424+
* guaranteed after projection. May contain duplicates when ungrouped.
423425
* @param isGrouped Whether partition keys are unique (no duplicates). Computed on first
424426
* creation, then preserved through copy operations to avoid recomputation.
427+
* @param isNarrowed Whether this partitioning was derived from a finer-grained one by dropping key
428+
* positions (e.g. via `PartitioningPreservingUnaryExecNode`). When true,
429+
* `GroupPartitionsExec` will merge partitions that shared distinct keys in the
430+
* original partitioning, carrying the same skew risk as
431+
* `allowKeysSubsetOfPartitionKeys`. Such a partitioning will not satisfy
432+
* `ClusteredDistribution` unless that config is enabled.
425433
*/
426434
case class KeyedPartitioning(
427435
expressions: Seq[Expression],
428436
@transient partitionKeys: Seq[InternalRowComparableWrapper],
429-
isGrouped: Boolean) extends Expression with Partitioning with Unevaluable {
437+
isGrouped: Boolean,
438+
isNarrowed: Boolean = false) extends Expression with Partitioning with Unevaluable {
430439
override val numPartitions = partitionKeys.length
431440

432441
override def children: Seq[Expression] = expressions
@@ -480,13 +489,19 @@ case class KeyedPartitioning(
480489
c.areAllClusterKeysMatched(expressions)
481490
} else {
482491
// We'll need to find leaf attributes from the partition expressions first.
483-
val attributes = expressions.flatMap(_.collectLeaves())
492+
lazy val attributes = expressions.flatMap(_.collectLeaves())
484493

485-
if (SQLConf.get.v2BucketingAllowJoinKeysSubsetOfPartitionKeys) {
486-
// check that join keys (required clustering keys)
494+
if (SQLConf.get.v2BucketingAllowKeysSubsetOfPartitionKeys) {
495+
// check that operation keys (required clustering keys)
487496
// overlap with partition keys (KeyedPartitioning attributes)
488497
requiredClustering.exists(x => attributes.exists(_.semanticEquals(x))) &&
489498
expressions.forall(_.collectLeaves().size == 1)
499+
} else if (isNarrowed && !isGrouped) {
500+
// A narrowed, non-grouped partitioning carries the same skew risk as using a subset of
501+
// partition keys for a join: GroupPartitionsExec will merge partitions that held
502+
// distinct keys in the original finer-grained partitioning. Require the same config to
503+
// opt in.
504+
false
490505
} else {
491506
attributes.forall(x => requiredClustering.exists(_.semanticEquals(x)))
492507
}
@@ -502,9 +517,9 @@ case class KeyedPartitioning(
502517

503518
override def createShuffleSpec(distribution: ClusteredDistribution): ShuffleSpec = {
504519
val result = KeyedShuffleSpec(this, distribution)
505-
if (SQLConf.get.v2BucketingAllowJoinKeysSubsetOfPartitionKeys) {
506-
// If allowing join keys to be subset of clustering keys, we should create a new
507-
// `KeyedPartitioning` here that is grouped on the join keys instead, and use that as
520+
if (SQLConf.get.v2BucketingAllowKeysSubsetOfPartitionKeys) {
521+
// If allowing operation keys to be a subset of partition keys, create a new
522+
// `KeyedPartitioning` grouped on the operation keys, and use that as
508523
// the returned shuffle spec.
509524
val joinKeyPositions = result.keyPositions.map(_.nonEmpty).zipWithIndex.filter(_._1).map(_._2)
510525
val projectedExpressions = joinKeyPositions.map(expressions)

sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2142,15 +2142,18 @@ object SQLConf {
21422142
.booleanConf
21432143
.createWithDefault(false)
21442144

2145-
val V2_BUCKETING_ALLOW_JOIN_KEYS_SUBSET_OF_PARTITION_KEYS =
2146-
buildConf("spark.sql.sources.v2.bucketing.allowJoinKeysSubsetOfPartitionKeys.enabled")
2147-
.doc("Whether to allow storage-partition join in the case where join keys are " +
2148-
"a subset of the partition keys of the source tables. At planning time, " +
2149-
"Spark will group the partitions by only those keys that are in the join keys. " +
2145+
val V2_BUCKETING_ALLOW_KEYS_SUBSET_OF_PARTITION_KEYS =
2146+
buildConf("spark.sql.sources.v2.bucketing.allowKeysSubsetOfPartitionKeys.enabled")
2147+
.withAlternative("spark.sql.sources.v2.bucketing.allowJoinKeysSubsetOfPartitionKeys.enabled")
2148+
.doc("Whether to allow storage-partitioned operations (joins and aggregates) in the case " +
2149+
"where the operation's keys are a subset of the partition keys of the source tables. At " +
2150+
"planning time, Spark will group the partitions by only those keys that are in the " +
2151+
"operation's keys. " +
21502152
s"This is currently enabled only if ${REQUIRE_ALL_CLUSTER_KEYS_FOR_DISTRIBUTION.key} " +
21512153
"is false."
21522154
)
21532155
.version("4.0.0")
2156+
.withBindingPolicy(ConfigBindingPolicy.SESSION)
21542157
.booleanConf
21552158
.createWithDefault(false)
21562159

@@ -7926,8 +7929,8 @@ class SQLConf extends Serializable with Logging with SqlApiConf {
79267929
def v2BucketingShuffleEnabled: Boolean =
79277930
getConf(SQLConf.V2_BUCKETING_SHUFFLE_ENABLED)
79287931

7929-
def v2BucketingAllowJoinKeysSubsetOfPartitionKeys: Boolean =
7930-
getConf(SQLConf.V2_BUCKETING_ALLOW_JOIN_KEYS_SUBSET_OF_PARTITION_KEYS)
7932+
def v2BucketingAllowKeysSubsetOfPartitionKeys: Boolean =
7933+
getConf(SQLConf.V2_BUCKETING_ALLOW_KEYS_SUBSET_OF_PARTITION_KEYS)
79317934

79327935
def v2BucketingAllowCompatibleTransforms: Boolean =
79337936
getConf(SQLConf.V2_BUCKETING_ALLOW_COMPATIBLE_TRANSFORMS)

sql/core/src/main/scala/org/apache/spark/sql/execution/AliasAwareOutputExpression.scala

Lines changed: 100 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ package org.apache.spark.sql.execution
1818

1919
import scala.collection.mutable
2020

21-
import org.apache.spark.sql.catalyst.expressions.{AttributeSet, Expression}
21+
import org.apache.spark.sql.catalyst.expressions.{AttributeSet, Expression, ExpressionSet}
2222
import org.apache.spark.sql.catalyst.plans.{AliasAwareOutputExpression, AliasAwareQueryOutputOrdering}
23-
import org.apache.spark.sql.catalyst.plans.physical.{Partitioning, PartitioningCollection, UnknownPartitioning}
23+
import org.apache.spark.sql.catalyst.plans.physical.{KeyedPartitioning, Partitioning, PartitioningCollection, UnknownPartitioning}
24+
import org.apache.spark.sql.catalyst.trees.MultiTransform
2425

2526
/**
2627
* A trait that handles aliases in the `outputExpressions` to produce `outputPartitioning` that
@@ -29,8 +30,31 @@ import org.apache.spark.sql.catalyst.plans.physical.{Partitioning, PartitioningC
2930
trait PartitioningPreservingUnaryExecNode extends UnaryExecNode
3031
with AliasAwareOutputExpression {
3132
final override def outputPartitioning: Partitioning = {
32-
val partitionings: Seq[Partitioning] = if (hasAlias) {
33-
flattenPartitioning(child.outputPartitioning).iterator.flatMap {
33+
val (keyedPartitionings, otherPartitionings) =
34+
flattenPartitioning(child.outputPartitioning).partition(_.isInstanceOf[KeyedPartitioning])
35+
36+
val projectedKPs =
37+
projectKeyedPartitionings(keyedPartitionings.map(_.asInstanceOf[KeyedPartitioning]))
38+
val projectedOthers = projectOtherPartitionings(otherPartitionings)
39+
40+
(projectedKPs ++ projectedOthers).take(aliasCandidateLimit) match {
41+
case Seq() => UnknownPartitioning(child.outputPartitioning.numPartitions)
42+
case Seq(p) => p
43+
case ps => PartitioningCollection(ps)
44+
}
45+
}
46+
47+
/**
48+
* Projects non-[[KeyedPartitioning]] partitionings through the current node's output expressions.
49+
*
50+
* With aliases, each partitioning expression is substituted with all possible alias combinations;
51+
* without aliases, partitionings whose expressions reference attributes outside the output are
52+
* dropped.
53+
*/
54+
private def projectOtherPartitionings(
55+
partitionings: Seq[Partitioning]): LazyList[Partitioning] = {
56+
if (hasAlias) {
57+
partitionings.to(LazyList).flatMap {
3458
case e: Expression =>
3559
// We need unique partitionings but if the input partitioning is
3660
// `HashPartitioning(Seq(id + id))` and we have `id -> a` and `id -> b` aliases then after
@@ -41,23 +65,85 @@ trait PartitioningPreservingUnaryExecNode extends UnaryExecNode
4165
val partitioningSet = mutable.Set.empty[Expression]
4266
projectExpression(e)
4367
.filter(e => partitioningSet.add(e.canonicalized))
44-
.take(aliasCandidateLimit)
4568
.asInstanceOf[LazyList[Partitioning]]
46-
case o => Seq(o)
47-
}.take(aliasCandidateLimit).toSeq
69+
case o => LazyList(o)
70+
}
4871
} else {
49-
// Filter valid partitiongs (only reference output attributes of the current plan node)
72+
// Filter valid partitionings (only reference output attributes of the current plan node)
5073
val outputSet = AttributeSet(outputExpressions.map(_.toAttribute))
51-
flattenPartitioning(child.outputPartitioning).filter {
74+
partitionings.to(LazyList).filter {
5275
case e: Expression => e.references.subsetOf(outputSet)
5376
case _ => true
5477
}
5578
}
56-
partitionings match {
57-
case Seq() => UnknownPartitioning(child.outputPartitioning.numPartitions)
58-
case Seq(p) => p
59-
case ps => PartitioningCollection(ps)
60-
}
79+
}
80+
81+
/**
82+
* Projects all input [[KeyedPartitioning]]s through the current node's output expressions.
83+
*
84+
* For each expression position (0..N-1), collects the unique expressions at that position across
85+
* all input KPs, projects each through the output aliases, and unions the alternatives.
86+
* Positions with at least one alternative are projectable; their count determines the maximum
87+
* achievable granularity. Positions that cannot be expressed in the output are dropped.
88+
*
89+
* The resulting [[KeyedPartitioning]]s are the cross-product of the per-position alternatives
90+
* restricted to the projectable positions. All share the same `partitionKeys` object (projected
91+
* to the same subset of positions), preserving the invariant required by [[GroupPartitionsExec]].
92+
*/
93+
private def projectKeyedPartitionings(
94+
kps: Seq[KeyedPartitioning]): LazyList[KeyedPartitioning] = {
95+
if (kps.isEmpty) return LazyList.empty
96+
val numPositions = kps.head.expressions.length
97+
98+
val alternativesPerPosition: IndexedSeq[LazyList[Expression]] =
99+
if (hasAlias) {
100+
// For each position, gather unique expressions across all KPs (ExpressionSet deduplicates
101+
// semantically equal expressions, e.g. the same join-key column shared by both KP sides)
102+
// and project each through the output aliases.
103+
(0 until numPositions).map { i =>
104+
val seen = mutable.Set.empty[Expression]
105+
ExpressionSet(kps.map(_.expressions(i))).to(LazyList).flatMap { expr =>
106+
projectExpression(expr).filter(e => seen.add(e.canonicalized))
107+
}
108+
}
109+
} else {
110+
// No aliases: filter out non-projectable expressions first, then deduplicate.
111+
val outputSet = AttributeSet(outputExpressions.map(_.toAttribute))
112+
(0 until numPositions).map { i =>
113+
ExpressionSet(kps.collect {
114+
case kp if kp.expressions(i).references.subsetOf(outputSet) => kp.expressions(i)
115+
}).to(LazyList)
116+
}
117+
}
118+
119+
// Non-empty positions define the maximum achievable granularity.
120+
val projectablePositions =
121+
(0 until numPositions).filter(i => alternativesPerPosition(i).nonEmpty)
122+
123+
if (projectablePositions.isEmpty) return LazyList.empty
124+
125+
// All input KPs share the same partitionKeys by invariant; use the first as the key source.
126+
val keySource = kps.head
127+
val sharedKeys =
128+
if (projectablePositions.length == numPositions) keySource.partitionKeys
129+
else keySource.projectKeys(projectablePositions)._2
130+
131+
val isGrouped = sharedKeys.distinct.size == sharedKeys.size
132+
// A KP is narrowed if this node drops positions, or if the input KPs were already narrowed
133+
// (i.e. came from a finer-grained partitioning). The flag must be sticky: a subsequent
134+
// PartitioningPreservingUnaryExecNode that passes all positions through would otherwise
135+
// recompute isNarrowed=false, silently dropping the protection.
136+
val isNarrowed = projectablePositions.length < numPositions || keySource.isNarrowed
137+
138+
// Cross-product the per-position alternatives to produce all concrete KPs.
139+
// Note: generateCartesianProduct expects thunks () => Seq[T], but wrapping LazyLists in thunks
140+
// here is not strictly necessary since they are already lazy -- we do it only to match the API.
141+
// No deduplication is needed here: per-position alternatives are already canonically distinct,
142+
// so all cross-product combinations are distinct by construction.
143+
MultiTransform.generateCartesianProduct(
144+
projectablePositions.map(i => () => alternativesPerPosition(i)))
145+
.map(projectedExprs =>
146+
new KeyedPartitioning(projectedExprs, sharedKeys, isGrouped, isNarrowed))
61147
}
62148

63149
private def flattenPartitioning(partitioning: Partitioning): Seq[Partitioning] = {

sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/EnsureRequirements.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -408,12 +408,12 @@ case class EnsureRequirements(
408408
reorder(leftKeys.toIndexedSeq, rightKeys.toIndexedSeq, rightExpressions, rightKeys)
409409
.orElse(reorderJoinKeysRecursively(
410410
leftKeys, rightKeys, leftPartitioning, None))
411-
case (Some(KeyedPartitioning(clustering, _, _)), _) =>
411+
case (Some(KeyedPartitioning(clustering, _, _, _)), _) =>
412412
val leafExprs = clustering.flatMap(_.collectLeaves())
413413
reorder(leftKeys.toIndexedSeq, rightKeys.toIndexedSeq, leafExprs, leftKeys)
414414
.orElse(reorderJoinKeysRecursively(
415415
leftKeys, rightKeys, None, rightPartitioning))
416-
case (_, Some(KeyedPartitioning(clustering, _, _))) =>
416+
case (_, Some(KeyedPartitioning(clustering, _, _, _))) =>
417417
val leafExprs = clustering.flatMap(_.collectLeaves())
418418
reorder(leftKeys.toIndexedSeq, rightKeys.toIndexedSeq, leafExprs, rightKeys)
419419
.orElse(reorderJoinKeysRecursively(
@@ -512,7 +512,7 @@ case class EnsureRequirements(
512512
leftSpec.isCompatibleWith(rightSpec)
513513
if ((!isCompatible || conf.v2BucketingPartiallyClusteredDistributionEnabled) &&
514514
(conf.v2BucketingPushPartValuesEnabled ||
515-
conf.v2BucketingAllowJoinKeysSubsetOfPartitionKeys)) {
515+
conf.v2BucketingAllowKeysSubsetOfPartitionKeys)) {
516516
logInfo("Pushing common partition values for storage-partitioned join")
517517
isCompatible = leftSpec.areKeysCompatible(rightSpec)
518518

sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/ShuffleExchangeExec.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ object ShuffleExchangeExec {
407407
val projection = UnsafeProjection.create(sortingExpressions.map(_.child), outputAttributes)
408408
row => projection(row)
409409
case SinglePartition => identity
410-
case KeyedPartitioning(expressions, _, _) =>
410+
case KeyedPartitioning(expressions, _, _, _) =>
411411
row => bindReferences(expressions, outputAttributes).map(_.eval(row))
412412
case s: ShufflePartitionIdPassThrough =>
413413
// For ShufflePartitionIdPassThrough, the expression directly evaluates to the partition ID

sql/core/src/test/scala/org/apache/spark/sql/connector/DistributionAndOrderingSuiteBase.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ abstract class DistributionAndOrderingSuiteBase
5050
plan: QueryPlan[T]): Partitioning = partitioning match {
5151
case HashPartitioning(exprs, numPartitions) =>
5252
HashPartitioning(exprs.map(resolveAttrs(_, plan)), numPartitions)
53-
case KeyedPartitioning(expressions, partitionKeys, isGrouped) =>
53+
case KeyedPartitioning(expressions, partitionKeys, isGrouped, _) =>
5454
KeyedPartitioning(expressions.map(resolveAttrs(_, plan)), partitionKeys, isGrouped)
5555
case PartitioningCollection(partitionings) =>
5656
PartitioningCollection(partitionings.map(resolvePartitioning(_, plan)))

0 commit comments

Comments
 (0)