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


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/BatchScanExec.scala:
##########
@@ -108,8 +112,7 @@ case class BatchScanExec(
   @transient lazy val prunedKeyGroupedPartitioning: Option[Seq[Expression]] =
     keyGroupedPartitioning.map(_.filter(_.references.subsetOf(outputSet)))
 
-  @transient override lazy val inputPartitions: Seq[InputPartition] =
-    batch.planInputPartitions().toImmutableArraySeq
+  override def inputPartitions: Seq[InputPartition] = 
plannedScan.inputPartitions

Review Comment:
   Thanks, I rebased onto master. The SPARK-60618 test now gives the rebuilt 
node a new `PlannedScan`, and the comment above it says why: 
[b1cfddb](https://github.com/apache/spark/pull/60030/commits/b1cfddb02bd6a749c355115df833e374ab22e123).



##########
sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala:
##########
@@ -389,6 +389,80 @@ trait KeyGroupedPartitioningRuntimeFilterTests extends 
KeyGroupedPartitioningSui
       checkAnswer(df, Seq(Row(1L, 42.0f), Row(1L, 44.0f)))
     }
   }
+
+  /**
+   * Runs `f` over `items` and `purchases`, both partitioned by the ids 1 to 
7, with `rowsPerKey`
+   * rows in each `purchases` key, under the confs of a DPP subquery that 
reuses no broadcast.
+   */
+  private def withDppSubqueryTables(rowsPerKey: Int)(f: => Unit): Unit = {
+    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).flatMap(i =>
+        Seq.fill(rowsPerKey)(s"($i, $i.0, cast('2020-01-01' as 
timestamp))")).mkString(", "))
+      f
+    }
+  }
+
+  /** Checks the rows of `query`, and that its join plans no shuffle, with AQE 
on and off. */
+  private def checkStoragePartitionedJoin(query: String, expected: Seq[Row]): 
Unit = {
+    Seq(true, false).foreach { aqe =>
+      withSQLConf(SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> aqe.toString) {
+        val df = sql(query)
+        checkAnswer(df, expected)
+        val plan = df.queryExecution.executedPlan
+        assert(collectShuffles(plan).isEmpty,
+          s"aqe=$aqe: the join should stay storage-partitioned:\n$plan")
+      }
+    }
+  }
+
+  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 this predicate.
+    withDppSubqueryTables(rowsPerKey = 1) {
+      checkStoragePartitionedJoin(
+        s"SELECT i.id, p.price FROM testcat.ns.$items i JOIN 
testcat.ns.$purchases p " +
+          s"ON i.id = p.item_id WHERE i.id = " +
+          s"(SELECT max(item_id) FROM testcat.ns.$purchases WHERE price > 
4.0)",
+        Seq(Row(7L, 7.0f)))
+    }
+  }
+
+  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. With
+    // `flag = 2` the build side's filter prunes every split.
+    val dim = "dim"
+    withDppSubqueryTables(rowsPerKey = 2) {
+      createTable(dim, Array(Column.create("k", LongType), 
Column.create("flag", IntegerType)),
+        Array.empty)
+      sql(s"INSERT INTO testcat.ns.$dim VALUES " +
+        (1 to 7).map(i => s"($i, ${if (i >= 5) 1 else 0})").mkString(", "))
+      Seq(1 -> (5 to 7), 2 -> Nil).foreach { case (flag, ids) =>

Review Comment:
   Thanks, the check runs with AQE off only now (item 15), where the fully 
pruned case keeps the join: 
[b1cfddb](https://github.com/apache/spark/pull/60030/commits/b1cfddb02bd6a749c355115df833e374ab22e123).



##########
sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala:
##########
@@ -389,6 +389,80 @@ trait KeyGroupedPartitioningRuntimeFilterTests extends 
KeyGroupedPartitioningSui
       checkAnswer(df, Seq(Row(1L, 42.0f), Row(1L, 44.0f)))
     }
   }
+
+  /**
+   * Runs `f` over `items` and `purchases`, both partitioned by the ids 1 to 
7, with `rowsPerKey`
+   * rows in each `purchases` key, under the confs of a DPP subquery that 
reuses no broadcast.
+   */
+  private def withDppSubqueryTables(rowsPerKey: Int)(f: => Unit): Unit = {
+    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).flatMap(i =>
+        Seq.fill(rowsPerKey)(s"($i, $i.0, cast('2020-01-01' as 
timestamp))")).mkString(", "))
+      f
+    }
+  }
+
+  /** Checks the rows of `query`, and that its join plans no shuffle, with AQE 
on and off. */
+  private def checkStoragePartitionedJoin(query: String, expected: Seq[Row]): 
Unit = {
+    Seq(true, false).foreach { aqe =>

Review Comment:
   Thanks, agreed. `checkStoragePartitionedJoin` runs with AQE off only now, 
and its doc says why: 
[b1cfddb](https://github.com/apache/spark/pull/60030/commits/b1cfddb02bd6a749c355115df833e374ab22e123).
 That matches what I measured: the AQE-on runs pass without the fix too.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/BatchScanExec.scala:
##########
@@ -181,3 +184,21 @@ case class BatchScanExec(
     s"BatchScan ${table.name()}".trim
   }
 }
+
+/**
+ * A `Scan`, with the `Batch` and the input partitions a [[BatchScanExec]] 
plans from it once, when
+ * a node first asks for them. The copies later rules make of the node share 
it, so they read the

Review Comment:
   Thanks, done in 
[b1cfddb](https://github.com/apache/spark/pull/60030/commits/b1cfddb02bd6a749c355115df833e374ab22e123).
 The `PlannedScan` doc and the description say that a rule that rebuilds a node 
over the same `Scan` passes on its `plannedScan` or uses `copy`. A new 
`PlannedScan` is for a node that is planned again, or one over another `Scan`.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/BatchScanExec.scala:
##########
@@ -181,3 +184,21 @@ case class BatchScanExec(
     s"BatchScan ${table.name()}".trim
   }
 }
+
+/**
+ * A `Scan`, with the `Batch` and the input partitions a [[BatchScanExec]] 
plans from it once, when
+ * a node first asks for them. The copies later rules make of the node share 
it, so they read the
+ * partitions the first node planned, with a reader factory from the `Batch` 
that planned them.
+ * The partitions have to be the planned ones, because the `Scan` can be 
shared with another node
+ * whose runtime filter prunes it first, e.g. the build side of a DPP subquery 
(SPARK-60752). A
+ * pushed runtime filter replans them from a new `Batch`, but keeps the 
planned `Batch`'s reader
+ * factory.
+ */
+final class PlannedScan(val scan: Scan) {
+  lazy val batch: Batch = scan.toBatch
+  lazy val inputPartitions: Seq[InputPartition] = 
batch.planInputPartitions().toImmutableArraySeq

Review Comment:
   Thanks, done in 
[b1cfddb](https://github.com/apache/spark/pull/60030/commits/b1cfddb02bd6a749c355115df833e374ab22e123).
 The `PlannedScan` builds the reader factory once, and `readerFactory` reads it 
from there.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/BatchScanExec.scala:
##########
@@ -181,3 +184,21 @@ case class BatchScanExec(
     s"BatchScan ${table.name()}".trim
   }
 }
+
+/**
+ * A `Scan`, with the `Batch` and the input partitions a [[BatchScanExec]] 
plans from it once, when
+ * a node first asks for them. The copies later rules make of the node share 
it, so they read the
+ * partitions the first node planned, with a reader factory from the `Batch` 
that planned them.
+ * The partitions have to be the planned ones, because the `Scan` can be 
shared with another node
+ * whose runtime filter prunes it first, e.g. the build side of a DPP subquery 
(SPARK-60752). A
+ * pushed runtime filter replans them from a new `Batch`, but keeps the 
planned `Batch`'s reader
+ * factory.
+ */
+final class PlannedScan(val scan: Scan) {
+  lazy val batch: Batch = scan.toBatch
+  lazy val inputPartitions: Seq[InputPartition] = 
batch.planInputPartitions().toImmutableArraySeq
+}
+
+object PlannedScan {

Review Comment:
   Thanks. I tried it in 
[b1cfddb](https://github.com/apache/spark/pull/60030/commits/b1cfddb02bd6a749c355115df833e374ab22e123),
 but I'd rather keep the extractor, and 
[c09f3b9](https://github.com/apache/spark/pull/60030/commits/c09f3b9ce21b2cf6d3daadcc71da65e883e5fe46)
 restores it with the compact patterns. It gives code that targets this line a 
compact form, and `BatchScanExec.scan` stays for code that builds against 
several releases.



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