Skip to content

Commit 8ee49ca

Browse files
committed
[SPARK-59285][SQL] Hold a KeyedPartitioning's shared partition layout in one value
### What changes were proposed in this pull request? `KeyedPartitioning` grows a `KeyLayout`, and everything its members share moves into it. **One value for what a partitioning's members share.** `KeyLayout(partitionKeys, dataTypes, isGrouped, isCollapsed, mayContainUnknownPartitionKeys)` holds everything about the partitions a `KeyedPartitioning` describes except the expressions naming them, so `KeyedPartitioning` is `(expressions, layout)`. The members of a `PartitioningCollection` name one layout with their own expressions and share the object by reference, so: - the collection's invariant is one `eq` on the layout in place of a clause per shared field, and it now covers `isGrouped`, which the field-by-field check left out; - `fromPartitionings` merges one canonical layout instead of interning the keys and ORing two flags, and refuses a member that describes another key space, which interning would otherwise retype; - `KeyedShuffleSpec.createPartitioning` has one thing to decide rather than three, the unknown-keys marker it must set; - `GroupPartitionsExec`'s `PartitionGrouping` is the layout it will report plus the child partitions each of its own is built from. **One derivation of a key schema's ordering.** `InternalRowComparableWrapper`'s `Factory` also answers with the `ordering` it holds, and `KeyedPartitioning.apply` takes a `sortKeys` flag that sorts with it. `DataSourceV2ScanExecBase` used to derive an ordering of its own to sort the splits and then hand the keys to `apply`, which derived the type list and the ordering again. It now passes `sortKeys = true`, so one factory answers for the types, the ordering and the wrappers. That is dongjoon-hyun's point on #58523 (#58523 (comment)), which needed this PR's shape to fix cleanly. **One answer for the key types, including where no key row is left.** `keyDataTypes` reads the layout rather than sampling the first key row and falling back to the partition expressions. A layout is given the types its keys were built at, at the four places one is built. `EnsureRequirements` therefore drops the exception SPARK-59176 added for a side with no key row, since the layout answers for it. ### Why are the changes needed? Two things, one structural and one a defect the structure hides. **The shared part of a `KeyedPartitioning` is four fields that every member of a collection has to agree on by hand.** `checkKeyedPartitioningInvariant` compares them clause by clause, and it left `isGrouped` out. Four places put a partitioning's expressions over keys they did not build, and each has to carry the shared fields forward correctly: 1. `GroupPartitionsExec.outputPartitioning` reports the keys `EnsureRequirements` merged, and picks its member with `collectFirst`, which need not be the member the planner merged from. 2. `PartitioningPreservingUnaryExecNode.projectKeyedPartitionings` projects `kps.head` once and stamps every alias alternative onto it with `copy(expressions = ...)`. 3. `KeyedShuffleSpec.createPartitioning` puts the other child's expressions over these keys. 4. `KeyedPartitioning.concat`, for a `UnionExec`. Sharing one layout by reference is what makes all four correct rather than merely lucky, and it turns the invariant into one `eq`. It also makes the field count stop mattering: SPARK-59050's `mayContainUnknownPartitionKeys` is the fifth shared field, and with the layout it is one more `copy` argument rather than one more clause in the invariant, one more OR in `fromPartitionings` and one more argument at every copy site. **A side with no key row answers from its partition expressions, and after a both-sides reduce that is a type no key of it holds.** The reduce leaves keys that are `r1(f1(x))` = `r2(f2(x))`, a space neither transform names, so the reported expression is marked and its own type is the un-reduced one. SPARK-59176 worked around it by leaving such a side out of the co-partition type check, which left the check not checking for the shape most likely to need it, and every other reader of `keyDataTypes` still getting the wrong answer. The layout now carries what the reduce produced, so the workaround goes. The types are on the layout rather than derived, because a partitioning whose partitions were all pruned has no row to read them off and its expressions do not describe a reduced key space. They are on the *layout* rather than on `KeyedPartitioning`, because that is what makes them shared: two members that share a layout share its keys, so the collection's `eq` covers them, and no consumer can pair one member's rows with another member's types. Two alternatives were tried and dropped. An independent `keyDataTypes` field on `KeyedPartitioning` has to be decided at each of the four sites above, and two of them mix members, which is how it produced two reachable regressions in review. A `TypedKeys(dataTypes, keys)` value object does not settle it either, since two members can still hold two different pairs. Sharing by reference is a constraint as well as a convenience, and one site had to change for it. `ShuffledJoin`'s marker clearing used to rewrite each member on its own; with the layout that would give each a different one, so it now builds one cleared layout for the whole input. The scan sorts through `apply` rather than sorting before it, because the ordering it wants is the one its keys are compared at. Deriving it separately worked only because a generated comparator is name-blind, so the raw and the erased type lists happen to give the same order. Taking it off the factory makes that an identity rather than a coincidence. No plan string changes. `KeyedPartitioning.stringArgs` prints the layout's contents where the value object would print, and deliberately leaves the key types out of that list: they have their field names, nullability and metadata erased (SPARK-59187), so printing them would put a struct field named `0` into a plan that appears nowhere in the query. ### Does this PR introduce _any_ user-facing change? No. It adds no behaviour of its own beyond making a pruned side report its own key types truthfully. ### How was this patch tested? Four new tests, plus SPARK-59176's two existing ones, which now pass with its exception removed. Ablation: with the exception removed and `keyDataTypes` derived from the rows and expressions again, "SPARK-59176: a leg reduced onto no key at all still joins" fails with the error SPARK-59176 was filed for. - `DistributionSuite`, "fromPartitionings refuses a member that disagrees on isGrouped", for the layout itself. - `GroupPartitionsExecSuite`, "a reduced key space's type reaches the reported partitioning with no key left": a both-sides reduce onto `LongType` under a `DateType` transform, with keys and without. - `KeyGroupedPartitioningSuite`, "two sides whose partitions were all pruned are not one layout": two legs pruned to nothing, one `identity(id)` on `LongType` and one `bucket(4, id)` on `IntegerType`, joined and then joined again through a FULL OUTER that brings real keys in. It asserts that no node reports two key spaces as one layout, that the plan passes `ValidateRequirements`, and the answer. - `KeyGroupedPartitioningSuite`, "two legs whose struct field names differ are still co-partitioned", the shape where two exact type lists differ while the space does not. SPARK-59050's two collection tests now assert on the layout reference rather than on a marker clause of their own, which is the same guarantee reached by the structure instead of by a check. `KeyGroupedPartitioningSuite`, `KeyGroupedPartitioningRuntimeFilterSuite`, `GroupPartitionsExecSuite`, `EnsureRequirementsSuite`, `ProjectedOrderingAndPartitioningSuite`, `DataSourceV2CatalystRuntimeFilterSuite`, `PlannerSuite`, `DataFrameSetOperationsSuite`, `DistributionSuite`, `ShuffleSpecSuite`, `TransformExpressionSuite` and `InternalRowComparableWrapperSuite`, `DataSourceV2Suite`, 561 tests. Scalastyle and scalafmt clean. ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 5)
1 parent b22b611 commit 8ee49ca

14 files changed

Lines changed: 454 additions & 252 deletions

File tree

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

Lines changed: 197 additions & 130 deletions
Large diffs are not rendered by default.

sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/DistributionSuite.scala

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ class DistributionSuite extends SparkFunSuite with SQLHelper {
446446

447447
val combined = PartitioningCollection.fromPartitionings(Seq(nested, kpY))
448448
val interned = combined.partitionings.last.asInstanceOf[KeyedPartitioning]
449-
assert(interned.partitionKeys eq kpX.partitionKeys)
449+
assert(interned.layout eq kpX.layout, "the whole layout is interned, keys and all")
450450
}
451451

452452
test("SPARK-59050: a marked one-partition layout keeps the global ordering claim") {
@@ -457,9 +457,9 @@ class DistributionSuite extends SparkFunSuite with SQLHelper {
457457
val a = AttributeReference("a", IntegerType)()
458458
val ordered = OrderedDistribution(Seq(SortOrder(a, Ascending)))
459459
val markedOne = KeyedPartitioning(Seq(a), Seq(InternalRow(1)))
460-
.copy(mayContainUnknownPartitionKeys = true)
460+
.withLayout(_.copy(mayContainUnknownPartitionKeys = true))
461461
val markedTwo = KeyedPartitioning(Seq(a), Seq(InternalRow(1), InternalRow(2)))
462-
.copy(mayContainUnknownPartitionKeys = true)
462+
.withLayout(_.copy(mayContainUnknownPartitionKeys = true))
463463
withSQLConf(SQLConf.V2_BUCKETING_SORTING_ENABLED.key -> "true") {
464464
checkSatisfied(markedOne, ordered, true)
465465
checkSatisfied(markedTwo, ordered, false)
@@ -470,7 +470,7 @@ class DistributionSuite extends SparkFunSuite with SQLHelper {
470470
val x = AttributeReference("x", IntegerType)()
471471
val y = AttributeReference("y", IntegerType)()
472472
val marked = KeyedPartitioning(Seq(x), Seq(InternalRow(1), InternalRow(2)))
473-
.copy(mayContainUnknownPartitionKeys = true)
473+
.withLayout(_.copy(mayContainUnknownPartitionKeys = true))
474474
val plain = KeyedPartitioning(Seq(y), Seq(InternalRow(1), InternalRow(2)))
475475
val combined = PartitioningCollection.fromPartitionings(Seq(marked, plain))
476476
// The conservative direction: an unmarked member must never excuse marked data, because
@@ -485,21 +485,23 @@ class DistributionSuite extends SparkFunSuite with SQLHelper {
485485
val y = AttributeReference("y", IntegerType)()
486486
val keys = Seq(InternalRow(1), InternalRow(2))
487487
val base = KeyedPartitioning(Seq(x), keys)
488-
val marked = base.copy(mayContainUnknownPartitionKeys = true)
489-
// Same keys reference, arity and isCollapsed, only the marker disagrees: it has to reach
490-
// the marker require rather than the reference check ahead of it.
491-
val disagree = marked.copy(expressions = Seq(y), mayContainUnknownPartitionKeys = false)
488+
val marked = base.withLayout(_.copy(mayContainUnknownPartitionKeys = true))
489+
// The marker lives on the layout, so two members that disagree on it hold two layouts, and the
490+
// one `eq` on the layout refuses them. There is no clause of its own to reach.
491+
val disagree =
492+
KeyedPartitioning(Seq(y), marked.layout.copy(mayContainUnknownPartitionKeys = false))
492493
val err = intercept[IllegalArgumentException] {
493494
PartitioningCollection(Seq(marked, disagree))
494495
}
495-
assert(err.getMessage.contains("agree on mayContainUnknownPartitionKeys"))
496+
assert(err.getMessage.contains("share the same KeyLayout reference"))
496497
}
497498

498499
test("SPARK-59050: fromPartitionings normalizes the unknown-keys marker through nesting") {
499500
val x = AttributeReference("x", IntegerType)()
500501
val y = AttributeReference("y", IntegerType)()
501502
val keys = Seq(InternalRow(1), InternalRow(2))
502-
val marked = KeyedPartitioning(Seq(x), keys).copy(mayContainUnknownPartitionKeys = true)
503+
val marked =
504+
KeyedPartitioning(Seq(x), keys).withLayout(_.copy(mayContainUnknownPartitionKeys = true))
503505
val plainNested = PartitioningCollection.fromPartitionings(
504506
Seq(KeyedPartitioning(Seq(y), keys)))
505507
val combined = PartitioningCollection.fromPartitionings(Seq(marked, plainNested))
@@ -509,8 +511,8 @@ class DistributionSuite extends SparkFunSuite with SQLHelper {
509511
.collect { case k: KeyedPartitioning => k }
510512
assert(leaves.length === 2, combined.toString)
511513
assert(leaves.forall(_.mayContainUnknownPartitionKeys), leaves.toString)
512-
assert(leaves.forall(_.partitionKeys eq leaves.head.partitionKeys),
513-
"the interned keys must survive the rebuild")
514+
assert(leaves.forall(_.layout eq leaves.head.layout),
515+
"the interned layout must survive the rebuild")
514516
}
515517

516518
test("SPARK-59050: PartitioningCollection requires a nested collection to agree on the " +
@@ -520,15 +522,15 @@ class DistributionSuite extends SparkFunSuite with SQLHelper {
520522
val keys = Seq(InternalRow(1), InternalRow(2))
521523
val base = KeyedPartitioning(Seq(x), keys)
522524
val markedNested = PartitioningCollection.fromPartitionings(Seq(
523-
base.copy(mayContainUnknownPartitionKeys = true)))
525+
base.withLayout(_.copy(mayContainUnknownPartitionKeys = true))))
524526
val plain = base.copy(expressions = Seq(y))
525-
// The nested collection is internally uniform, so its own construction passes; the outer
526-
// constructor must still catch its representative against the unmarked sibling. Same keys
527-
// reference, arity and isCollapsed, only the marker disagrees.
527+
// The nested collection is internally uniform, so its own construction passes. The outer
528+
// constructor must still catch its representative against the unmarked sibling, and the marker
529+
// living on the layout is what makes that one `eq` enough.
528530
val err = intercept[IllegalArgumentException] {
529531
PartitioningCollection(Seq(markedNested, plain))
530532
}
531-
assert(err.getMessage.contains("agree on mayContainUnknownPartitionKeys"))
533+
assert(err.getMessage.contains("share the same KeyLayout reference"))
532534
}
533535

534536
test("SPARK-56877: PartitioningCollection enforces the invariant through nesting") {
@@ -542,7 +544,7 @@ class DistributionSuite extends SparkFunSuite with SQLHelper {
542544
val refMismatch = intercept[IllegalArgumentException] {
543545
PartitioningCollection(Seq(nested, kpY))
544546
}
545-
assert(refMismatch.getMessage.contains("share the same partitionKeys reference"))
547+
assert(refMismatch.getMessage.contains("share the same KeyLayout reference"))
546548

547549
val kpXY = KeyedPartitioning(Seq(x, y), Seq(InternalRow(1, 1), InternalRow(2, 2)))
548550
val arityMismatch = intercept[IllegalArgumentException] {
@@ -551,12 +553,28 @@ class DistributionSuite extends SparkFunSuite with SQLHelper {
551553
assert(arityMismatch.getMessage.contains("matching expression arity"))
552554
}
553555

556+
test("SPARK-59285: fromPartitionings refuses a member that disagrees on isGrouped") {
557+
// Interning now replaces a member's layout whole, where it used to keep each member's own
558+
// `isGrouped`. Whether the keys are unique is a property of the keys, so a member that
559+
// disagrees about it over equal keys is wrong, and interning would hide that.
560+
val x = AttributeReference("x", IntegerType)()
561+
val y = AttributeReference("y", IntegerType)()
562+
val keys = Seq(InternalRow(1), InternalRow(2))
563+
564+
val kpX = KeyedPartitioning(Seq(x), keys)
565+
val ungrouped = KeyedPartitioning(Seq(y), keys).withLayout(_.copy(isGrouped = false))
566+
val mismatch = intercept[IllegalArgumentException] {
567+
PartitioningCollection.fromPartitionings(Seq(kpX, ungrouped))
568+
}
569+
assert(mismatch.getMessage.contains("must agree on isGrouped"))
570+
}
571+
554572
test("SPARK-59057: toGrouped and KeyedShuffleSpec.createPartitioning keep isCollapsed sticky") {
555573
val x = AttributeReference("x", IntegerType)()
556574
val y = AttributeReference("y", IntegerType)()
557575

558576
val collapsedKP = KeyedPartitioning(Seq(x), Seq(InternalRow(1), InternalRow(1), InternalRow(2)))
559-
.copy(isCollapsed = true)
577+
.withLayout(_.copy(isCollapsed = true))
560578
assert(collapsedKP.toGrouped.isCollapsed, "toGrouped must keep isCollapsed sticky")
561579

562580
val spec = KeyedShuffleSpec(collapsedKP, ClusteredDistribution(Seq(x)))

sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/ShuffleSpecSuite.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@ class ShuffleSpecSuite extends SparkFunSuite with SQLHelper {
547547
keys: Seq[Int],
548548
hasUnknown: Boolean = false): KeyedShuffleSpec = KeyedShuffleSpec(
549549
KeyedPartitioning(Seq(a), keys.map(k => InternalRow(k)))
550-
.copy(mayContainUnknownPartitionKeys = hasUnknown), distribution)
550+
.withLayout(_.copy(mayContainUnknownPartitionKeys = hasUnknown)), distribution)
551551

552552
// A partitioning with unknown partition keys (e.g. a side re-shuffled onto a keyed layout by
553553
// `KeyedShuffleSpec.createPartitioning`) only guarantees co-location for its declared keys, so
@@ -602,12 +602,12 @@ class ShuffleSpecSuite extends SparkFunSuite with SQLHelper {
602602
keys: Seq[Long],
603603
hasUnknown: Boolean = false): KeyedShuffleSpec = KeyedShuffleSpec(
604604
KeyedPartitioning(Seq(bucket(4, a)), keys.map(k => InternalRow(k)))
605-
.copy(mayContainUnknownPartitionKeys = hasUnknown), distribution)
605+
.withLayout(_.copy(mayContainUnknownPartitionKeys = hasUnknown)), distribution)
606606
def keyedSpec(
607607
keys: Seq[Long],
608608
hasUnknown: Boolean = false): KeyedShuffleSpec = KeyedShuffleSpec(
609609
KeyedPartitioning(Seq(a), keys.map(k => InternalRow(k)))
610-
.copy(mayContainUnknownPartitionKeys = hasUnknown), distribution)
610+
.withLayout(_.copy(mayContainUnknownPartitionKeys = hasUnknown)), distribution)
611611

612612
// `isExpressionCompatible` admits an identity-vs-transform pair when compatible transforms
613613
// are allowed, but then the two sides' partition keys live in different domains: raw values
@@ -636,7 +636,7 @@ class ShuffleSpecSuite extends SparkFunSuite with SQLHelper {
636636
val a = $"a".int
637637
val b = $"b".int
638638
val marked = KeyedPartitioning(Seq(a, b), Seq(InternalRow(1, 2), InternalRow(3, 4)))
639-
.copy(mayContainUnknownPartitionKeys = true)
639+
.withLayout(_.copy(mayContainUnknownPartitionKeys = true))
640640
withSQLConf(
641641
SQLConf.V2_BUCKETING_SHUFFLE_ENABLED.key -> "true",
642642
SQLConf.V2_BUCKETING_ALLOW_KEYS_SUBSET_OF_PARTITION_KEYS.key -> "true") {
@@ -682,7 +682,7 @@ class ShuffleSpecSuite extends SparkFunSuite with SQLHelper {
682682
KeyedPartitioning(
683683
Seq(TransformExpression(fn, Seq(a), Some(numBuckets))),
684684
Seq(InternalRow(0L), InternalRow(1L)))
685-
.copy(mayContainUnknownPartitionKeys = hasUnknown),
685+
.withLayout(_.copy(mayContainUnknownPartitionKeys = hasUnknown)),
686686
ClusteredDistribution(Seq(a)))
687687

688688
// `allowCompatibleTransforms` lets a differing-bucket-count pair through

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

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,15 @@ trait PartitioningPreservingUnaryExecNode extends UnaryExecNode
9898
* achievable granularity. Positions that cannot be expressed in the output are dropped.
9999
*
100100
* The resulting [[KeyedPartitioning]]s are the cross-product of the per-position alternatives
101-
* restricted to the projectable positions. All share the same `partitionKeys` object (projected
102-
* to the same subset of positions), preserving the invariant required by
101+
* restricted to the projectable positions. All share the same `KeyLayout` object (projected to
102+
* the same subset of positions), preserving the invariant required by
103103
* [[PartitioningCollection]].
104104
*/
105105
private def projectKeyedPartitionings(
106106
kps: Seq[KeyedPartitioning]): LazyList[KeyedPartitioning] = {
107107
if (kps.isEmpty) return LazyList.empty
108-
// All input KPs share the same `partitionKeys` reference and matching arity by the
109-
// [[PartitioningCollection]] invariant (the only producer of multi-KP inputs here).
108+
// All input KPs have matching arity by the [[PartitioningCollection]] invariant (the only
109+
// producer of multi-KP inputs here).
110110
val numPositions = kps.head.expressions.length
111111

112112
val alternativesPerPosition: IndexedSeq[LazyList[Expression]] =
@@ -136,20 +136,16 @@ trait PartitioningPreservingUnaryExecNode extends UnaryExecNode
136136

137137
if (projectablePositions.isEmpty) return LazyList.empty
138138

139-
// `PartitioningCollection` requires its members to agree on the marker, so the head
140-
// represents them all.
141-
val mayContainUnknownPartitionKeys = kps.head.mayContainUnknownPartitionKeys
142-
143139
// Dropping a key position coarsens the declared set, which an unknown-keyed claim cannot
144-
// survive.
145-
if (projectablePositions.length < numPositions && mayContainUnknownPartitionKeys) {
140+
// survive. The members share one layout, so the head answers for the marker.
141+
if (projectablePositions.length < numPositions && kps.head.mayContainUnknownPartitionKeys) {
146142
return LazyList.empty
147143
}
148144

149-
// All input KPs share the same partitionKeys and flags by invariant, so the first one
150-
// projects the keys for every combination below; only the expressions differ. The marker
151-
// rides the copies unchanged: the guard above turned away the one shape that could not, a
152-
// narrowing projection of a marked collection.
145+
// All input KPs share one layout by invariant, so the first one projects it for every
146+
// combination below. Only the expressions differ, and the marker rides the copies unchanged:
147+
// the guard above turned away the one shape that could not, a narrowing projection of a marked
148+
// collection.
153149
val projected = kps.head.project(projectablePositions)
154150

155151
// Cross-product the per-position alternatives to produce all concrete KPs.

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -952,12 +952,17 @@ case class UnionExec(children: Seq[SparkPlan]) extends SparkPlan with CodegenSup
952952
(left, right) match {
953953
case (SinglePartition, SinglePartition) => true
954954
case (l: HashPartitioningLike, r: HashPartitioningLike) => l == r
955-
// For `KeyedPartitioning`, only the partition expressions must match (both sides'
956-
// expressions have already been remapped to this union's output attributes by
957-
// `prepareOutputPartitioning`). The partition keys are intentionally not compared here:
958-
// children typically carry different key sets, and `outputPartitioning` merges them.
955+
// For `KeyedPartitioning`, the partition expressions must match (both sides' expressions
956+
// have already been remapped to this union's output attributes by
957+
// `prepareOutputPartitioning`), and so must the types their keys were built with. The
958+
// partition keys themselves are intentionally not compared: children typically carry
959+
// different key sets, and `outputPartitioning` merges them. Their types cannot be merged the
960+
// same way, since one type list has to stand for every row of the concatenation, and a
961+
// wrapper compares its types before its values, so keys of two types would never be found
962+
// equal to one another.
959963
case (l: KeyedPartitioning, r: KeyedPartitioning) =>
960964
l.expressions.length == r.expressions.length &&
965+
l.keyDataTypes == r.keyDataTypes &&
961966
l.expressions.zip(r.expressions).forall { case (le, re) => le.semanticEquals(re) }
962967
// Note: two `RangePartitioning`s with even same ordering and number of partitions
963968
// are not equal, because they might have different partition bounds.

sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2ScanExecBase.scala

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package org.apache.spark.sql.execution.datasources.v2
1919

2020
import org.apache.spark.rdd.RDD
2121
import org.apache.spark.sql.catalyst.InternalRow
22-
import org.apache.spark.sql.catalyst.expressions.{Ascending, Expression, RowOrdering, SortOrder}
22+
import org.apache.spark.sql.catalyst.expressions.{Ascending, Expression, SortOrder}
2323
import org.apache.spark.sql.catalyst.plans.physical
2424
import org.apache.spark.sql.catalyst.plans.physical.KeyedPartitioning
2525
import org.apache.spark.sql.catalyst.util.truncatedString
@@ -104,11 +104,13 @@ trait DataSourceV2ScanExecBase
104104
keyGroupedPartitioning match {
105105
case Some(exprs) if conf.v2BucketingEnabled && KeyedPartitioning.supportsExpressions(exprs) &&
106106
inputPartitions.nonEmpty && inputPartitions.forall(_.isInstanceOf[HasPartitionKey]) =>
107-
val dataTypes = exprs.map(_.dataType)
108-
val rowOrdering = RowOrdering.createNaturalAscendingOrdering(dataTypes)
109-
val partitionKeys =
110-
inputPartitions.map(_.asInstanceOf[HasPartitionKey].partitionKey()).sorted(rowOrdering)
111-
Some(KeyedPartitioning(exprs, partitionKeys))
107+
// `sortKeys` rather than sorting here, so the ordering and the type list come off the one
108+
// factory that also wraps the keys, instead of this deriving a second ordering over the
109+
// same schema.
110+
Some(KeyedPartitioning(
111+
exprs,
112+
inputPartitions.map(_.asInstanceOf[HasPartitionKey].partitionKey()),
113+
sortKeys = true))
112114
case _ => None
113115
}
114116
}

0 commit comments

Comments
 (0)