Skip to content

Commit 84052f2

Browse files
committed
[SPARK-56164][SQL] Fix SPJ merged key ordering
1 parent 4f23d3e commit 84052f2

3 files changed

Lines changed: 147 additions & 32 deletions

File tree

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -525,9 +525,14 @@ case class EnsureRequirements(
525525
rightReducedDataTypes = rightReducedDataTypes)
526526
}
527527

528+
val reducedKeyRowOrdering = RowOrdering.createNaturalAscendingOrdering(leftReducedDataTypes)
529+
val reducedKeyOrdering =
530+
reducedKeyRowOrdering.on((t: InternalRowComparableWrapper) => t.row)
531+
528532
// merge values on both sides
529-
var mergedPartitionKeys = mergeAndDedupPartitions(leftReducedKeys, rightReducedKeys,
530-
joinType, leftPartitioning.keyOrdering).map((_, 1))
533+
var mergedPartitionKeys =
534+
mergeAndDedupPartitions(leftReducedKeys, rightReducedKeys, joinType, reducedKeyOrdering)
535+
.map((_, 1))
531536

532537
logInfo(log"After merging, there are " +
533538
log"${MDC(LogKeys.NUM_PARTITIONS, mergedPartitionKeys.size)} partitions")

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

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,23 @@ class KeyGroupedPartitioningSuite extends DistributionAndOrderingSuiteBase with
7575
Column.create("dept_id", IntegerType),
7676
Column.create("data", StringType))
7777

78-
def withFunction[T](fn: UnboundFunction)(f: => T): T = {
79-
val id = Identifier.of(Array.empty, fn.name())
80-
val oldFn = Option.when(catalog.listFunctions(Array.empty).contains(id)) {
81-
val fn = catalog.loadFunction(id)
82-
catalog.dropFunction(id)
83-
fn
84-
}
85-
catalog.createFunction(id, fn)
78+
def withFunctions[T](fns: UnboundFunction*)(f: => T): T = {
79+
val fnIds = catalog.listFunctions(Array.empty)
80+
val oldFns = fns.map { fn =>
81+
val id = Identifier.of(Array.empty, fn.name())
82+
val oldFn = Option.when(fnIds.contains(id)) {
83+
val fn = catalog.loadFunction(id)
84+
catalog.dropFunction(id)
85+
fn
86+
}
87+
catalog.createFunction(id, fn)
88+
(id, oldFn)
89+
}
8690
try f finally {
87-
catalog.dropFunction(id)
88-
oldFn.foreach(catalog.createFunction(id, _))
91+
oldFns.foreach { case (id, oldFn ) =>
92+
catalog.dropFunction(id)
93+
oldFn.foreach(catalog.createFunction(id, _))
94+
}
8995
}
9096
}
9197

@@ -3441,7 +3447,7 @@ class KeyGroupedPartitioningSuite extends DistributionAndOrderingSuiteBase with
34413447
}
34423448

34433449
test("SPARK-56046: Reducers with different result types") {
3444-
withFunction(UnboundDaysFunctionWithIncompatibleResultTypeReducer) {
3450+
withFunctions(UnboundDaysFunctionWithToYearsReducerWithDateResult) {
34453451
val items_partitions = Array(days("arrive_time"))
34463452
createTable(items, itemsColumns, items_partitions)
34473453
sql(s"INSERT INTO testcat.ns.$items VALUES " +
@@ -3478,4 +3484,48 @@ class KeyGroupedPartitioningSuite extends DistributionAndOrderingSuiteBase with
34783484
}
34793485
}
34803486
}
3487+
3488+
test("SPARK-56164: Reducers with different result types to original keys") {
3489+
withFunctions(
3490+
UnboundDaysFunctionWithToYearsReducerWithLongResult,
3491+
UnboundYearsFunctionWithToYearsReducerWithLongResult) {
3492+
val items_partitions = Array(days("arrive_time"))
3493+
createTable(items, itemsColumns, items_partitions)
3494+
sql(s"INSERT INTO testcat.ns.$items VALUES " +
3495+
s"(0, 'aa', 39.0, cast('2020-01-01' as timestamp)), " +
3496+
s"(1, 'aa', 40.0, cast('2020-01-01' as timestamp)), " +
3497+
s"(2, 'bb', 41.0, cast('2021-01-03' as timestamp)), " +
3498+
s"(3, 'bb', 42.0, cast('2021-01-04' as timestamp))")
3499+
3500+
val purchases_partitions = Array(years("time"))
3501+
createTable(purchases, purchasesColumns, purchases_partitions)
3502+
sql(s"INSERT INTO testcat.ns.$purchases VALUES " +
3503+
s"(1, 42.0, cast('2020-01-01' as timestamp)), " +
3504+
s"(5, 44.0, cast('2020-01-15' as timestamp)), " +
3505+
s"(7, 46.5, cast('2021-02-08' as timestamp))")
3506+
3507+
withSQLConf(
3508+
SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> "true",
3509+
SQLConf.V2_BUCKETING_ALLOW_COMPATIBLE_TRANSFORMS.key -> "true") {
3510+
Seq(
3511+
s"testcat.ns.$items i JOIN testcat.ns.$purchases p ON p.time = i.arrive_time",
3512+
s"testcat.ns.$purchases p JOIN testcat.ns.$items i ON i.arrive_time = p.time"
3513+
).foreach { joinString =>
3514+
val df = sql(
3515+
s"""
3516+
|${selectWithMergeJoinHint("i", "p")} id, item_id
3517+
|FROM $joinString
3518+
|ORDER BY id, item_id
3519+
|""".stripMargin)
3520+
3521+
val shuffles = collectShuffles(df.queryExecution.executedPlan)
3522+
assert(shuffles.isEmpty, "should not add shuffle for both sides of the join")
3523+
val groupPartitions = collectGroupPartitions(df.queryExecution.executedPlan)
3524+
assert(groupPartitions.forall(_.outputPartitioning.numPartitions == 2))
3525+
3526+
checkAnswer(df, Seq(Row(0, 1), Row(1, 1)))
3527+
}
3528+
}
3529+
}
3530+
}
34813531
}

sql/core/src/test/scala/org/apache/spark/sql/connector/catalog/functions/transformFunctions.scala

Lines changed: 79 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,34 @@ import org.apache.spark.sql.catalyst.util.DateTimeUtils
2424
import org.apache.spark.sql.types._
2525
import org.apache.spark.unsafe.types.UTF8String
2626

27-
object UnboundYearsFunction extends UnboundFunction {
27+
abstract class UnboundYearsFunctionBase extends UnboundFunction {
28+
protected def isValidType(dt: DataType): Boolean = dt match {
29+
case DateType | TimestampType => true
30+
case _ => false
31+
}
32+
33+
override def description(): String = name()
34+
override def name(): String = "years"
35+
}
36+
37+
object UnboundYearsFunction extends UnboundYearsFunctionBase {
2838
override def bind(inputType: StructType): BoundFunction = {
2939
if (inputType.size == 1 && isValidType(inputType.head.dataType)) YearsFunction
3040
else throw new UnsupportedOperationException(
3141
"'years' only take date or timestamp as input type")
3242
}
43+
}
3344

34-
private def isValidType(dt: DataType): Boolean = dt match {
35-
case DateType | TimestampType => true
36-
case _ => false
45+
object UnboundYearsFunctionWithToYearsReducerWithLongResult extends UnboundYearsFunctionBase {
46+
override def bind(inputType: StructType): BoundFunction = {
47+
if (inputType.size == 1 && isValidType(inputType.head.dataType)) {
48+
YearsFunctionWithToYearsReducerWithLongResult
49+
} else throw new UnsupportedOperationException(
50+
"'years' only take date or timestamp as input type")
3751
}
38-
39-
override def description(): String = name()
40-
override def name(): String = "years"
4152
}
4253

43-
object YearsFunction extends ScalarFunction[Int] with ReducibleFunction[Int, Int] {
54+
abstract class YearsFunctionBase[O] extends ScalarFunction[Int] with ReducibleFunction[Int, O] {
4455
override def inputTypes(): Array[DataType] = Array(TimestampType)
4556
override def resultType(): DataType = IntegerType
4657
override def name(): String = "years"
@@ -53,10 +64,31 @@ object YearsFunction extends ScalarFunction[Int] with ReducibleFunction[Int, Int
5364
val localDate = DateTimeUtils.microsToInstant(ts).atZone(UTC).toLocalDate
5465
ChronoUnit.YEARS.between(EPOCH_LOCAL_DATE, localDate).toInt
5566
}
67+
}
5668

69+
// This `years` function reduces `IntegerType` partition keys to `IntegerType` partition keys when
70+
// partitions are reduced to partitions of a `days` function, which produces `DateType` keys.
71+
object YearsFunction extends YearsFunctionBase[Int] {
5772
override def reducer(otherFunction: ReducibleFunction[_, _]): Reducer[Int, Int] = null
5873
}
5974

75+
// This `years` function reduces `IntegerType` partition keys to `LongType` partition keys when
76+
// partitions are reduced to partitions of a `days` function, which produces `DateType` keys.
77+
object YearsFunctionWithToYearsReducerWithLongResult extends YearsFunctionBase[Long] {
78+
override def reducer(otherFunction: ReducibleFunction[_, _]): Reducer[Int, Long] = {
79+
if (otherFunction == DaysFunctionWithToYearsReducerWithLongResult) {
80+
YearsToYearsReducerWithLogResult()
81+
} else {
82+
null
83+
}
84+
}
85+
}
86+
87+
case class YearsToYearsReducerWithLogResult() extends Reducer[Int, Long] {
88+
override def resultType(): DataType = LongType
89+
override def reduce(days: Int): Long = days.toLong
90+
}
91+
6092
abstract class UnboundDaysFunctionBase extends UnboundFunction {
6193
protected def isValidType(dt: DataType): Boolean = dt match {
6294
case DateType | TimestampType => true
@@ -75,16 +107,25 @@ object UnboundDaysFunction extends UnboundDaysFunctionBase {
75107
}
76108
}
77109

78-
object UnboundDaysFunctionWithIncompatibleResultTypeReducer extends UnboundDaysFunctionBase {
110+
object UnboundDaysFunctionWithToYearsReducerWithDateResult extends UnboundDaysFunctionBase {
111+
override def bind(inputType: StructType): BoundFunction = {
112+
if (inputType.size == 1 && isValidType(inputType.head.dataType)) {
113+
DaysFunctionWithToYearsReducerWithDateResult
114+
} else throw new UnsupportedOperationException(
115+
"'days' only take date or timestamp as input type")
116+
}
117+
}
118+
119+
object UnboundDaysFunctionWithToYearsReducerWithLongResult extends UnboundDaysFunctionBase {
79120
override def bind(inputType: StructType): BoundFunction = {
80121
if (inputType.size == 1 && isValidType(inputType.head.dataType)) {
81-
DaysFunctionWithIncompatibleResultTypeReducer
122+
DaysFunctionWithToYearsReducerWithLongResult
82123
} else throw new UnsupportedOperationException(
83124
"'days' only take date or timestamp as input type")
84125
}
85126
}
86127

87-
abstract class DaysFunctionBase extends ScalarFunction[Int] with ReducibleFunction[Int, Int] {
128+
abstract class DaysFunctionBase[O] extends ScalarFunction[Int] with ReducibleFunction[Int, O] {
88129
override def inputTypes(): Array[DataType] = Array(TimestampType)
89130
override def resultType(): DataType = DateType
90131
override def name(): String = "days"
@@ -93,7 +134,7 @@ abstract class DaysFunctionBase extends ScalarFunction[Int] with ReducibleFuncti
93134

94135
// This `days` function reduces `DateType` partition keys to `IntegerType` partition keys when
95136
// partitions are reduced to partitions of a `years` function, which produces `IntegerType` keys.
96-
object DaysFunction extends DaysFunctionBase {
137+
object DaysFunction extends DaysFunctionBase[Int] {
97138
override def reducer(otherFunc: ReducibleFunction[_, _]): Reducer[Int, Int] = {
98139
if (otherFunc == YearsFunction) {
99140
DaysToYearsReducer()
@@ -105,32 +146,51 @@ object DaysFunction extends DaysFunctionBase {
105146

106147
// This `days` function reduces `DateType` partition keys to `DateType` partition keys when
107148
// partitions are reduced to partitions of a `years` function, which produces `IntegerType` keys.
108-
object DaysFunctionWithIncompatibleResultTypeReducer extends DaysFunctionBase {
149+
object DaysFunctionWithToYearsReducerWithDateResult extends DaysFunctionBase[Int] {
109150
override def reducer(otherFunc: ReducibleFunction[_, _]): Reducer[Int, Int] = {
110151
if (otherFunc == YearsFunction) {
111-
DaysToYearsReducerWithIncompatibleResultType()
152+
DaysToYearsReducerWithDateResult()
112153
} else {
113154
null
114155
}
115156
}
116157
}
117158

118-
abstract class DaysToYearsReducerBase extends Reducer[Int, Int] {
159+
// This `days` function reduces `DateType` partition keys to `LongType` partition keys when
160+
// partitions are reduced to partitions of a `years` function, which produces `IntegerType` keys.
161+
object DaysFunctionWithToYearsReducerWithLongResult extends DaysFunctionBase[Long] {
162+
override def reducer(otherFunc: ReducibleFunction[_, _]): Reducer[Int, Long] = {
163+
if (otherFunc == YearsFunctionWithToYearsReducerWithLongResult) {
164+
DaysToYearsReducerWithLongResult()
165+
} else {
166+
null
167+
}
168+
}
169+
}
170+
171+
abstract class DaysToYearsReducerBase[O] extends Reducer[Int, O] {
119172
val UTC: ZoneId = ZoneId.of("UTC")
120173
val EPOCH_LOCAL_DATE: LocalDate = Instant.EPOCH.atZone(UTC).toLocalDate
121174

122-
override def reduce(days: Int): Int = {
175+
protected def doReduce(days: Int): Long = {
123176
val localDate = EPOCH_LOCAL_DATE.plusDays(days)
124-
ChronoUnit.YEARS.between(EPOCH_LOCAL_DATE, localDate).toInt
177+
ChronoUnit.YEARS.between(EPOCH_LOCAL_DATE, localDate)
125178
}
126179
}
127180

128-
case class DaysToYearsReducer() extends DaysToYearsReducerBase {
181+
case class DaysToYearsReducer() extends DaysToYearsReducerBase[Int] {
129182
override def resultType(): DataType = IntegerType
183+
override def reduce(days: Int): Int = doReduce(days).toInt
130184
}
131185

132-
case class DaysToYearsReducerWithIncompatibleResultType() extends DaysToYearsReducerBase {
186+
case class DaysToYearsReducerWithDateResult() extends DaysToYearsReducerBase[Int] {
133187
override def resultType(): DataType = DateType
188+
override def reduce(days: Int): Int = doReduce(days).toInt
189+
}
190+
191+
case class DaysToYearsReducerWithLongResult() extends DaysToYearsReducerBase[Long] {
192+
override def resultType(): DataType = LongType
193+
override def reduce(days: Int): Long = doReduce(days)
134194
}
135195

136196
object UnboundBucketFunction extends UnboundFunction {

0 commit comments

Comments
 (0)