peter-toth commented on code in PR #58552:
URL: https://github.com/apache/spark/pull/58552#discussion_r3976730655
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -719,16 +755,18 @@ case class KeyedPartitioning(
}
copy(
expressions = positions.map(expressions),
- partitionKeys = projectedKeys,
- isGrouped = !collapses && sourceOf.size == projectedKeys.length,
- isCollapsed = isCollapsed || collapses)
+ layout = KeyLayout(
Review Comment:
You are right, and it was an unintended behaviour change rather than a
judgement: `copy(...)` carried the marker, a fresh `KeyLayout` does not. Fixed
in
[`d839aee`](https://github.com/apache/spark/commit/d839aeeb3146f07d69b4cebda2b7ed9efdb2d9ff),
so the contract holds whatever positions a caller asks for and the
`createShuffleSpec` comment is true again.
Your reading of the reachability matches mine, and I would rather not have
this method depend on it.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -572,58 +642,37 @@ case class CoalescedNullAwareHashPartitioning(
* }}}
*
* @param expressions Partition transform expressions (e.g., `years(col)`,
`bucket(10, col)`).
- * @param partitionKeys Partition keys wrapped in InternalRowComparableWrapper
for efficient
- * comparison and grouping. One per partition. Typically
in sorted order when
- * produced by a data source or `GroupPartitionsExec`,
but this is not
- * guaranteed after projection. May contain duplicates
when ungrouped.
- * @param isGrouped Whether partition keys are unique (no duplicates).
Computed on first
- * creation, then preserved through copy operations to avoid
recomputation.
- * @param isCollapsed Whether a projection or a reduction mapped keys that
were distinct in the
- * partitioning this one was derived from onto the same
key, so one key here can
- * stand for several of the original ones. Sticky. See "Key
Collapse" above for
- * what it gates and how it travels.
- * @param mayContainUnknownPartitionKeys Whether the data may contain rows
whose partition key is
- * not among the declared `partitionKeys`.
`KeyGroupedPartitioner`
- * routes such rows by a deterministic hash
when a side is
- * re-shuffled onto this partitioning (see
- * `KeyedShuffleSpec.createPartitioning`), so
co-location holds
- * for whole keys only: two marked
partitionings declaring the
- * same keys in the same order and using the
same partition
- * function per position still pair (equal
undeclared keys hash
- * to the same partition), but a row of an
undeclared key sits in
- * the partition of some other declared key
and need not be
- * co-located with rows sharing only a subset
of its columns.
- * `satisfies` and
`KeyedShuffleSpec.areKeysCompatible` therefore
- * accept a marked partitioning only for
full-key clustering,
- * never for a subset of its partition columns
and never for a
- * global ordering across several partitions.
Two carry rules:
- * (1) a node that changes the declared key
set must drop the
- * keyed partitioning, whether it coarsens it
(a key-dropping
- * projection, a key-changing reduction, a
join-key projection)
- * or expands it over a marked leg (a union,
where another leg
- * may declare exactly the key that leg holds
out-of-set);
- * (2) marker agreement is enforced by
`PartitioningCollection`:
- * the constructor requires it and
`fromPartitionings` normalizes
- * by OR, so members are uniformly marked or
unmarked and
- * consumers may read one member.
`ShuffledJoin`'s `InnerLike`
- * arm, the only site that meets a marked
input with an unmarked
- * one, clears the markers first. That is
precision, not the
- * guarantee: without it the OR would spread
the spurious marker
- * onto the accurate side.
+ * @param layout The partitions this one describes, which is everything about
them except the
+ * expressions naming them. See [[KeyLayout]].
*/
case class KeyedPartitioning(
expressions: Seq[Expression],
- @transient partitionKeys: Seq[InternalRowComparableWrapper],
- isGrouped: Boolean,
- isCollapsed: Boolean,
- mayContainUnknownPartitionKeys: Boolean = false)
- extends Expression with Partitioning with Unevaluable {
- override val numPartitions = partitionKeys.length
+ layout: KeyLayout) extends Expression with Partitioning with Unevaluable {
+ override val numPartitions = layout.partitionKeys.length
+
+ def partitionKeys: Seq[InternalRowComparableWrapper] = layout.partitionKeys
+ def isGrouped: Boolean = layout.isGrouped
+ def isCollapsed: Boolean = layout.isCollapsed
+ def mayContainUnknownPartitionKeys: Boolean =
layout.mayContainUnknownPartitionKeys
+
+ /** This partitioning over a changed layout, e.g.
`withLayout(_.copy(isGrouped = false))`. */
+ def withLayout(f: KeyLayout => KeyLayout): KeyedPartitioning = copy(layout =
f(layout))
override def children: Seq[Expression] = expressions
override def nullable: Boolean = false
override def dataType: DataType = IntegerType
+ /**
+ * Prints the layout's contents where the value object would print, which
keeps the plan string as
+ * it was before the layout held them. The list is curated rather than the
layout's own fields,
+ * for two reasons. `partitionKeys` has to be printed as a `Seq` for
`maxFields` to truncate it,
+ * and a partitioning can hold one key per split. And `dataTypes` has its
naming erased, so
+ * printing it would put a struct field name into a plan that appears
nowhere in the query. A
+ * field the layout grows is therefore a decision here.
+ */
+ override protected def stringArgs: Iterator[Any] =
Review Comment:
Also right, and the description was wrong to claim no plan string change.
Fixed in
[`d839aee`](https://github.com/apache/spark/commit/d839aeeb3146f07d69b4cebda2b7ed9efdb2d9ff)
by appending the marker, so the string is what it was before this PR and the
description is accurate as written.
I took the unconditional print rather than the `TransformExpression` shape.
Printing only when true keeps the marked case visible, but it changes the
string for every unmarked partitioning, which is the opposite of what the
description promises. The scaladoc now says that printing is the default for a
field the layout grows.
##########
sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala:
##########
@@ -2598,6 +2598,89 @@ class KeyGroupedPartitioningSuite
}
}
+ test("SPARK-59285: two legs whose struct field names differ are still
co-partitioned") {
+ // Both legs prune to nothing, and their key spaces differ only in a
struct field name, which
+ // `identity` carries into the key type. A reduce cannot bridge that,
since there is no reducer
+ // between two attributes, so calling the two sides incompatible leaves
nowhere to go: the join
+ // must keep taking them as one layout.
+ withTable("p1", "p2", "p3", "p4") {
+ createTable("p1", Array(Column.create("id", structA)),
Array(identity("id")))
+ sql("INSERT INTO testcat.ns.p1 VALUES (named_struct('a', 1))")
+ createTable("p2", Array(Column.create("id", structA)),
Array(identity("id")))
+ sql("INSERT INTO testcat.ns.p2 VALUES (named_struct('a', 2))")
+ createTable("p3", Array(Column.create("k", structB)),
Array(identity("k")))
+ sql("INSERT INTO testcat.ns.p3 VALUES (named_struct('b', 1))")
+ createTable("p4", Array(Column.create("k", structB)),
Array(identity("k")))
+ sql("INSERT INTO testcat.ns.p4 VALUES (named_struct('b', 2))")
+
+ withSQLConf(
+ SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+ SQLConf.V2_BUCKETING_PUSH_PART_VALUES_ENABLED.key -> "true",
+ SQLConf.V2_BUCKETING_PARTITION_FILTER_ENABLED.key -> "true") {
+ val df = sql(
+ """SELECT leg1.id, leg2.k FROM
+ | (SELECT p1.id AS id FROM testcat.ns.p1 JOIN testcat.ns.p2 ON
p1.id = p2.id) leg1
+ | JOIN
+ | (SELECT p3.k AS k FROM testcat.ns.p3 JOIN testcat.ns.p4 ON p3.k
= p4.k) leg2
+ | ON leg1.id = leg2.k
+ |""".stripMargin)
+ assert(collectShuffles(df.queryExecution.executedPlan).isEmpty,
+ "the two legs are taken as one layout, so neither is shuffled")
+ checkAnswer(df, Nil)
+ }
+ }
+ }
+
+ test("SPARK-59285: two sides whose partitions were all pruned are not one
layout") {
Review Comment:
Taken, in
[`d839aee`](https://github.com/apache/spark/commit/d839aeeb3146f07d69b4cebda2b7ed9efdb2d9ff).
Two asserts that are not on absence: `collectAllShuffles(plan).isEmpty`, which
holds as you expected, and a key-space pin.
The pin is on the topmost node that reports a keyed partitioning, not on the
whole plan. I tried the whole plan first and measured why it cannot work: the
identity legs below still report their own space, so the plan holds
`ArraySeq(IntegerType)` and `ArraySeq(LongType)`, one per node. Asking the
topmost node says the two legs agreed on the bucket space, which is the claim.
Together they close the vacuity you describe. The first fails if the plan
falls back to shuffles, the second if there is no keyed partitioning left to
report.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -1687,6 +1744,13 @@ case class KeyedShuffleSpec(
case otherSpec @ KeyedShuffleSpec(otherPartitioning, otherDistribution, _)
=>
distribution.clustering.length == otherDistribution.clustering.length &&
numPartitions == other.numPartitions && areKeysCompatible(otherSpec) &&
+ // The key rows are compared at their types, since
`InternalRowComparableWrapper.equals`
+ // compares those first. Two empty key lists compare equal whatever
they describe, so the
+ // key space is asked separately: without that, a join between two
sides whose partitions
+ // were all pruned would call two different spaces one layout, and
+ // `ShuffledJoin.outputPartitioning` would then report both as
alternative descriptions of
+ // it. Where a key row exists this clause is implied.
+ partitioning.keyDataTypes == otherPartitioning.keyDataTypes &&
Review Comment:
Documented in
[`d839aee`](https://github.com/apache/spark/commit/d839aeeb3146f07d69b4cebda2b7ed9efdb2d9ff),
with your argument: an empty layout holds no row, so every claim over it is
vacuous, and a merge that later brings real rows under it rewrites each
member's expressions through `reducersBothWays` or a `GroupPartitionsExec`
first.
I left the same-function-per-position requirement out. It would have the
gate compare transforms rather than types, which is wider than this PR, and the
pair it would refuse cannot produce a wrong answer. Thank you for the probe.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]