andygrove commented on code in PR #6786:
URL: https://github.com/apache/datafusion-comet/pull/6786#discussion_r4241895250
##########
spark/src/test/scala/org/apache/comet/exec/CometExecSuite.scala:
##########
@@ -301,6 +301,366 @@ class CometExecSuite extends CometTestBase {
}
}
+ test("broadcast build side with unsupported text source goes native
(#6008)") {
Review Comment:
Here it is. It runs as its own suite under
`spark/src/test/scala/org/apache/comet/`. Each query goes through
`checkSparkAnswer`, and the printed line shows whether the bridge and the
native join were planned. All three tests pass on `3738210b84` with 3.4, and
the first two also pass on 4.1. For the PR, asserting the bridge and the
`CometBroadcastHashJoinExec` instead of printing them should be enough.
<details><summary><code>Pr6786BuildSideProbeSuite.scala</code></summary>
```scala
package org.apache.comet
import org.apache.spark.sql.{CometTestBase, DataFrame}
import org.apache.spark.sql.catalyst.expressions.{DynamicPruningExpression,
Literal}
import org.apache.spark.sql.comet._
import org.apache.spark.sql.execution._
import org.apache.spark.sql.execution.adaptive.{AdaptiveSparkPlanExec,
QueryStageExec}
import org.apache.spark.sql.execution.exchange.ReusedExchangeExec
import org.apache.spark.sql.internal.SQLConf
// Probes from the PR #6786 review: ORC and Iceberg build sides that Comet
cannot scan natively,
// bridged by spark.comet.convert.broadcastBuildSide.enabled. Each query is
compared with Spark
// through checkSparkAnswer, and the summary line shows whether the bridge
and the native join
// were planned.
class Pr6786BuildSideProbeSuite extends CometTestBase with
CometIcebergTestBase {
/** Every node, descending through AQE, stages and subqueries, but not
into reused exchanges. */
private def allNodes(p: SparkPlan): Seq[SparkPlan] = {
val kids = p match {
case a: AdaptiveSparkPlanExec => Seq(a.executedPlan)
case s: QueryStageExec => Seq(s.plan)
case _: ReusedExchangeExec => Seq.empty
case other => other.children
}
val subs = p.expressions.flatMap(_.collect { case e:
ExecSubqueryExpression => e.plan })
Seq(p) ++ kids.flatMap(allNodes) ++ subs.flatMap(allNodes)
}
private def summary(df: DataFrame): String = {
df.collect()
val nodes = allNodes(df.queryExecution.executedPlan)
s"bridges=${nodes.count(_.isInstanceOf[CometSparkToColumnarExec])} " +
s"cometBHJ=${nodes.count(_.isInstanceOf[CometBroadcastHashJoinExec])}
" +
s"icebergNative=${nodes.count(_.isInstanceOf[CometIcebergNativeScanExec])}"
}
private def dppSummary(df: DataFrame): String = {
df.collect()
val nodes = allNodes(df.queryExecution.executedPlan)
val dppTrue = nodes.exists(_.expressions.exists(_.exists {
case DynamicPruningExpression(Literal.TrueLiteral) => true
case _ => false
}))
val dppLive = nodes.exists(_.expressions.exists(_.exists {
case DynamicPruningExpression(e) if e != Literal.TrueLiteral => true
case _ => false
}))
summary(df) + s" dppLive=$dppLive dppTrue=$dppTrue " +
s"reusedX=${nodes.count(_.isInstanceOf[ReusedExchangeExec])}"
}
test("ORC build side, all types, non-UTC session (#6008)") {
withTempDir { dir =>
val fact = s"${dir.getAbsolutePath}/fact.parquet"
val dim = s"${dir.getAbsolutePath}/dim.orc"
withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
spark.range(0, 2000).selectExpr("CAST(id % 60 AS INT) AS k", "id AS
v").write.parquet(fact)
spark
.range(0, 60, 1, 3)
.selectExpr(
"CAST(id AS INT) AS k",
"IF(id % 7 = 0, NULL, CAST(id AS BYTE)) AS b",
"IF(id % 7 = 1, NULL, CAST(id AS SHORT)) AS s",
"IF(id % 7 = 2, NULL, id * 1000000000L) AS l",
"IF(id % 7 = 3, NULL, id % 2 = 0) AS bo",
"IF(id % 7 = 4, NULL, CAST(id AS FLOAT) / 3) AS f",
"CASE WHEN id % 7 = 5 THEN NULL WHEN id = 6 THEN CAST('NaN' AS
DOUBLE) " +
"WHEN id = 8 THEN -0.0D ELSE id / 3.0D END AS d",
"IF(id % 7 = 6, NULL, CAST(id * 1.25 AS DECIMAL(10,2))) AS dec1",
"IF(id % 5 = 0, NULL, CAST(id AS DECIMAL(38,10)) / 7) AS dec2",
"IF(id % 5 = 1, NULL, CONCAT('s', id, 'é')) AS str",
"IF(id % 5 = 2, NULL, CAST(CONCAT('b', id) AS BINARY)) AS bin",
"IF(id % 5 = 3, NULL, DATE_ADD(DATE'1960-01-01', CAST(id * 300
AS INT))) AS dt",
"IF(id % 5 = 4, NULL, TIMESTAMP_MICROS(id * 8640000000000L -
900000000000000L)) AS ts",
"IF(id % 9 = 0, NULL, " +
"CAST(TIMESTAMP_MICROS(id * 8640000000000L - 900000000000000L)
AS TIMESTAMP_NTZ)) " +
"AS ntz",
"IF(id % 6 = 0, NULL, named_struct('a', CAST(id AS INT), 'b', " +
"IF(id % 4 = 0, NULL, CONCAT('x', id)))) AS st",
"CASE WHEN id % 6 = 1 THEN NULL WHEN id % 6 = 2 THEN
CAST(array() AS ARRAY<STRING>) " +
"ELSE array(CONCAT('e', id), NULL, 'z') END AS arr",
"CASE WHEN id % 6 = 3 THEN NULL WHEN id % 6 = 4 THEN " +
"CAST(map() AS MAP<STRING,STRING>) " +
"ELSE map(CONCAT('k', id), IF(id % 2 = 0, NULL, 'v')) END AS
m")
.write
.orc(dim)
}
val query =
"""SELECT /*+ BROADCAST(d) */ f.v, d.*, hour(d.ts) AS h, CAST(d.ts
AS STRING) AS tss,
| CAST(d.ntz AS STRING) AS ntzs, date_trunc('DAY', d.ts) AS tday
|FROM fact_o f JOIN dim_o d ON f.k = d.k""".stripMargin
for {
aqe <- Seq("false", "true")
v1List <- Seq("avro,csv,json,kafka,orc,parquet,text", "parquet")
(vec, nestedVec) <- Seq(("true", "true"), ("true", "false"),
("false", "false"))
} {
withSQLConf(
SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> aqe,
SQLConf.USE_V1_SOURCE_LIST.key -> v1List,
SQLConf.ORC_VECTORIZED_READER_ENABLED.key -> vec,
SQLConf.ORC_VECTORIZED_READER_NESTED_COLUMN_ENABLED.key ->
nestedVec,
SQLConf.SESSION_LOCAL_TIMEZONE.key -> "America/Los_Angeles") {
spark.read.parquet(fact).createOrReplaceTempView("fact_o")
spark.read.orc(dim).createOrReplaceTempView("dim_o")
// scalastyle:off println
println(s"ORC aqe=$aqe v1=$v1List vec=$vec nested=$nestedVec: " +
summary(sql(query)))
// scalastyle:on println
checkSparkAnswer(sql(query))
}
}
}
}
test("Iceberg build side that Comet's native reader declines (#6008)") {
assume(icebergAvailable, "Iceberg not available in classpath")
withTempIcebergDir { warehouseDir =>
withTempDir { dir =>
val fact = s"${dir.getAbsolutePath}/fact.parquet"
withSQLConf(
"spark.sql.catalog.pc" -> "org.apache.iceberg.spark.SparkCatalog",
"spark.sql.catalog.pc.type" -> "hadoop",
"spark.sql.catalog.pc.warehouse" -> warehouseDir.getAbsolutePath) {
withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
spark
.range(0, 2000)
.selectExpr("CAST(id % 40 AS INT) AS k", "id AS v")
.write
.parquet(fact)
for ((name, props) <- Seq(
"dim_orc" -> "'write.format.default'='orc'",
"dim_pq" -> ("'format-version'='2',
'write.delete.mode'='merge-on-read', " +
"'write.update.mode'='merge-on-read'"))) {
spark.sql(s"""CREATE TABLE pc.db.$name (
| k INT, s STRING, d DECIMAL(10,2), ts TIMESTAMP,
| st STRUCT<a: INT, b: STRING>, arr
ARRAY<STRING>,
| m MAP<STRING, STRING>, region STRING)
|USING iceberg PARTITIONED BY (region)
|TBLPROPERTIES ($props)""".stripMargin)
spark
.range(0, 40, 1, 2)
.selectExpr(
"CAST(id AS INT) AS k",
"IF(id % 5 = 0, NULL, CONCAT('s', id)) AS s",
"IF(id % 6 = 0, NULL, CAST(id * 1.5 AS DECIMAL(10,2))) AS
d",
"IF(id % 7 = 0, NULL, " +
"TIMESTAMP_MICROS(id * 8640000000000L -
900000000000000L)) AS ts",
"IF(id % 4 = 0, NULL, named_struct('a', CAST(id AS INT),
'b', " +
"IF(id % 3 = 0, NULL, CONCAT('b', id)))) AS st",
"CASE WHEN id % 4 = 1 THEN NULL WHEN id % 4 = 2 THEN " +
"CAST(array() AS ARRAY<STRING>) ELSE array(CONCAT('e',
id), NULL) END AS arr",
"IF(id % 3 = 1, NULL, map(CONCAT('k', id), IF(id % 2 = 0,
NULL, 'v'))) AS m",
"IF(id % 2 = 0, 'east', 'west') AS region")
.writeTo(s"pc.db.$name")
.append()
}
spark.sql("DELETE FROM pc.db.dim_pq WHERE k % 9 = 0")
}
spark.read.parquet(fact).createOrReplaceTempView("ice_fact")
val queries = Seq(
"orc" -> ("SELECT /*+ BROADCAST(d) */ f.v, d.* FROM ice_fact f "
+
"JOIN pc.db.dim_orc d ON f.k = d.k"),
"pq-mor" -> ("SELECT /*+ BROADCAST(d) */ f.v, d.* FROM ice_fact
f " +
"JOIN pc.db.dim_pq d ON f.k = d.k"),
"snapshots" -> ("SELECT /*+ BROADCAST(s) */ f.v, s.operation,
s.summary FROM " +
"ice_fact f JOIN pc.db.dim_pq.snapshots s ON f.k =
CAST(s.snapshot_id % 2 AS INT)"))
for {
(name, query) <- queries
native <- Seq("true", "false")
aqe <- Seq("false", "true")
} {
withSQLConf(
CometConf.COMET_ICEBERG_NATIVE_ENABLED.key -> native,
SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> aqe,
SQLConf.SESSION_LOCAL_TIMEZONE.key -> "Asia/Kolkata") {
// scalastyle:off println
println(s"ICEBERG $name icebergNative=$native aqe=$aqe:
${summary(sql(query))}")
// scalastyle:on println
checkSparkAnswer(sql(query))
}
}
}
}
}
}
test("Spark 3.4: native Iceberg fact, text dimension, AQE DPP (#6008)") {
assume(icebergAvailable, "Iceberg not available in classpath")
withTempIcebergDir { warehouseDir =>
withTempDir { dir =>
val dim = s"${dir.getAbsolutePath}/dim.txt"
withSQLConf(
"spark.sql.catalog.pc" -> "org.apache.iceberg.spark.SparkCatalog",
"spark.sql.catalog.pc.type" -> "hadoop",
"spark.sql.catalog.pc.warehouse" -> warehouseDir.getAbsolutePath) {
withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
spark.sql("""CREATE TABLE pc.db.fact (v BIGINT, fk STRING)
|USING iceberg PARTITIONED BY (fk)""".stripMargin)
spark
.range(0, 1000, 1, 2)
.selectExpr("id AS v", "CAST(id % 10 AS STRING) AS fk")
.writeTo("pc.db.fact")
.append()
spark.range(0, 10).selectExpr("CAST(id AS STRING) AS
value").write.text(dim)
}
spark.read.text(dim).createOrReplaceTempView("ice_dim")
val query =
"""SELECT /*+ BROADCAST(d) */ f.v FROM pc.db.fact f JOIN
|(SELECT value FROM ice_dim WHERE CAST(value AS INT) % 3 = 0)
d ON f.fk = d.value""".stripMargin
for {
aqe <- Seq("true", "false")
feature <- Seq("true", "false")
} {
withSQLConf(
SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> aqe,
SQLConf.DYNAMIC_PARTITION_PRUNING_ENABLED.key -> "true",
SQLConf.DYNAMIC_PARTITION_PRUNING_REUSE_BROADCAST_ONLY.key ->
"true",
"spark.comet.convert.broadcastBuildSide.enabled" -> feature) {
// scalastyle:off println
val df = sql(query)
val dpp = dppSummary(df) // runs the query
val factRows =
allNodes(df.queryExecution.executedPlan).collect {
case s: CometIcebergNativeScanExec =>
s.metrics("output_rows").value
}
// With DPP live the fact scan reads 400 of its 1000 rows.
println(s"DPP aqe=$aqe feature=$feature: $dpp
factRows=${factRows.mkString(",")}")
// scalastyle:on println
checkSparkAnswer(sql(query))
}
}
}
}
}
}
}
```
</details>
##########
spark/src/main/scala/org/apache/comet/rules/CometExecRule.scala:
##########
@@ -1518,6 +1535,126 @@ case class CometExecRule(session: SparkSession,
queryStagePrep: Boolean = false)
}
}
+ /**
+ * An unsupported file-source scan is bridged to Arrow when the general
sparkToColumnar opt-in
+ * covers it, or when it feeds a broadcast join's build side (see issue
#6008). This is only
+ * reached from the unsupported-format arms, so the per-format
`COMET_CONVERT_FROM_*` opt-outs
+ * for CSV/JSON/Parquet still take precedence.
+ */
+ private def shouldBridgeUnsupportedScan(conf: SQLConf, op: SparkPlan):
Boolean =
+ isSparkToArrowEnabled(conf, op) || isBroadcastBuildSideLeaf(op)
+
+ /**
+ * True when the build-side auto-bridge should run: its own config is on and
Comet broadcast
+ * exchange conversion is enabled. Without the latter the
`BroadcastExchangeExec` cannot become
+ * a `CometBroadcastExchangeExec`, so bridging the build side would only add
a row to Arrow copy
+ * for no native join. This does not also check the join serdes
+ * (COMET_EXEC_BROADCAST_HASH_JOIN_ENABLED /
COMET_EXEC_BROADCAST_NESTED_LOOP_JOIN_ENABLED): if
+ * the exchange converts but the join does not, the broadcast arm returns
the original plan and
+ * EliminateRedundantTransitions removes the now-unused bridge, so a
leftover copy never reaches
Review Comment:
I don't think `revertBridgedBroadcasts` ever fires. During the transform the
bridge is `CometScanWrapper(nativeOp, CometSparkToColumnarExec(scan))`, and
`CometScanWrapper` is a leaf, so
`b.exists(_.isInstanceOf[CometSparkToColumnarExec])` never sees it. On
`3738210b84` the `FULL OUTER` and build-left `LEFT OUTER` nested loop joins
still end up with `BroadcastExchange <- CometColumnarToRow <- CometFilter <-
CometSparkRowToColumnar <- FileScan text`, with AQE on and off. The same
happens when the hash join declines because an operator on the probe side stays
on Spark. I used an `explode` with `spark.comet.exec.explode.enabled=false` to
get one. With DPP on top, that query scans the text dimension twice and builds
two broadcasts. With the config off it scans it once and reuses the broadcast.
I tried making the check look inside `CometScanWrapper`, and that isn't
enough on its own. With AQE off, a filtered dimension goes back to plain Spark
and DPP reuses the broadcast again. With AQE on, the stage gets prepared again
without the join above it, and the scan still has `BROADCAST_BUILD_SIDE_TAG`,
so the bridge is back in the final plan. `revertCometToSpark` also rebuilds
both a `CometSinkPlaceHolder` and the Comet node inside it from the same
`originalPlan`. With an aggregate on the build side, that gives a Spark
`Exchange` over a `CometExchange` over a Spark partial aggregate, which fails
at runtime with `ClassCastException: OffHeapColumnVector cannot be cast to
CometVector`. With a `UNION ALL` on the build side, planning fails with
`Incorrect number of children`. It also skips the `sparkFallback` overrides in
`CometNativeScanExec`, `CometIcebergNativeScanExec` and `CometLocalTopKExec`.
Could the rule remove the tag from the scans under a join that stays on
Spark and convert that build side again, instead of rebuilding the Spark nodes?
That should give exactly the plan we get today, and with the tag gone the AQE
stage pass won't bridge the scan again. Could you also add tests for a `FULL
OUTER` nested loop join in both AQE modes, with a filter, an aggregate and a
union on the build side, that assert there's no `CometSparkToColumnarExec`
under a Spark `BroadcastExchangeExec`? A DPP query where the join declines
could assert the `ReusedExchangeExec`. The docstring here on
`broadcastBuildSideBridgeEnabled` also still says
`EliminateRedundantTransitions` removes the unused bridge and that the join
configs aren't checked.
##########
spark/src/main/scala/org/apache/comet/rules/CometExecRule.scala:
##########
@@ -1518,6 +1535,126 @@ case class CometExecRule(session: SparkSession,
queryStagePrep: Boolean = false)
}
}
+ /**
+ * An unsupported file-source scan is bridged to Arrow when the general
sparkToColumnar opt-in
+ * covers it, or when it feeds a broadcast join's build side (see issue
#6008). This is only
+ * reached from the unsupported-format arms, so the per-format
`COMET_CONVERT_FROM_*` opt-outs
+ * for CSV/JSON/Parquet still take precedence.
+ */
+ private def shouldBridgeUnsupportedScan(conf: SQLConf, op: SparkPlan):
Boolean =
+ isSparkToArrowEnabled(conf, op) || isBroadcastBuildSideLeaf(op)
+
+ /**
+ * True when the build-side auto-bridge should run: its own config is on and
Comet broadcast
+ * exchange conversion is enabled. Without the latter the
`BroadcastExchangeExec` cannot become
+ * a `CometBroadcastExchangeExec`, so bridging the build side would only add
a row to Arrow copy
+ * for no native join. This does not also check the join serdes
+ * (COMET_EXEC_BROADCAST_HASH_JOIN_ENABLED /
COMET_EXEC_BROADCAST_NESTED_LOOP_JOIN_ENABLED): if
+ * the exchange converts but the join does not, the broadcast arm returns
the original plan and
+ * EliminateRedundantTransitions removes the now-unused bridge, so a
leftover copy never reaches
+ * runtime. See issue #6008.
+ */
+ private def broadcastBuildSideBridgeEnabled: Boolean =
+ CometConf.COMET_SPARK_TO_ARROW_BROADCAST_BUILD_SIDE_ENABLED.get(conf) &&
+ CometConf.COMET_EXEC_BROADCAST_EXCHANGE_ENABLED.get(conf)
+
+ /**
+ * True when `op` is a scan on a broadcast join's build side and build-side
bridging is enabled.
+ * `tagBroadcastBuildSideLeaves` only sets the tag when bridging is enabled,
but the tag lives
+ * on the physical node and can survive onto a reused instance, so re-check
here: a stale tag
+ * must not bridge a scan once the feature is turned off. A tagged scan is
bridged to Arrow even
+ * when the general sparkToColumnar path is off, letting the build branch
and the join run
+ * natively. See issue #6008.
+ */
+ private def isBroadcastBuildSideLeaf(op: SparkPlan): Boolean =
+ broadcastBuildSideBridgeEnabled &&
+ op.getTagValue(CometExecRule.BROADCAST_BUILD_SIDE_TAG).isDefined
+
+ /**
+ * Tag the file-source scan leaves on a broadcast join's build side so
+ * shouldApplySparkToColumnar can bridge an unsupported build-side scan
(e.g. a Text scan) to
+ * Arrow, letting the whole join run natively. The build side is usually
small (auto-broadcasts
+ * are capped by the broadcast threshold), so the row to Arrow copy is
usually cheap - but that
+ * is a heuristic, not a bound: an explicit BROADCAST hint can force a
larger build side, and a
+ * selective filter above the scan means the bridge copies the pre-filter
scan output. See issue
+ * #6008.
+ *
+ * The bridge is applied ONLY when the probe side is already natively
scannable, i.e. the join
+ * can actually become a fully native CometBroadcastHashJoinExec. Bridging a
build side under a
+ * join that stays on Spark is pure overhead and, worse, it rewrites the
build broadcast's
+ * subtree to Comet and breaks Spark's DPP broadcast reuse (the DPP subquery
keeps an unbridged
+ * copy of the same scan, so the two exchanges no longer share a canonical
form). CometScanRule
+ * has already run by this point, so a native probe scan is a Comet scan and
an unsupported
+ * probe scan is still a plain Spark scan - see `hasOnlyNativeScans`.
+ *
+ * Only `FileSourceScanExec` / `BatchScanExec` are tagged - the scan types
the per-format arms
+ * of shouldApplySparkToColumnar handle. A natively scannable file (e.g.
Parquet) is normally
+ * already a `CometScanExec` here; if native scan conversion did not take,
its
+ * `FileSourceScanExec` is still tagged, but the tag is harmless because the
per-format arm
+ * decides that scan before isBroadcastBuildSideLeaf is ever consulted. DPP
subquery broadcasts
+ * live in expressions, not `children`, so `foreach` never reaches them -
that scope is
+ * intentional.
+ */
+ private def tagBroadcastBuildSideLeaves(plan: SparkPlan): Unit = {
+ if (!broadcastBuildSideBridgeEnabled) {
+ return
+ }
+ plan.foreach {
+ case j: BroadcastHashJoinExec => tagBuildIfProbeNative(j.buildSide,
j.left, j.right)
+ case j: BroadcastNestedLoopJoinExec =>
tagBuildIfProbeNative(j.buildSide, j.left, j.right)
+ case _ =>
+ }
+ }
+
+ /** Tag the build side's file-source scans, but only if the probe side is
natively scannable. */
+ private def tagBuildIfProbeNative(
+ buildSide: BuildSide,
+ left: SparkPlan,
+ right: SparkPlan): Unit = {
+ val (buildPlan, probePlan) = buildSide match {
+ case BuildLeft => (left, right)
+ case BuildRight => (right, left)
+ }
+ if (hasOnlyNativeScans(probePlan)) {
Review Comment:
Thanks, this works now. On 3.4.3 with AQE the fact scan reads 400 rows again
and the broadcast is reused. The new DPP test is skipped below 3.5, though, so
nothing would catch it if this came back. Could you add a 3.4 test with a
native Iceberg fact partitioned on the join key and a text dimension, filtered
with something Spark can't infer onto the fact, that asserts the fact's DPP
filter isn't `true`? The last test in the probe suite I posted in the ORC
thread does this.
##########
spark/src/main/scala/org/apache/comet/rules/CometPlanAdaptiveDynamicPruningFilters.scala:
##########
@@ -351,6 +360,26 @@ case object CometPlanAdaptiveDynamicPruningFilters
}
}
+ /**
+ * True when `plan` (a DPP subquery's build, taken from an
AdaptiveSparkPlanExec.executedPlan)
+ * produces native Comet columnar output, so it is safe as a
CometBroadcastExchangeExec child.
+ * Unwraps AQE stage wrappers and any columnar->row transition (mirrors the
stripping in
+ * CometExecRule.rewriteInSubqueryPlan), then checks for a CometNativeExec.
See issue #6008.
+ */
+ private def isNativeBuildSide(plan: SparkPlan): Boolean = {
+ val stripped = stripAQEPlan(plan) match {
+ case c2r: CometNativeColumnarToRowExec => c2r.child
+ case c2r: CometColumnarToRowExec => c2r.child
+ case WholeStageCodegenExec(c2r: CometColumnarToRowExec) =>
+ c2r.child match {
+ case InputAdapter(child) => child
+ case other => other
+ }
+ case other => other
+ }
+ stripped.isInstanceOf[CometNativeExec]
Review Comment:
Thanks, the union dimension gets its `ReusedExchange` back under AQE on 4.1.
Could you add a test so it stays that way? An AQE DPP query whose dimension is
a `UNION ALL` of two filtered Parquet tables, asserting a `ReusedExchangeExec`
under the DPP subquery, fails with the old `CometNativeExec` check. The
docstring above `isNativeBuildSide` also still says it checks for a
`CometNativeExec`.
--
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]