peter-toth commented on code in PR #60030:
URL: https://github.com/apache/spark/pull/60030#discussion_r4456685990


##########
sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala:
##########
@@ -389,6 +389,95 @@ trait KeyGroupedPartitioningRuntimeFilterTests extends 
KeyGroupedPartitioningSui
       checkAnswer(df, Seq(Row(1L, 42.0f), Row(1L, 44.0f)))
     }
   }
+
+  test("SPARK-60752: a DPP subquery's build side does not change the layout of 
the scan it " +
+    "shares with the join") {
+    // A DPP subquery that does not reuse a broadcast plans its build side 
again, over the same
+    // `Scan` as the join's `purchases` scan. A scalar subquery filter on 
`item_id` is a runtime
+    // filter of both, and the subquery runs it first, which prunes the shared 
`Scan`. The join's
+    // scan is a copy made after planning, when the scalar subquery is 
replaced by a reused one. Had
+    // it planned its partitions again, it would report fewer of them than 
`items`, and the join
+    // would fail with "Can't zip RDDs". Of the two suites that run this, only 
the Catalyst one
+    // prunes on these predicates.
+    withSQLConf(
+        SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1",
+        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") {
+      createTable(items, itemsColumns, Array(identity("id")))
+      sql(s"INSERT INTO testcat.ns.$items VALUES " + (1 to 7).map(i =>
+        s"($i, 'x', 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(", "))
+
+      def bound(agg: String): String =
+        s"(SELECT $agg(item_id) FROM testcat.ns.$purchases WHERE price > 4.0)"
+      Seq(true, false).foreach { aqe =>
+        withSQLConf(SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> aqe.toString) {
+          Seq(s"i.id = ${bound("max")}" -> Seq(7), s"i.id >= ${bound("min")}" 
-> (5 to 7)).foreach {
+            case (filter, ids) =>
+              val df = sql(s"SELECT i.id, p.price FROM testcat.ns.$items i " +
+                s"JOIN testcat.ns.$purchases p ON i.id = p.item_id WHERE 
$filter")
+              checkAnswer(df, ids.map(i => Row(i.toLong, i.toFloat)))
+              val plan = df.queryExecution.executedPlan
+              assert(collectShuffles(plan).isEmpty,
+                s"aqe=$aqe, $filter: the join should stay 
storage-partitioned:\n$plan")
+          }
+        }
+      }
+    }
+  }
+
+  test("SPARK-60752: a nested DPP filter on a DPP subquery's build side does 
not change the " +
+    "layout of the scan it shares with the join") {
+    // As above, with a DPP filter from a broadcast dimension as the build 
side's own runtime
+    // filter, which every fixture prunes on. Two rows per key give 
`purchases` two splits per key,
+    // so a GroupPartitionsExec groups that side and checks the layout of the 
copied scan.
+    val dim = "dim"
+    withSQLConf(

Review Comment:
   Thanks, done in 
[c016753](https://github.com/apache/spark/pull/60030/commits/c016753d73373f5bdd2a3a0cc65eb9f97f730179)
 with `withDppSubqueryTables` and `checkStoragePartitionedJoin`.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/BatchScanExec.scala:
##########
@@ -181,3 +188,24 @@ case class BatchScanExec(
     s"BatchScan ${table.name()}".trim
   }
 }
+
+/**

Review Comment:
   Thanks, done in 
[474ef79](https://github.com/apache/spark/pull/60030/commits/474ef79bb89401903919eefd1d8495443a7b6843).
 The contract is in the `PlannedScan` doc, and the `@param` points there.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -126,9 +126,10 @@ case class GroupPartitionsExec(
    * The comparison is by reference for a child that reports the same object, 
which the columnar and
    * codegen wrappers do. A rule that rewrites a subquery in the runtime or 
storage filters of a
    * `BatchScanExec` after `EnsureRequirements` rebuilds it, so there it 
compares the partition keys
-   * by value, and reads equal only while the connector returns the same input 
partitions for the
-   * same scan. Such rules are `PlanAdaptiveDynamicPruningFilters` and 
`ReuseAdaptiveSubquery` with
-   * AQE, and `ReuseExchangeAndSubquery` without it.
+   * by value. Such rules are `PlanAdaptiveDynamicPruningFilters` and 
`ReuseAdaptiveSubquery` with
+   * AQE, and `ReuseExchangeAndSubquery` without it. The rebuilt node shares 
the input partitions
+   * the first one planned (`BatchScanExec.plannedInputPartitions`), so its 
keys come from the same

Review Comment:
   Agreed, it predates this PR and is small next to the planning, so I left it.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/BatchScanExec.scala:
##########
@@ -181,3 +188,24 @@ case class BatchScanExec(
     s"BatchScan ${table.name()}".trim
   }
 }
+
+/**
+ * The input partitions of a [[BatchScanExec]], planned once, by the first 
node that asks for them.
+ * Each newly built node gets its own instance, and the copies later rules 
make of the node share

Review Comment:
   Agreed, thanks for counting.



-- 
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]

Reply via email to