Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ trait HashPartitioningLike extends Expression with Partitioning with Unevaluable
case class HashPartitioning(expressions: Seq[Expression], numPartitions: Int)
extends HashPartitioningLike {

override def createShuffleSpec(distribution: ClusteredDistribution): ShuffleSpec =
override def createShuffleSpec(distribution: ClusteredDistribution): HashShuffleSpec =
HashShuffleSpec(this, distribution)

/**
Expand Down Expand Up @@ -364,7 +364,7 @@ case class NullAwareHashPartitioning(expressions: Seq[Expression], numPartitions
}
}

override def createShuffleSpec(distribution: ClusteredDistribution): ShuffleSpec =
override def createShuffleSpec(distribution: ClusteredDistribution): NullAwareHashShuffleSpec =
NullAwareHashShuffleSpec(this, distribution)

override protected def withNewChildrenInternal(
Expand Down Expand Up @@ -1198,18 +1198,6 @@ case class BroadcastPartitioning(mode: BroadcastMode) extends Partitioning {
}
}

/**
* This is used in the scenario where an operator has multiple children (e.g., join) and one or more
* of which have their own requirement regarding whether its data can be considered as
* co-partitioned from others. This offers APIs for:
*
* - Comparing with specs from other children of the operator and check if they are compatible.
* When two specs are compatible, we can say their data are co-partitioned, and Spark will
* potentially be able to eliminate shuffle if necessary.
* - Creating a partitioning that can be used to re-partition another child, so that to make it
* having a compatible partitioning as this node.
*/

/**
* Represents a partitioning where partition IDs are passed through directly from the
* DirectShufflePartitionID expression. This partitioning scheme is used when users
Expand Down Expand Up @@ -1253,12 +1241,17 @@ case class ShufflePartitionIdPassThrough(
copy(expr = newChildren.head.asInstanceOf[DirectShufflePartitionID])
}

trait ShuffleSpec {
/**
* Returns the number of partitions of this shuffle spec
*/
def numPartitions: Int

/**
* Describes how a child's data is laid out, for the purpose of deciding whether two children are
* co-partitioned and, if not, what to shuffle the other one onto.
*
* There are two kinds, and the split is deliberate. A [[LeafShuffleSpec]] is one concrete layout.
* A [[ShuffleSpecCollection]] stands for a choice between several, so it can answer
* [[isCompatibleWith]], which succeeds when any member matches, but it cannot answer anything that
* needs one member: which member is the right one depends on what the other side matched, and that
* is only visible to the caller comparing the two sides.
*/
sealed trait ShuffleSpec {
/**
* Returns true iff this spec is compatible with the provided shuffle spec.
*
Expand All @@ -1275,6 +1268,21 @@ trait ShuffleSpec {
*/
def canCreatePartitioning: Boolean

/**
* This spec's leaf specs: a [[ShuffleSpecCollection]] yields its members recursively, and a
* [[LeafShuffleSpec]] yields itself. A caller that needs one member picks from these.
*/
def flatten: Seq[LeafShuffleSpec]
}

trait LeafShuffleSpec extends ShuffleSpec {
override final def flatten: Seq[LeafShuffleSpec] = this +: Nil

/**
* Returns the number of partitions of this shuffle spec
*/
def numPartitions: Int

/**
* Creates a partitioning that can be used to re-partition the other side with the given
* clustering expressions.
Expand All @@ -1286,9 +1294,10 @@ trait ShuffleSpec {
throw SparkUnsupportedOperationException()
}

case object SinglePartitionShuffleSpec extends ShuffleSpec {
override def isCompatibleWith(other: ShuffleSpec): Boolean = {
other.numPartitions == 1
case object SinglePartitionShuffleSpec extends LeafShuffleSpec {
override def isCompatibleWith(other: ShuffleSpec): Boolean = other match {
case leaf: LeafShuffleSpec => leaf.numPartitions == 1
case ShuffleSpecCollection(specs) => specs.exists(isCompatibleWith)
}

override def canCreatePartitioning: Boolean = false
Expand All @@ -1301,7 +1310,7 @@ case object SinglePartitionShuffleSpec extends ShuffleSpec {

case class RangeShuffleSpec(
numPartitions: Int,
distribution: ClusteredDistribution) extends ShuffleSpec {
distribution: ClusteredDistribution) extends LeafShuffleSpec {

// `RangePartitioning` is not compatible with any other partitioning since it can't guarantee
// data are co-partitioned for all the children, as range boundaries are randomly sampled. We
Expand Down Expand Up @@ -1338,7 +1347,7 @@ private object HashShuffleSpecCompatibility {

case class HashShuffleSpec(
partitioning: HashPartitioning,
distribution: ClusteredDistribution) extends ShuffleSpec {
distribution: ClusteredDistribution) extends LeafShuffleSpec {

/**
* A sequence where each element is a set of positions of the hash partition key to the cluster
Expand Down Expand Up @@ -1424,7 +1433,7 @@ case class HashShuffleSpec(
*/
case class NullAwareHashShuffleSpec(
partitioning: NullAwareHashPartitioning,
distribution: ClusteredDistribution) extends ShuffleSpec {
distribution: ClusteredDistribution) extends LeafShuffleSpec {

lazy val hashKeyPositions: Seq[mutable.BitSet] = {
val distKeyToPos = mutable.Map.empty[Expression, mutable.BitSet]
Expand Down Expand Up @@ -1481,8 +1490,8 @@ case class NullAwareHashShuffleSpec(
}

case class CoalescedHashShuffleSpec(
from: ShuffleSpec,
partitions: Seq[CoalescedBoundary]) extends ShuffleSpec {
from: LeafShuffleSpec,
partitions: Seq[CoalescedBoundary]) extends LeafShuffleSpec {

override def isCompatibleWith(other: ShuffleSpec): Boolean = other match {
case SinglePartitionShuffleSpec =>
Expand Down Expand Up @@ -1558,7 +1567,7 @@ case class IdentityReducer(transform: TransformExpression) extends Reducer[Any,
case class KeyedShuffleSpec(
partitioning: KeyedPartitioning,
distribution: ClusteredDistribution,
joinKeyPositions: Option[Seq[Int]] = None) extends ShuffleSpec {
joinKeyPositions: Option[Seq[Int]] = None) extends LeafShuffleSpec {

/**
* A sequence where each element is a set of positions of the partition expression to the cluster
Expand Down Expand Up @@ -1595,7 +1604,7 @@ case class KeyedShuffleSpec(
// 4. the partition values from both sides are following the same order.
case otherSpec @ KeyedShuffleSpec(otherPartitioning, otherDistribution, _) =>
distribution.clustering.length == otherDistribution.clustering.length &&
numPartitions == other.numPartitions && areKeysCompatible(otherSpec) &&
numPartitions == otherSpec.numPartitions && areKeysCompatible(otherSpec) &&
partitioning.partitionKeys == otherPartitioning.partitionKeys
case ShuffleSpecCollection(specs) =>
specs.exists(isCompatibleWith)
Expand Down Expand Up @@ -1775,7 +1784,7 @@ case class KeyedShuffleSpec(

case class ShufflePartitionIdPassThroughSpec(
partitioning: ShufflePartitionIdPassThrough,
distribution: ClusteredDistribution) extends ShuffleSpec {
distribution: ClusteredDistribution) extends LeafShuffleSpec {

/**
* A sequence where each element is a set of positions of the partition key to the cluster
Expand Down Expand Up @@ -1818,24 +1827,20 @@ case class ShufflePartitionIdPassThroughSpec(
override def numPartitions: Int = partitioning.numPartitions
}

/**
* A choice between several layouts, produced by [[PartitioningCollection.createShuffleSpec]].
*
* `specs` can hold a nested collection, since a [[PartitioningCollection]] can hold a nested one.
*/
case class ShuffleSpecCollection(specs: Seq[ShuffleSpec]) extends ShuffleSpec {
require(specs.nonEmpty, "expected specs to be non-empty")

override def isCompatibleWith(other: ShuffleSpec): Boolean = {
specs.exists(_.isCompatibleWith(other))
}

override def canCreatePartitioning: Boolean =
specs.forall(_.canCreatePartitioning)

override def createPartitioning(clustering: Seq[Expression]): Partitioning = {
// as we only consider # of partitions as the cost now, it doesn't matter which one we choose
// since they should all have the same # of partitions.
require(specs.map(_.numPartitions).toSet.size == 1, "expected all specs in the collection " +
"to have the same number of partitions")
specs.head.createPartitioning(clustering)
}

override def numPartitions: Int = {
require(specs.nonEmpty, "expected specs to be non-empty")
specs.head.numPartitions
}
override def flatten: Seq[LeafShuffleSpec] = specs.flatMap(_.flatten)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package org.apache.spark.sql.catalyst

import org.apache.spark.{SparkFunSuite, SparkUnsupportedOperationException}
import org.apache.spark.sql.catalyst.dsl.expressions._
import org.apache.spark.sql.catalyst.expressions.{Attribute, DirectShufflePartitionID, Expression, TransformExpression}
import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, DirectShufflePartitionID, Expression, TransformExpression}
import org.apache.spark.sql.catalyst.plans.SQLHelper
import org.apache.spark.sql.catalyst.plans.physical._
import org.apache.spark.sql.connector.catalog.functions.ScalarFunction
Expand Down Expand Up @@ -59,7 +59,7 @@ class ShuffleSpecSuite extends SparkFunSuite with SQLHelper {
}

protected def checkCreatePartitioning(
spec: ShuffleSpec,
spec: LeafShuffleSpec,
dist: ClusteredDistribution,
expected: Partitioning): Unit = {
val actual = spec.createPartitioning(dist.clustering)
Expand Down Expand Up @@ -613,13 +613,6 @@ class ShuffleSpecSuite extends SparkFunSuite with SQLHelper {
SinglePartition
)

checkCreatePartitioning(ShuffleSpecCollection(Seq(
HashShuffleSpec(HashPartitioning(Seq($"a"), 10), distribution),
RangeShuffleSpec(10, distribution))),
ClusteredDistribution(Seq($"c", $"d")),
HashPartitioning(Seq($"c"), 10)
)

// unsupported cases

checkError(
Expand All @@ -629,7 +622,7 @@ class ShuffleSpecSuite extends SparkFunSuite with SQLHelper {
condition = "UNSUPPORTED_CALL.WITHOUT_SUGGESTION",
parameters = Map(
"methodName" -> "createPartitioning$",
"className" -> "org.apache.spark.sql.catalyst.plans.physical.ShuffleSpec"))
"className" -> "org.apache.spark.sql.catalyst.plans.physical.LeafShuffleSpec"))
}

test("compatibility: ShufflePartitionIdPassThroughSpec on both sides") {
Expand Down Expand Up @@ -690,4 +683,69 @@ class ShuffleSpecSuite extends SparkFunSuite with SQLHelper {
expected = false
)
}

test("SPARK-59080: a collection whose members cover different key subsets disagrees") {
val id = AttributeReference("id", IntegerType)()
val t1 = AttributeReference("t1", IntegerType)()
val t2 = AttributeReference("t2", IntegerType)()
val keys = Seq(InternalRow(1, 1), InternalRow(1, 2), InternalRow(2, 1))

// The shape an alias cross-product produces: same arity, same keys, different expressions. The
// operation clusters on (id, t1), so the first member projects onto both positions and keeps
// three partitions, while the second matches only `id` and keeps two.
val collection = PartitioningCollection.fromPartitionings(Seq(
KeyedPartitioning(Seq(id, t1), keys),
KeyedPartitioning(Seq(id, t2), keys)))

withSQLConf(SQLConf.V2_BUCKETING_ALLOW_KEYS_SUBSET_OF_PARTITION_KEYS.key -> "true") {
val spec = collection.createShuffleSpec(ClusteredDistribution(Seq(id, t1)))
.asInstanceOf[ShuffleSpecCollection]

// The disagreement is kept rather than resolved here. Every member has to stay for
// `isCompatibleWith`, which answers for any of them, and the collection cannot know which one
// the other side matched. `EnsureRequirements` resolves that and asks the member, so the
// collection has no partition count of its own to read.
val memberPartitions = spec.flatten.map(_.numPartitions)
assert(memberPartitions.toSet === Set(3, 2))
assert(spec.isCompatibleWith(spec), "every member stays available for matching")
}
}

test("SPARK-59256: a single-partition side matches a collection through any member") {
val id = AttributeReference("id", IntegerType)()
val t1 = AttributeReference("t1", IntegerType)()
val t2 = AttributeReference("t2", IntegerType)()
// Clustering on (id, t1) again. The first member projects onto both positions and keeps three
// partitions, the second matches `id` only and collapses to one. The single-partition member
// is deliberately not the head.
val keys = Seq(InternalRow(1, 1), InternalRow(1, 2), InternalRow(1, 3))
val collection = PartitioningCollection.fromPartitionings(Seq(
KeyedPartitioning(Seq(id, t1), keys),
KeyedPartitioning(Seq(id, t2), keys)))

withSQLConf(SQLConf.V2_BUCKETING_ALLOW_KEYS_SUBSET_OF_PARTITION_KEYS.key -> "true") {
val spec = collection.createShuffleSpec(ClusteredDistribution(Seq(id, t1)))
.asInstanceOf[ShuffleSpecCollection]
val memberPartitions = spec.flatten.map(_.numPartitions)
// Ordered, not just as a set: the whole point is that the head is the wrong one to read.
assert(memberPartitions === Seq(3, 1))

// Reading the head's count would answer 3, and miss the member that does have one partition.
// Only this direction is asserted: `KeyedShuffleSpec` has no `SinglePartitionShuffleSpec`
// case at all, so the reverse is false whatever the member counts are. That asymmetry is
// pre-existing and is not what this change is about.
assert(SinglePartitionShuffleSpec.isCompatibleWith(spec))
}
}

test("SPARK-59256: flattening reaches the members of a nested collection") {
val distribution = ClusteredDistribution(Seq($"a", $"b"))
val buried = HashShuffleSpec(HashPartitioning(Seq($"a"), 10), distribution)
val direct = HashShuffleSpec(HashPartitioning(Seq($"b"), 10), distribution)
// A `PartitioningCollection` can hold another one, so a spec collection can nest too.
val collection = ShuffleSpecCollection(Seq(ShuffleSpecCollection(Seq(buried)), direct))

assert(collection.flatten === Seq(buried, direct))
assert(buried.flatten === Seq(buried), "a leaf flattens to itself")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,13 @@ case class GroupPartitionsExec(
assert(projectedExpressions.length == exprs.length)
projectedExpressions.zip(exprs).map {
case (expr, Some(KeyReducer(_, reduced))) =>
// `reduced` was stored from the single spec that `createKeyedShuffleSpec`
// picked (`collectFirst`); re-target it at this `KeyedPartitioning`'s own key
// attribute so that every `KeyedPartitioning` in a collection keeps its own.
// `reduced` was stored from the one member `checkKeyGroupCompatible` paired
// this side on, which need not be the member re-derived here. Sound because
// the members of a `PartitioningCollection` share their `partitionKeys`
// reference and arity, and reducing reads the key values at their own types, so
// every member reduces to the same values. Only the key attribute differs, and
// re-targeting `reduced` at this one is exactly what keeps each
// `KeyedPartitioning` in the collection reporting its own.
reduced.withReference(expr.references.head)
case (expr, None) => expr
}
Expand Down
Loading