peter-toth commented on code in PR #59789:
URL: https://github.com/apache/spark/pull/59789#discussion_r4425648141
##########
sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala:
##########
@@ -389,6 +389,106 @@ trait KeyGroupedPartitioningRuntimeFilterTests extends
KeyGroupedPartitioningSui
checkAnswer(df, Seq(Row(1L, 42.0f), Row(1L, 44.0f)))
}
}
+
+ test("SPARK-60260: a storage-partitioned join keeps the planned keyed layout
when v2 bucketing " +
+ "is disabled after planning") {
+ // reportedKeyedPartitioning used to read
spark.sql.sources.v2.bucketing.enabled at execution,
+ // so turning it off between planning and execution dropped the keyed
layout on the scan that
+ // PlanAdaptiveDynamicPruningFilters rebuilds at stage creation, and the
sort-merge join then
+ // lined partitions up wrong. The decision is frozen at planning now.
Covered across three
+ // shapes, each planned with bucketing on and then executed with it off,
AQE on.
+ val letters = Seq("a", "b", "c", "d", "e", "f", "g")
+
+ // Plan under bucketing on (so the keyed layout and DPP are decided here),
assert the shape,
+ // then execute with bucketing off and check both the rows and that the
rebuilt keyed scan kept
+ // its KeyedPartitioning. checkToRDD = false runs only the committed plan,
not a new one.
+ def check(filter: String, expected: Seq[Row], groupPartitions: Boolean,
shuffle: Boolean)
+ : Unit = {
+ val df = sql(s"SELECT i.id, i.name, p.price FROM testcat.ns.$items i " +
+ s"JOIN testcat.ns.$purchases p ON i.id = p.item_id WHERE $filter")
+ val plan = df.queryExecution.executedPlan
+ assert(collect(plan) { case j: SortMergeJoinExec => j }.nonEmpty,
+ s"expected a sort-merge join:\n$plan")
+ assert(collectScans(plan).exists(_.runtimeFilters.nonEmpty),
+ s"expected dynamic partition pruning on the keyed scan:\n$plan")
+ assert(collectGroupPartitions(plan).nonEmpty == groupPartitions,
+ s"expected GroupPartitionsExec present == $groupPartitions:\n$plan")
+ assert(collectShuffles(plan).nonEmpty == shuffle,
+ s"expected a shuffle present == $shuffle:\n$plan")
+
+ withSQLConf(SQLConf.V2_BUCKETING_ENABLED.key -> "false") {
+ // Without checkToRDD, so only the committed plan runs, not a newly
planned one.
+ QueryTest.checkAnswer(df, expected, checkToRDD = false)
+ // Re-collected, because under AQE executedPlan only reaches the
rebuilt scans once the
+ // query has run. Without this fix the rebuilt keyed scan reports
UnknownPartitioning here.
+ val executed = df.queryExecution.executedPlan
+ // The scan with the DPP filter is the one
PlanAdaptiveDynamicPruningFilters rebuilds.
+ val rebuilt = collectScans(executed).filter(_.runtimeFilters.nonEmpty)
+ assert(rebuilt.nonEmpty &&
+ rebuilt.forall(_.outputPartitioning.isInstanceOf[KeyedPartitioning]),
+ s"expected the rebuilt scan to keep its keyed layout:\n$executed")
+ }
+ }
+
+ withSQLConf(
+ SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1",
+ SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "true",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_ENABLED.key -> "true",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_REUSE_BROADCAST_ONLY.key -> "false",
+ SQLConf.DYNAMIC_PARTITION_PRUNING_FALLBACK_FILTER_RATIO.key -> "10") {
+
+ // Shape 1: one split per key on both sides, no GroupPartitionsExec
above the join. Without
+ // this fix it returns 2 of the 7 rows, and fails with "Can't zip RDDs"
once a filter prunes.
+ withTable(s"testcat.ns.$items", s"testcat.ns.$purchases") {
+ createTable(items, itemsColumns, Array(identity("id")))
+ sql(s"INSERT INTO testcat.ns.$items VALUES " + (1 to 7).map(i =>
+ s"($i, '${letters(i - 1)}', 10.0, cast('2020-01-01' as
timestamp))").mkString(", "))
+ createTable(purchases, purchasesColumns, Array(identity("item_id")))
+ sql(s"INSERT INTO testcat.ns.$purchases VALUES " + (1 to 7).map(i =>
+ s"($i, $i.0, cast('2020-01-01' as timestamp))").mkString(", "))
+ check("p.price > 0.0", (1 to 7).map(i => Row(i.toLong, letters(i - 1),
i.toFloat)),
+ groupPartitions = false, shuffle = false)
+ check("p.price > 5.0", Seq(6, 7).map(i => Row(i.toLong, letters(i -
1), i.toFloat)),
+ groupPartitions = false, shuffle = false)
+ }
+
+ // Shape 2: two rows per id on items, so a GroupPartitionsExec sits
above the pruned scan.
+ // Without this fix it fails with INTERNAL_ERROR (the node's child no
longer reports the
+ // partitioning it was planned over).
+ withTable(s"testcat.ns.$items", s"testcat.ns.$purchases") {
+ createTable(items, itemsColumns, Array(identity("id")))
+ sql(s"INSERT INTO testcat.ns.$items VALUES " + (1 to 7).flatMap(i =>
Seq(
+ s"($i, '${letters(i - 1)}', 10.0, cast('2020-01-01' as timestamp))",
+ s"($i, '${letters(i - 1)}', 11.0, cast('2020-01-02' as
timestamp))")).mkString(", "))
+ createTable(purchases, purchasesColumns, Array(identity("item_id")))
+ sql(s"INSERT INTO testcat.ns.$purchases VALUES " + (1 to 7).map(i =>
+ s"($i, $i.0, cast('2020-01-01' as timestamp))").mkString(", "))
+ check("p.price > 0.0",
+ (1 to 7).flatMap(i => Seq.fill(2)(Row(i.toLong, letters(i - 1),
i.toFloat))),
+ groupPartitions = true, shuffle = false)
+ check("p.price > 5.0",
+ Seq(6, 7).flatMap(i => Seq.fill(2)(Row(i.toLong, letters(i - 1),
i.toFloat))),
+ groupPartitions = true, shuffle = false)
+ }
+
+ // Shape 3: purchases is not partitioned and the SPJ shuffle is on, so
it is shuffled onto the
+ // keyed items scan. Without this fix it returns 2 of 7, or fails to zip
once a filter prunes.
+ withSQLConf(SQLConf.V2_BUCKETING_SHUFFLE_ENABLED.key -> "true") {
+ withTable(s"testcat.ns.$items", s"testcat.ns.$purchases") {
+ createTable(items, itemsColumns, Array(identity("id")))
+ sql(s"INSERT INTO testcat.ns.$items VALUES " + (1 to 7).map(i =>
+ s"($i, '${letters(i - 1)}', 10.0, cast('2020-01-01' as
timestamp))").mkString(", "))
+ createTable(purchases, purchasesColumns, Array.empty[Transform])
+ sql(s"INSERT INTO testcat.ns.$purchases VALUES " + (1 to 7).map(i =>
+ s"($i, $i.0, cast('2020-01-01' as timestamp))").mkString(", "))
+ check("p.price > 0.0", (1 to 7).map(i => Row(i.toLong, letters(i -
1), i.toFloat)),
+ groupPartitions = false, shuffle = true)
+ check("p.price > 5.0", Seq(6, 7).map(i => Row(i.toLong, letters(i -
1), i.toFloat)),
+ groupPartitions = false, shuffle = true)
+ }
+ }
Review Comment:
**Finding 13.** `DataSourceV2Strategy` also routes a scalar subquery over
the keyed column into the scan's runtime filters (`scalarSubqueryFilters`), and
that path breaks on master the same way. With shape 1's tables:
```sql
SELECT i.id, i.name, p.price FROM items i JOIN purchases p ON i.id =
p.item_id
WHERE i.id >= (SELECT min(item_id) FROM purchases WHERE price > 4.0)
```
Planned with bucketing on and run with it off, with DPP off:
- With AQE off, master returns 1 of the 3 rows. Against
`InMemoryCatalystRuntimeFilterCatalog`, it fails with `Can't zip RDDs`.
- With AQE on, master returns the right rows, but the scan whose runtime
filter now reads a `ReusedSubquery` no longer reports a `KeyedPartitioning`.
- Here, all four are right, and the scan keeps its keyed layout.
A fourth shape pins it:
```scala
// Shape 4: a scalar subquery over the keyed column is a runtime
filter of the keyed scan too.
// Without this fix it returns 1 of 3 rows with AQE off, or fails to
zip with the Catalyst
// runtime filters.
Seq(true, false).foreach { aqe =>
withSQLConf(
SQLConf.DYNAMIC_PARTITION_PRUNING_ENABLED.key -> "false",
SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> aqe.toString) {
withTable(s"testcat.ns.$items", s"testcat.ns.$purchases") {
createTable(items, itemsColumns, Array(identity("id")))
sql(s"INSERT INTO testcat.ns.$items VALUES " + (1 to 7).map(i =>
s"($i, '${letters(i - 1)}', 10.0, cast('2020-01-01' as
timestamp))").mkString(", "))
createTable(purchases, purchasesColumns,
Array(identity("item_id")))
sql(s"INSERT INTO testcat.ns.$purchases VALUES " + (1 to
7).map(i =>
s"($i, $i.0, cast('2020-01-01' as timestamp))").mkString(", "))
check(s"i.id >= (SELECT min(item_id) FROM testcat.ns.$purchases
WHERE price > 4.0)",
(5 to 7).map(i => Row(i.toLong, letters(i - 1), i.toFloat)),
groupPartitions = false, shuffle = false)
}
}
}
```
It goes after shape 3, inside the outer `withSQLConf`. With it, the test
passes here in both runtime-filter suites. On master, this shape alone fails in
both. DPP stays off because with it on and AQE off, the Catalyst runtime
filters hit the separate failure in finding 14.
--
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]