dongjoon-hyun commented on code in PR #60030:
URL: https://github.com/apache/spark/pull/60030#discussion_r4470331673
##########
sql/core/src/test/scala/org/apache/spark/sql/execution/SparkPlanSuite.scala:
##########
@@ -321,6 +322,29 @@ class SparkPlanSuite extends SharedSparkSession {
assert(canonicalizedTypes == Seq(IntegerType, StringType),
s"Expected [IntegerType, StringType] but got $canonicalizedTypes")
}
+
+ test("SPARK-60752: copies of a BatchScanExec share its Batch and the
partitions it planned") {
+ // A `Scan` that returns a new `Batch` from each `toBatch`, which a
connector may do.
+ class NewBatchScan extends Scan {
+ override def readSchema(): StructType = new StructType()
+ override def toBatch: Batch = new Batch {
+ override def planInputPartitions(): Array[InputPartition] = Array(new
InputPartition {})
+ override def createReaderFactory(): PartitionReaderFactory =
+ throw new UnsupportedOperationException()
+ }
+ }
+ val scan = BatchScanExec(Seq.empty, new PlannedScan(new NewBatchScan),
Seq.empty, table = null)
+ def sharesPlanning(copy: SparkPlan): Boolean = copy match {
+ case c: BatchScanExec =>
+ (c.batch eq scan.batch) && (c.inputPartitions eq scan.inputPartitions)
Review Comment:
**24. [Nit] No test checks that the copies share the reader factory**
Thanks for moving the reader factory into the holder (item 17). Here
`createReaderFactory()` throws (L332-L333) and `sharesPlanning` compares the
`Batch` and the partitions only, so moving `readerFactory` back to a lazy val
of each node would still pass every test. If `createReaderFactory()` returned a
new factory on each call, one whose `createReader` throws, this could also
compare the factories:
```scala
(c.batch eq scan.batch) && (c.inputPartitions eq
scan.inputPartitions) &&
(c.readerFactory eq scan.readerFactory)
```
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2Strategy.scala:
##########
@@ -239,8 +239,8 @@ class DataSourceV2Strategy(session: SparkSession) extends
Strategy with Predicat
val storageFilters = storageFiltersFor(relation, filtersAbove)
val checked = StorageFilterPlanning.checkedColumn(
session.sessionState.conf, storageFilters, relation.output)
- val batchExec = BatchScanExec(relation.output ++ checked, relation.scan,
runtimeFilters,
- relation.ordering, relation.relation.table, keyGroupedPartitioning,
+ val batchExec = BatchScanExec(relation.output ++ checked, new
PlannedScan(relation.scan),
+ runtimeFilters, relation.ordering, relation.relation.table,
keyGroupedPartitioning,
Review Comment:
**22. [Nit] The description still says the copies read the live bucketing
conf**
After the rebase onto SPARK-60260, this line passes the
`keyGroupedPartitioning` decided once at planning (L228-L232), and
`reportedKeyedPartitioning` no longer reads the conf. The first section of the
description still ends with "The copies still decide their keyed layout from
the live `spark.sql.sources.v2.bucketing.enabled`. SPARK-60260 (#59789) moves
that decision to `DataSourceV2Strategy`." Since the merge script puts the
description into the commit message, that would record a limitation that master
no longer has. Could the paragraph go, or say that SPARK-60260 already decides
it here?
Also, branch-4.x does not have SPARK-60260, so for "branch-4.x will take the
same change" this argument needs SPARK-60260 there first, or
`relation.keyGroupedPartitioning` as before.
##########
sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala:
##########
@@ -511,6 +511,82 @@ trait KeyGroupedPartitioningRuntimeFilterTests extends
KeyGroupedPartitioningSui
}
}
}
+
+ /**
+ * 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
off only, since with
Review Comment:
**20. [Low] With AQE on, the base commit also fails for a scan that declares
its columnar support mode**
This corrects my item 15, which I based on the in-memory fixtures. The
reason given here, that with AQE on the copies plan their partitions when the
result stage is created, holds only for a scan in the default
`PARTITION_DEFINED` mode. Only that mode reads the partitions in
`supportsColumnar` (`DataSourceV2ScanExecBase.scala` L200-L211). `SUPPORTED`
and `UNSUPPORTED` return without planning anything, which is what SPARK-44505
added them for (`DataSourceV2Suite.scala` L477-L492).
Reading the AQE path on the base commit, for a keyed scan with runtime
filtering whose `columnarSupportMode()` returns `UNSUPPORTED`, with the confs
of `withDppSubqueryTables` and AQE on, the default:
1. At result-stage creation, `ReuseAdaptiveSubquery` (first test) or
`PlanAdaptiveDynamicPruningFilters` (nested test, `flag = 1`) copies the
`purchases` scan. `ApplyColumnarRulesAndInsertTransitions` asks the copy
`supportsColumnar`, which plans nothing in this mode, and with no shuffle stage
nothing validates the plan.
2. The sort-merge join executes `items` first, which waits for the DPP
subquery in `waitForSubqueries`. The subquery's build side calls `filter()` on
the shared `Scan`.
3. Only then is the copy asked, by `checkChildStillMatches`
(`GroupPartitionsExec.scala` L355-L360), which `doExecute` calls first, or by
its own `filteredPartitions`. On the base commit the copy plans from the pruned
`Scan`, so the query fails with the INTERNAL_ERROR or `Can't zip RDDs ...
List(7, 1)`, the same as with AQE off.
This PR fixes that case too, since the copy shares the `PlannedScan` that
`EnsureRequirements` planned. But the description scopes the bug to AQE off
("With AQE off:" and the AQE-on note in the testing section), and no test runs
this path. No in-repo source reaches it: the keyed in-memory fixtures keep the
default mode, and the sources that declare a mode report no partitioning. So
the exposure is external connectors, with the default AQE.
Could the doc here and the description say that this holds for
`PARTITION_DEFINED` scans, and that a scan that declares `SUPPORTED` or
`UNSUPPORTED` fails the same way with AQE on? An AQE-on run of these checks
over such a scan would also cover the fix, for example with a catalog like
`CustomReportingCatalog` whose scan builder returns an `InMemoryBatchScan` that
overrides `columnarSupportMode()`. It would need `flag = 1` or the first test's
query, since with `flag = 2` the empty broadcast lets AQE drop the join (item
14). I traced this from the code and did not run it.
--
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]