Skip to content

Commit 580ca49

Browse files
committed
[SPARK-56046][SQL] Typed SPJ partition key reducers
1 parent f322271 commit 580ca49

8 files changed

Lines changed: 377 additions & 33 deletions

File tree

sql/catalyst/src/main/java/org/apache/spark/sql/connector/catalog/functions/Reducer.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.apache.spark.sql.connector.catalog.functions;
1818

1919
import org.apache.spark.annotation.Evolving;
20+
import org.apache.spark.sql.types.DataType;
2021

2122
/**
2223
* A 'reducer' for output of user-defined functions.
@@ -39,4 +40,12 @@
3940
@Evolving
4041
public interface Reducer<I, O> {
4142
O reduce(I arg);
43+
44+
/**
45+
* Returns the {@link DataType data type} of values produced by this function.
46+
* It can return null to signal it doesn't change the input type.
47+
*
48+
* @return a data type for values produced by this function.
49+
*/
50+
default DataType resultType() { return null; }
4251
}

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

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -464,11 +464,15 @@ case class KeyedPartitioning(
464464
KeyedPartitioning.projectKeys(partitionKeys, expressionDataTypes, positions)
465465

466466
/**
467-
* Reduces this partitioning's partition keys by applying the given reducers.
467+
* Reduces this partitioning's partition keys by applying the given reducers and use the provided
468+
* types for comparison.
468469
* Returns the distinct reduced keys.
469470
*/
470-
def reduceKeys(reducers: Seq[Option[Reducer[_, _]]]): Seq[InternalRowComparableWrapper] =
471-
KeyedPartitioning.reduceKeys(partitionKeys, expressionDataTypes, reducers).distinct
471+
def reduceKeys(
472+
reducers: Seq[Option[Reducer[_, _]]],
473+
reducedDataTypes: Seq[DataType]): Seq[InternalRowComparableWrapper] =
474+
KeyedPartitioning.reduceKeys(partitionKeys, expressionDataTypes, reducers, reducedDataTypes)
475+
.distinct
472476

473477
override def satisfies0(required: Distribution): Boolean = {
474478
nonGroupedSatisfies(required) || groupedSatisfies(required)
@@ -581,14 +585,28 @@ object KeyedPartitioning {
581585
}
582586

583587
/**
584-
* Reduces a sequence of partition keys by applying reducers to each position.
588+
* Reduces a sequence of data types by applying reducers to each position.
589+
*/
590+
def reduceTypes(
591+
dataTypes: Seq[DataType],
592+
reducers: Seq[Option[Reducer[_, _]]]): Seq[DataType] = {
593+
dataTypes.zip(reducers).map {
594+
case (t, Some(reducer: Reducer[Any, Any])) => Option(reducer.resultType()).getOrElse(t)
595+
case (t, _) => t
596+
}
597+
}
598+
599+
/**
600+
* Reduces a sequence of partition keys by applying reducers to each position and using the
601+
* provided types for comparison.
585602
*/
586603
def reduceKeys(
587604
keys: Seq[InternalRowComparableWrapper],
588605
dataTypes: Seq[DataType],
589-
reducers: Seq[Option[Reducer[_, _]]]): Seq[InternalRowComparableWrapper] = {
606+
reducers: Seq[Option[Reducer[_, _]]],
607+
reducedDataTypes: Seq[DataType]): Seq[InternalRowComparableWrapper] = {
590608
val comparableKeyWrapperFactory =
591-
InternalRowComparableWrapper.getInternalRowComparableWrapperFactory(dataTypes)
609+
InternalRowComparableWrapper.getInternalRowComparableWrapperFactory(reducedDataTypes)
592610
keys.map { key =>
593611
val keyValues = key.row.toSeq(dataTypes)
594612
val reducedKey = keyValues.zip(reducers).map {

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2133,6 +2133,14 @@ object SQLConf {
21332133
.booleanConf
21342134
.createWithDefault(false)
21352135

2136+
val V2_BUCKETING_ALLOW_INCOMPATIBLE_TRANSFORM_TYPES =
2137+
buildConf("spark.sql.legacy.allowIncompatibleTransformTypes.enabled")
2138+
.doc("Whether to allow storage-partition join where the partition transforms produce " +
2139+
"incompatible reduced types and use the left partition key type for comparison.")
2140+
.version("4.2.0")
2141+
.booleanConf
2142+
.createWithDefault(true)
2143+
21362144
val V2_BUCKETING_PARTITION_FILTER_ENABLED =
21372145
buildConf("spark.sql.sources.v2.bucketing.partition.filter.enabled")
21382146
.doc(s"Whether to filter partitions when running storage-partition join. " +
@@ -7692,6 +7700,9 @@ class SQLConf extends Serializable with Logging with SqlApiConf {
76927700
def v2BucketingAllowCompatibleTransforms: Boolean =
76937701
getConf(SQLConf.V2_BUCKETING_ALLOW_COMPATIBLE_TRANSFORMS)
76947702

7703+
def v2BucketingAllowIncompatibleTransformTypes: Boolean =
7704+
getConf(SQLConf.V2_BUCKETING_ALLOW_INCOMPATIBLE_TRANSFORM_TYPES)
7705+
76957706
def v2BucketingAllowSorting: Boolean =
76967707
getConf(SQLConf.V2_BUCKETING_SORTING_ENABLED)
76977708

sql/catalyst/src/test/scala/org/apache/spark/sql/connector/catalog/InMemoryBaseTable.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,10 @@ abstract class InMemoryBaseTable(
203203
case YearsTransform(ref) =>
204204
extractor(ref.fieldNames, cleanedSchema, row) match {
205205
case (days: Int, DateType) =>
206-
ChronoUnit.YEARS.between(EPOCH_LOCAL_DATE, DateTimeUtils.daysToLocalDate(days))
206+
ChronoUnit.YEARS.between(EPOCH_LOCAL_DATE, DateTimeUtils.daysToLocalDate(days)).toInt
207207
case (micros: Long, TimestampType) =>
208208
val localDate = DateTimeUtils.microsToInstant(micros).atZone(UTC).toLocalDate
209-
ChronoUnit.YEARS.between(EPOCH_LOCAL_DATE, localDate)
209+
ChronoUnit.YEARS.between(EPOCH_LOCAL_DATE, localDate).toInt
210210
case (v, t) =>
211211
throw new IllegalArgumentException(s"Match: unsupported argument(s) type - ($v, $t)")
212212
}
@@ -225,7 +225,7 @@ abstract class InMemoryBaseTable(
225225
case (days, DateType) =>
226226
days
227227
case (micros: Long, TimestampType) =>
228-
ChronoUnit.DAYS.between(Instant.EPOCH, DateTimeUtils.microsToInstant(micros))
228+
ChronoUnit.DAYS.between(Instant.EPOCH, DateTimeUtils.microsToInstant(micros)).toInt
229229
case (v, t) =>
230230
throw new IllegalArgumentException(s"Match: unsupported argument(s) type - ($v, $t)")
231231
}

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,15 +141,21 @@ case class GroupPartitionsExec(
141141
)(keyedPartitioning.projectKeys)
142142

143143
// Reduce keys if reducers are specified
144-
val reducedKeys = reducers.fold(projectedKeys)(
145-
KeyedPartitioning.reduceKeys(projectedKeys, projectedDataTypes, _))
144+
val (reducedDataTypes, reducedKeys) = reducers match {
145+
case Some(reducers) =>
146+
val reducedDataTypes = KeyedPartitioning.reduceTypes(projectedDataTypes, reducers)
147+
val reducedKeys = KeyedPartitioning.reduceKeys(projectedKeys, projectedDataTypes, reducers,
148+
reducedDataTypes)
149+
(reducedDataTypes, reducedKeys)
150+
case _ => (projectedDataTypes, projectedKeys)
151+
}
146152

147153
val keyToPartitionIndices = reducedKeys.zipWithIndex.groupMap(_._1)(_._2)
148154

149155
if (expectedPartitionKeys.isDefined) {
150156
alignToExpectedKeys(keyToPartitionIndices)
151157
} else {
152-
(groupAndSortByKeys(keyToPartitionIndices, projectedDataTypes), true)
158+
(groupAndSortByKeys(keyToPartitionIndices, reducedDataTypes), true)
153159
}
154160
}
155161

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

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import org.apache.spark.sql.execution._
3131
import org.apache.spark.sql.execution.datasources.v2.GroupPartitionsExec
3232
import org.apache.spark.sql.execution.joins.{ShuffledHashJoinExec, SortMergeJoinExec}
3333
import org.apache.spark.sql.internal.SQLConf
34+
import org.apache.spark.sql.types.DataType
3435

3536
/**
3637
* Ensures that the [[org.apache.spark.sql.catalyst.plans.physical.Partitioning Partitioning]]
@@ -509,11 +510,25 @@ case class EnsureRequirements(
509510
// in case of compatible but not identical partition expressions, we apply 'reduce'
510511
// transforms to group one side's partitions as well as the common partition values
511512
val leftReducers = leftSpec.reducers(rightSpec)
512-
val leftReducedKeys =
513-
leftReducers.fold(leftPartitioning.partitionKeys)(leftPartitioning.reduceKeys)
514513
val rightReducers = rightSpec.reducers(leftSpec)
515-
val rightReducedKeys =
516-
rightReducers.fold(rightPartitioning.partitionKeys)(rightPartitioning.reduceKeys)
514+
val leftReducedDataTypes = leftReducers.fold(leftPartitioning.expressionDataTypes)(
515+
KeyedPartitioning.reduceTypes(leftPartitioning.expressionDataTypes, _))
516+
val rightReducedDataTypes = rightReducers.fold(rightPartitioning.expressionDataTypes)(
517+
KeyedPartitioning.reduceTypes(rightPartitioning.expressionDataTypes, _))
518+
if (leftReducedDataTypes != rightReducedDataTypes && (
519+
!conf.v2BucketingAllowIncompatibleTransformTypes ||
520+
leftReducedDataTypes.map(PhysicalDataType(_)) !=
521+
rightReducedDataTypes.map(PhysicalDataType(_)))) {
522+
throw new SparkException("Storage-partition join partition transforms produced " +
523+
s"incompatible reduced types, left: $leftReducedDataTypes, right: " +
524+
s"$rightReducedDataTypes")
525+
}
526+
val commonDataTypes = leftReducedDataTypes
527+
val leftReducedKeys = leftReducers.fold(leftPartitioning.partitionKeys)(
528+
leftPartitioning.reduceKeys(_, commonDataTypes))
529+
val rightReducedKeys = rightReducers.fold(
530+
rewrapKeys(rightPartitioning.partitionKeys, rightReducedDataTypes, commonDataTypes))(
531+
rightPartitioning.reduceKeys(_, commonDataTypes))
517532

518533
// merge values on both sides
519534
var mergedPartitionKeys =
@@ -628,10 +643,14 @@ case class EnsureRequirements(
628643
}
629644
}
630645

646+
val leftMergedPartitionKeys = mergedPartitionKeys
647+
val rightMergedPartitionKeys =
648+
rewrapKeyMap(mergedPartitionKeys, commonDataTypes, rightReducedDataTypes)
649+
631650
// Now we need to push-down the common partition information to the `GroupPartitionsExec`s.
632-
newLeft = applyGroupPartitions(left, leftSpec.joinKeyPositions, mergedPartitionKeys,
651+
newLeft = applyGroupPartitions(left, leftSpec.joinKeyPositions, leftMergedPartitionKeys,
633652
leftReducers, distributePartitions = applyPartialClustering && !replicateLeftSide)
634-
newRight = applyGroupPartitions(right, rightSpec.joinKeyPositions, mergedPartitionKeys,
653+
newRight = applyGroupPartitions(right, rightSpec.joinKeyPositions, rightMergedPartitionKeys,
635654
rightReducers, distributePartitions = applyPartialClustering && !replicateRightSide)
636655
}
637656
}
@@ -656,6 +675,32 @@ case class EnsureRequirements(
656675
}
657676
}
658677

678+
private def rewrapKeys(
679+
keys: Seq[InternalRowComparableWrapper],
680+
currentDataTypes: Seq[DataType],
681+
expectedDataType: Seq[DataType]) = {
682+
if (currentDataTypes != expectedDataType) {
683+
val comparableKeyWrapperFactory =
684+
InternalRowComparableWrapper.getInternalRowComparableWrapperFactory(expectedDataType)
685+
keys.map(key => comparableKeyWrapperFactory(key.row))
686+
} else {
687+
keys
688+
}
689+
}
690+
691+
private def rewrapKeyMap(
692+
keyMap: Seq[(InternalRowComparableWrapper, Int)],
693+
currentDataTypes: Seq[DataType],
694+
expectedDataType: Seq[DataType]) = {
695+
if (currentDataTypes != expectedDataType) {
696+
val comparableKeyWrapperFactory =
697+
InternalRowComparableWrapper.getInternalRowComparableWrapperFactory(expectedDataType)
698+
keyMap.map { case (key, numParts) => (comparableKeyWrapperFactory(key.row), numParts) }
699+
} else {
700+
keyMap
701+
}
702+
}
703+
659704
// Similar to `OptimizeSkewedJoin.canSplitRightSide`
660705
private def canReplicateLeftSide(joinType: JoinType): Boolean = {
661706
joinType == Inner || joinType == Cross || joinType == RightOuter

0 commit comments

Comments
 (0)