parthchandra commented on code in PR #6786:
URL: https://github.com/apache/datafusion-comet/pull/6786#discussion_r4265484822


##########
spark/src/main/scala/org/apache/comet/rules/CometExecRule.scala:
##########
@@ -1480,6 +1508,194 @@ 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, 
`clearWastedBuildSideBridges` clears the tag
+   * after the transform and a second transform rebuilds the Spark build side, 
so a leftover copy
+   * never reaches runtime. See issue #6008.
+   */
+  private def broadcastBuildSideBridgeEnabled: Boolean =
+    CometConf.COMET_CONVERT_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,
+          CometConf.COMET_EXEC_BROADCAST_HASH_JOIN_ENABLED)
+      case j: BroadcastNestedLoopJoinExec =>
+        tagBuildIfProbeNative(
+          j.buildSide,
+          j.left,
+          j.right,
+          CometConf.COMET_EXEC_BROADCAST_NESTED_LOOP_JOIN_ENABLED)
+      case _ =>
+    }
+  }
+
+  /**
+   * Tag the build side's file-source scans, but only when the join looks like 
it can become a
+   * native Comet broadcast join: the probe side is natively scannable and the 
join's own serde is
+   * enabled. This is a best-effort gate that avoids inserting a bridge we 
would only tear down
+   * again - it cannot see every reason a join stays on Spark (an unsupported 
operator above a
+   * native probe scan, or a join type the serde declines), so 
`clearWastedBuildSideBridges` is
+   * the backstop that undoes any bridge left under a Spark 
`BroadcastExchangeExec` after the
+   * transform. Skipping here still matters because a bridge under a Spark 
broadcast breaks
+   * Spark's DPP broadcast reuse until it is torn down (the DPP subquery keeps 
an unbridged copy).
+   * We skip when:
+   *   - the join's own serde is disabled (`joinEnabled`), so it will stay on 
Spark; or
+   *   - the build-side `BroadcastExchangeExec` carries 
`SKIP_COMET_BROADCAST_TAG`, which
+   *     `CometSpark34AqeDppFallbackRule` sets on Spark 3.4 to keep the 
broadcast Spark-native so
+   *     `PlanAdaptiveDynamicPruningFilters` can match it (bridging it would 
turn DPP off). See
+   *     issue #6008.
+   */
+  private def tagBuildIfProbeNative(
+      buildSide: BuildSide,
+      left: SparkPlan,
+      right: SparkPlan,
+      joinEnabled: ConfigEntry[Boolean]): Unit = {
+    if (!joinEnabled.get(conf)) {
+      return
+    }
+    val (buildPlan, probePlan) = buildSide match {
+      case BuildLeft => (left, right)
+      case BuildRight => (right, left)
+    }
+    val skipForDpp = buildPlan match {
+      case b: BroadcastExchangeExec =>
+        b.getTagValue(CometExecRule.SKIP_COMET_BROADCAST_TAG).isDefined
+      case _ => false
+    }
+    if (!skipForDpp && hasOnlyNativeScans(probePlan)) {
+      tagBuildSideScans(buildPlan)
+    }
+  }
+
+  /**
+   * Tag the broadcast dimension's own file-source scans, descending through 
its filters, projects
+   * and aggregations (incl. the aggregate's shuffle) but stopping at a nested 
join. A nested
+   * join's inputs are not the bounded broadcast dimension - its streamed side 
can be far larger
+   * than the broadcast output - so bridging them would copy a large input to 
Arrow for no reason.
+   * That nested join is tagged by its own pre-pass visit based on its own 
probe. See issue #6008.
+   */
+  private def tagBuildSideScans(plan: SparkPlan): Unit = plan match {
+    case scan @ (_: FileSourceScanExec | _: BatchScanExec) =>
+      scan.setTagValue(CometExecRule.BROADCAST_BUILD_SIDE_TAG, ())
+    case _: BroadcastHashJoinExec | _: BroadcastNestedLoopJoinExec | _: 
ShuffledHashJoinExec |
+        _: SortMergeJoinExec =>
+    case other => other.children.foreach(tagBuildSideScans)
+  }
+
+  /**
+   * True when `plan` has at least one scan and none of its scan leaves is an 
unsupported plain
+   * Spark scan - i.e. every scan is already a Comet scan (CometScanRule ran 
before this rule).
+   * Used to decide whether a broadcast join's probe side can feed a native 
join. A probe that
+   * itself contains an unsupported scan (e.g. a DSv2 
`InMemoryTableWithV2Filter` fact) keeps the
+   * join on Spark, so bridging its build side would only break DPP reuse.
+   */
+  private def hasOnlyNativeScans(plan: SparkPlan): Boolean = {
+    var sawNativeScan = false
+    var sawUnsupportedScan = false
+    plan.foreach {
+      case _: CometScanExec | _: CometNativeScanExec | _: CometBatchScanExec =>
+        sawNativeScan = true
+      case _: FileSourceScanExec | _: BatchScanExec =>
+        sawUnsupportedScan = true
+      case _ =>
+    }
+    sawNativeScan && !sawUnsupportedScan
+  }
+
+  /**
+   * After the main transform, a #6008 build-side bridge is left under a plain 
Spark
+   * `BroadcastExchangeExec` only when its broadcast join did not go native - 
an unsupported join
+   * type (e.g. a full outer `BroadcastNestedLoopJoinExec`), a build side the 
serde declined, or a
+   * probe that is not fully native (e.g. an unsupported operator above a 
native scan). Clear
+   * `BROADCAST_BUILD_SIDE_TAG` from every scan under such a broadcast so a 
second transform
+   * rebuilds that build side exactly as it would without the feature, and so 
a later AQE stage
+   * pass - which prepares the build stage on its own, without the join above 
it - does not bridge
+   * the scan again. Returns true if any tag was cleared.
+   *
+   * Scans under a `CometBroadcastExchangeExec` keep their tag: that join went 
native and wants

Review Comment:
    Right, the walk didn't match the comment. clearScanTags now stops at 
CometBroadcastExchangeExec, so a nested native broadcast join inside a 
Spark-staying build keeps its bridge.



##########
spark/src/main/scala/org/apache/comet/rules/CometExecRule.scala:
##########
@@ -1480,6 +1508,194 @@ 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, 
`clearWastedBuildSideBridges` clears the tag
+   * after the transform and a second transform rebuilds the Spark build side, 
so a leftover copy
+   * never reaches runtime. See issue #6008.
+   */
+  private def broadcastBuildSideBridgeEnabled: Boolean =
+    CometConf.COMET_CONVERT_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,
+          CometConf.COMET_EXEC_BROADCAST_HASH_JOIN_ENABLED)
+      case j: BroadcastNestedLoopJoinExec =>
+        tagBuildIfProbeNative(
+          j.buildSide,
+          j.left,
+          j.right,
+          CometConf.COMET_EXEC_BROADCAST_NESTED_LOOP_JOIN_ENABLED)
+      case _ =>
+    }
+  }
+
+  /**
+   * Tag the build side's file-source scans, but only when the join looks like 
it can become a
+   * native Comet broadcast join: the probe side is natively scannable and the 
join's own serde is
+   * enabled. This is a best-effort gate that avoids inserting a bridge we 
would only tear down
+   * again - it cannot see every reason a join stays on Spark (an unsupported 
operator above a
+   * native probe scan, or a join type the serde declines), so 
`clearWastedBuildSideBridges` is
+   * the backstop that undoes any bridge left under a Spark 
`BroadcastExchangeExec` after the
+   * transform. Skipping here still matters because a bridge under a Spark 
broadcast breaks
+   * Spark's DPP broadcast reuse until it is torn down (the DPP subquery keeps 
an unbridged copy).
+   * We skip when:
+   *   - the join's own serde is disabled (`joinEnabled`), so it will stay on 
Spark; or
+   *   - the build-side `BroadcastExchangeExec` carries 
`SKIP_COMET_BROADCAST_TAG`, which
+   *     `CometSpark34AqeDppFallbackRule` sets on Spark 3.4 to keep the 
broadcast Spark-native so
+   *     `PlanAdaptiveDynamicPruningFilters` can match it (bridging it would 
turn DPP off). See
+   *     issue #6008.
+   */
+  private def tagBuildIfProbeNative(
+      buildSide: BuildSide,
+      left: SparkPlan,
+      right: SparkPlan,
+      joinEnabled: ConfigEntry[Boolean]): Unit = {
+    if (!joinEnabled.get(conf)) {
+      return
+    }
+    val (buildPlan, probePlan) = buildSide match {
+      case BuildLeft => (left, right)
+      case BuildRight => (right, left)
+    }
+    val skipForDpp = buildPlan match {
+      case b: BroadcastExchangeExec =>
+        b.getTagValue(CometExecRule.SKIP_COMET_BROADCAST_TAG).isDefined
+      case _ => false
+    }
+    if (!skipForDpp && hasOnlyNativeScans(probePlan)) {
+      tagBuildSideScans(buildPlan)
+    }
+  }
+
+  /**
+   * Tag the broadcast dimension's own file-source scans, descending through 
its filters, projects
+   * and aggregations (incl. the aggregate's shuffle) but stopping at a nested 
join. A nested
+   * join's inputs are not the bounded broadcast dimension - its streamed side 
can be far larger
+   * than the broadcast output - so bridging them would copy a large input to 
Arrow for no reason.
+   * That nested join is tagged by its own pre-pass visit based on its own 
probe. See issue #6008.
+   */
+  private def tagBuildSideScans(plan: SparkPlan): Unit = plan match {
+    case scan @ (_: FileSourceScanExec | _: BatchScanExec) =>
+      scan.setTagValue(CometExecRule.BROADCAST_BUILD_SIDE_TAG, ())
+    case _: BroadcastHashJoinExec | _: BroadcastNestedLoopJoinExec | _: 
ShuffledHashJoinExec |
+        _: SortMergeJoinExec =>
+    case other => other.children.foreach(tagBuildSideScans)
+  }
+
+  /**
+   * True when `plan` has at least one scan and none of its scan leaves is an 
unsupported plain
+   * Spark scan - i.e. every scan is already a Comet scan (CometScanRule ran 
before this rule).
+   * Used to decide whether a broadcast join's probe side can feed a native 
join. A probe that
+   * itself contains an unsupported scan (e.g. a DSv2 
`InMemoryTableWithV2Filter` fact) keeps the
+   * join on Spark, so bridging its build side would only break DPP reuse.
+   */
+  private def hasOnlyNativeScans(plan: SparkPlan): Boolean = {
+    var sawNativeScan = false
+    var sawUnsupportedScan = false
+    plan.foreach {
+      case _: CometScanExec | _: CometNativeScanExec | _: CometBatchScanExec =>
+        sawNativeScan = true
+      case _: FileSourceScanExec | _: BatchScanExec =>
+        sawUnsupportedScan = true
+      case _ =>
+    }
+    sawNativeScan && !sawUnsupportedScan
+  }
+
+  /**
+   * After the main transform, a #6008 build-side bridge is left under a plain 
Spark
+   * `BroadcastExchangeExec` only when its broadcast join did not go native - 
an unsupported join
+   * type (e.g. a full outer `BroadcastNestedLoopJoinExec`), a build side the 
serde declined, or a
+   * probe that is not fully native (e.g. an unsupported operator above a 
native scan). Clear
+   * `BROADCAST_BUILD_SIDE_TAG` from every scan under such a broadcast so a 
second transform
+   * rebuilds that build side exactly as it would without the feature, and so 
a later AQE stage
+   * pass - which prepares the build stage on its own, without the join above 
it - does not bridge
+   * the scan again. Returns true if any tag was cleared.
+   *
+   * Scans under a `CometBroadcastExchangeExec` keep their tag: that join went 
native and wants
+   * the bridged build. The descent enters `CometScanWrapper`'s wrapped 
`originalPlan` by hand
+   * because it is a leaf, so a plain `foreach` would never reach the bridged 
scan inside it. See
+   * issue #6008.
+   */
+  private def clearWastedBuildSideBridges(plan: SparkPlan): Boolean = {
+    var cleared = false
+    def clearScanTags(node: SparkPlan): Unit = {
+      if (node.getTagValue(CometExecRule.BROADCAST_BUILD_SIDE_TAG).isDefined) {
+        node.unsetTagValue(CometExecRule.BROADCAST_BUILD_SIDE_TAG)
+        cleared = true

Review Comment:
   Good catch — fixed. clearScanTags now only counts a clear when the tagged 
scan is under a CometSparkToColumnarExec, so a tagged-but-unbridged 
CSV/JSON/Parquet scan no longer triggers the second transform. Matches what you 
measured.Good catch — fixed. clearScanTags now only counts a clear when the 
tagged scan is under a CometSparkToColumnarExec, so a tagged-but-unbridged 
CSV/JSON/Parquet scan no longer triggers the second transform. Matches what you 
measured.



##########
spark/src/test/scala/org/apache/comet/exec/CometExecSuite.scala:
##########
@@ -307,6 +307,592 @@ class CometExecSuite extends CometTestBase {
     }
   }
 
+  test("broadcast build side with unsupported text source goes native 
(#6008)") {
+    // A tiny lookup table read from a Text file (which Comet cannot scan 
natively) sits on the
+    // build side of a broadcast join over a large native probe. Without the 
auto-bridge the whole
+    // join stays on Spark. With 
spark.comet.convert.broadcastBuildSide.enabled (default on)
+    // the text leaf is bridged to Arrow so the broadcast and the join run 
natively.
+    
assert(CometConf.COMET_CONVERT_BROADCAST_BUILD_SIDE_ENABLED.defaultValue.contains(true))
+    withTempDir { dir =>
+      val probePath = s"${dir.getAbsolutePath}/probe.parquet"
+      val allowPath = s"${dir.getAbsolutePath}/allow.txt"
+      withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
+        spark
+          .range(0, 100)
+          .selectExpr("CAST(id AS STRING) AS key", "id AS v")
+          .write
+          .parquet(probePath)
+        spark
+          .range(0, 5)
+          .selectExpr("CAST(id AS STRING) AS value")
+          .write
+          .text(allowPath)
+      }
+
+      // Cover both the AQE and non-AQE planning paths - the tag pre-pass runs 
per stage under AQE.
+      for (aqe <- Seq(false, true)) {
+        withSQLConf(SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> aqe.toString) {
+          spark.read.parquet(probePath).createOrReplaceTempView("probe_6008")
+          spark.read.text(allowPath).createOrReplaceTempView("allow_6008")
+          val query =
+            "SELECT /*+ BROADCAST(a) */ p.v FROM probe_6008 p JOIN allow_6008 
a ON p.key = a.value"
+
+          // Default: the text build side is bridged to Arrow and the join 
goes native. The plan is
+          // fully native (the bridge counts as a native leaf), so use 
checkSparkAnswerAndOperator -
+          // checkSparkAnswer would fail under COMET_STRICT_TESTING on a 
fully-native plan.
+          val (_, cometPlan) = checkSparkAnswerAndOperator(sql(query))
+          assert(
+            collect(cometPlan) { case s: CometSparkToColumnarExec => s 
}.nonEmpty,
+            s"aqe=$aqe: expected a CometSparkToColumnarExec on the text build 
side:\n" +
+              cometPlan.treeString)
+          assert(
+            collect(cometPlan) { case b: CometBroadcastExchangeExec => b 
}.nonEmpty,
+            s"aqe=$aqe: expected a 
CometBroadcastExchangeExec:\n${cometPlan.treeString}")
+          assert(
+            collect(cometPlan) { case j: CometBroadcastHashJoinExec => j 
}.nonEmpty,
+            s"aqe=$aqe: expected a 
CometBroadcastHashJoinExec:\n${cometPlan.treeString}")
+
+          // Off: the unsupported text leaf blocks native execution of the 
broadcast and the join.
+          withSQLConf(CometConf.COMET_CONVERT_BROADCAST_BUILD_SIDE_ENABLED.key 
-> "false") {
+            val (_, cometPlanOff) = checkSparkAnswer(sql(query))
+            assert(
+              collect(cometPlanOff) { case s: CometSparkToColumnarExec => s 
}.isEmpty,
+              s"aqe=$aqe: expected no CometSparkToColumnarExec when 
disabled:\n" +
+                cometPlanOff.treeString)
+            assert(
+              collect(cometPlanOff) { case b: CometBroadcastExchangeExec => b 
}.isEmpty,
+              s"aqe=$aqe: expected no CometBroadcastExchangeExec when 
disabled:\n" +
+                cometPlanOff.treeString)
+            assert(
+              collect(cometPlanOff) { case j: CometBroadcastHashJoinExec => j 
}.isEmpty,
+              s"aqe=$aqe: expected no CometBroadcastHashJoinExec when 
disabled:\n" +
+                cometPlanOff.treeString)
+          }
+        }
+      }
+    }
+  }
+
+  test("broadcast build side bridge is gated on broadcast-exchange conversion 
(#6008)") {
+    // The bridge is pointless when Comet broadcast exchange conversion is 
off: the broadcast (and
+    // so the join) cannot go native. With 
COMET_EXEC_BROADCAST_EXCHANGE_ENABLED=false the text
+    // build side must not be bridged even though broadcastBuildSide is on.
+    withTempDir { dir =>
+      val probePath = s"${dir.getAbsolutePath}/probe.parquet"
+      val allowPath = s"${dir.getAbsolutePath}/allow.txt"
+      withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
+        spark
+          .range(0, 100)
+          .selectExpr("CAST(id AS STRING) AS key", "id AS v")
+          .write
+          .parquet(probePath)
+        spark.range(0, 5).selectExpr("CAST(id AS STRING) AS 
value").write.text(allowPath)
+      }
+      withSQLConf(
+        SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+        CometConf.COMET_CONVERT_BROADCAST_BUILD_SIDE_ENABLED.key -> "true",
+        CometConf.COMET_EXEC_BROADCAST_EXCHANGE_ENABLED.key -> "false") {
+        
spark.read.parquet(probePath).createOrReplaceTempView("probe_gate_6008")
+        spark.read.text(allowPath).createOrReplaceTempView("allow_gate_6008")
+        val (_, cometPlan) = checkSparkAnswer(sql("""
+            |SELECT /*+ BROADCAST(a) */ p.v
+            |FROM probe_gate_6008 p JOIN allow_gate_6008 a ON p.key = a.value
+            |""".stripMargin))
+        assert(
+          collect(cometPlan) { case s: CometSparkToColumnarExec => s }.isEmpty,
+          "with broadcast-exchange conversion off the build side must not be 
bridged:\n" +
+            cometPlan.treeString)
+      }
+    }
+  }
+
+  test("broadcast build side scan below an aggregate is bridged, shuffle stage 
is not (#6008)") {
+    // A file scan can sit BELOW build-side operators. Broadcasting an 
aggregated Text table puts a
+    // Text FileSourceScanExec under a partial aggregate + shuffle on the 
build side. The tag
+    // pre-pass walks the whole broadcast subtree, so the deep text scan must 
be bridged to Arrow,
+    // yet no query stage (e.g. the build-side shuffle under AQE) may be 
wrapped in the bridge.
+    withTempDir { dir =>
+      val allowPath = s"${dir.getAbsolutePath}/allow.txt"
+      val probePath = s"${dir.getAbsolutePath}/probe.parquet"
+      withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
+        spark.range(0, 20).selectExpr("CAST(id % 5 AS STRING) AS 
value").write.text(allowPath)
+        spark
+          .range(0, 100)
+          .selectExpr("CAST(id % 5 AS STRING) AS key", "id AS v")
+          .write
+          .parquet(probePath)
+      }
+      spark.read.text(allowPath).createOrReplaceTempView("allow_agg_6008")
+      spark.read.parquet(probePath).createOrReplaceTempView("probe_agg_6008")
+      val query = """
+          |SELECT /*+ BROADCAST(d) */ p.v, d.n
+          |FROM probe_agg_6008 p
+          |JOIN (SELECT value AS k, COUNT(*) AS n FROM allow_agg_6008 GROUP BY 
value) d
+          |  ON p.key = d.k
+          |""".stripMargin
+      for (aqe <- Seq(false, true)) {
+        withSQLConf(
+          SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> aqe.toString,
+          SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> (10 * 1024 * 
1024).toString) {
+          // The plan is fully native (the bridged text scan under the 
aggregate is a native leaf),
+          // so checkSparkAnswerAndOperator also pins that the text scan did 
get bridged - an
+          // un-bridged Spark scan would read as non-native and fail here. It 
is also strict-testing
+          // safe, unlike checkSparkAnswer on a fully-native plan.
+          val (_, cometPlan) = checkSparkAnswerAndOperator(sql(query))
+          // `collect` descends AQE query stages, so the bridge on the deep 
text scan is reachable
+          // under AQE too (it lives in the build-side shuffle stage). Assert 
it in both modes - and
+          // because checkSparkAnswerAndOperator only inspects the initial 
plan under AQE, this is
+          // the only thing that checks the final AQE plan.
+          assert(
+            collect(cometPlan) { case s: CometSparkToColumnarExec => s 
}.nonEmpty,
+            s"aqe=$aqe: expected the build-side text scan to be 
bridged:\n${cometPlan.treeString}")
+          // A query stage (the build-side shuffle) must never be wrapped in 
the bridge.
+          collect(cometPlan) { case s: CometSparkToColumnarExec => s.child 
}.foreach { child =>
+            assert(
+              !child.isInstanceOf[QueryStageExec],
+              s"aqe=$aqe: a QueryStageExec must not be bridged to 
Arrow:\n${cometPlan.treeString}")
+          }
+        }
+      }
+    }
+  }
+
+  test("per-format opt-out still wins on a broadcast build side (#6008)") {
+    // A CSV build side stays a Spark FileSourceScanExec and is tagged, but 
the CSV arm of
+    // shouldApplySparkToColumnar honors COMET_CONVERT_FROM_CSV_ENABLED and 
never calls the
+    // broadcast-build-side bypass. So with convert-from-csv off, the CSV 
build side must NOT be
+    // bridged and the join must stay on Spark, even with broadcastBuildSide 
enabled.
+    withTempDir { dir =>
+      val allowPath = s"${dir.getAbsolutePath}/allow.csv"
+      val probePath = s"${dir.getAbsolutePath}/probe.parquet"
+      withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
+        spark.range(0, 5).selectExpr("CAST(id AS STRING) AS 
value").write.csv(allowPath)
+        spark
+          .range(0, 100)
+          .selectExpr("CAST(id AS STRING) AS key", "id AS v")
+          .write
+          .parquet(probePath)
+      }
+      withSQLConf(
+        SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+        CometConf.COMET_CONVERT_FROM_CSV_ENABLED.key -> "false",
+        CometConf.COMET_CONVERT_BROADCAST_BUILD_SIDE_ENABLED.key -> "true") {
+        spark.read.csv(allowPath).createOrReplaceTempView("allow_csv_6008")
+        spark.read.parquet(probePath).createOrReplaceTempView("probe_csv_6008")
+        val (_, cometPlan) = checkSparkAnswer(sql("""
+            |SELECT /*+ BROADCAST(a) */ p.v
+            |FROM probe_csv_6008 p JOIN allow_csv_6008 a ON p.key = a._c0
+            |""".stripMargin))
+        assert(
+          collect(cometPlan) { case s: CometSparkToColumnarExec => s }.isEmpty,
+          "a CSV build side with convert-from-csv disabled must not be 
bridged:\n" +
+            cometPlan.treeString)
+      }
+    }
+  }
+
+  test("broadcast build side with an unsupported DSv2 scan goes native 
(#6008)") {
+    // Exercises the BatchScanExec arm: an unsupported-format scan (Text read 
as DSv2) on the build
+    // side is bridged so the join goes native. Keep parquet on the V1 list so 
the probe stays a
+    // native CometScan; drop text from the list so it is read as a DSv2 
BatchScan/TextScan.
+    withTempDir { dir =>
+      val allowPath = s"${dir.getAbsolutePath}/allow.txt"
+      val probePath = s"${dir.getAbsolutePath}/probe.parquet"
+      withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
+        spark.range(0, 5).selectExpr("CAST(id AS STRING) AS 
value").write.text(allowPath)
+        spark
+          .range(0, 100)
+          .selectExpr("CAST(id AS STRING) AS key", "id AS v")
+          .write
+          .parquet(probePath)
+      }
+      withSQLConf(
+        SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+        SQLConf.USE_V1_SOURCE_LIST.key -> "parquet") {
+        spark.read.text(allowPath).createOrReplaceTempView("allow_v2_6008")
+        spark.read.parquet(probePath).createOrReplaceTempView("probe_v2_6008")
+        // Fully native (bridged DSv2 text build side + native parquet probe + 
native join), so use
+        // checkSparkAnswerAndOperator - strict-testing safe, unlike 
checkSparkAnswer here.
+        val (_, cometPlan) = checkSparkAnswerAndOperator(sql("""
+            |SELECT /*+ BROADCAST(a) */ p.v
+            |FROM probe_v2_6008 p JOIN allow_v2_6008 a ON p.key = a.value
+            |""".stripMargin))
+        assert(
+          collect(cometPlan) { case s: CometSparkToColumnarExec => s 
}.nonEmpty,
+          s"expected the DSv2 text build side to be 
bridged:\n${cometPlan.treeString}")
+        assert(
+          collect(cometPlan) { case j: CometBroadcastHashJoinExec => j 
}.nonEmpty,
+          s"expected the join to go native over the DSv2 build 
side:\n${cometPlan.treeString}")
+      }
+    }
+  }
+
+  test("broadcast join with an unsupported probe stays on Spark (#6008)") {
+    // Both sides are unsupported Text sources. The probe is not natively 
scannable, so the join
+    // cannot go native - and because the probe is not native, the build side 
must NOT be bridged
+    // either (bridging it would be useless and would break Spark's 
broadcast/DPP reuse). This pins
+    // the probe-native gate that fixed the DynamicPartitionPruningV2 
regression.
+    withTempDir { dir =>
+      val probePath = s"${dir.getAbsolutePath}/probe.txt"
+      val allowPath = s"${dir.getAbsolutePath}/allow.txt"
+      withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
+        spark.range(0, 100).selectExpr("CAST(id AS STRING) AS 
value").write.text(probePath)
+        spark.range(0, 5).selectExpr("CAST(id AS STRING) AS 
value").write.text(allowPath)
+      }
+      for (aqe <- Seq(false, true)) {
+        withSQLConf(SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> aqe.toString) {
+          spark.read.text(probePath).createOrReplaceTempView("probe_txt_6008")
+          spark.read.text(allowPath).createOrReplaceTempView("allow_txt_6008")
+          val (_, cometPlan) = checkSparkAnswer(sql("""
+              |SELECT /*+ BROADCAST(a) */ p.value
+              |FROM probe_txt_6008 p JOIN allow_txt_6008 a ON p.value = a.value
+              |""".stripMargin))
+          // The non-native probe must leave the build side un-bridged (the 
probe-native gate).
+          assert(
+            collect(cometPlan) { case s: CometSparkToColumnarExec => s 
}.isEmpty,
+            s"aqe=$aqe: a non-native probe must leave the build side 
un-bridged:\n" +
+              cometPlan.treeString)
+          assert(
+            collect(cometPlan) { case j: CometBroadcastHashJoinExec => j 
}.isEmpty,
+            s"aqe=$aqe: the join must stay on Spark when the probe is not 
native:\n" +
+              cometPlan.treeString)
+          assert(
+            collect(cometPlan) { case b: CometBroadcastExchangeExec => b 
}.isEmpty,
+            s"aqe=$aqe: the broadcast must stay on Spark when the join does 
not convert:\n" +
+              cometPlan.treeString)
+        }
+      }
+    }
+  }
+
+  test("broadcast build side on the left (BuildLeft) with a text source goes 
native (#6008)") {
+    // All other tests broadcast the right table (BuildRight). Put the small 
text table first and
+    // hint it so it becomes the left build side, exercising the BuildLeft arm 
of tagBuildIfProbeNative.
+    withTempDir { dir =>
+      val probePath = s"${dir.getAbsolutePath}/probe.parquet"
+      val allowPath = s"${dir.getAbsolutePath}/allow.txt"
+      withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
+        spark
+          .range(0, 100)
+          .selectExpr("CAST(id AS STRING) AS key", "id AS v")
+          .write
+          .parquet(probePath)
+        spark.range(0, 5).selectExpr("CAST(id AS STRING) AS 
value").write.text(allowPath)
+      }
+      withSQLConf(SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false") {
+        spark.read.text(allowPath).createOrReplaceTempView("allow_bl_6008")
+        spark.read.parquet(probePath).createOrReplaceTempView("probe_bl_6008")
+        val (_, cometPlan) = checkSparkAnswerAndOperator(sql("""
+            |SELECT /*+ BROADCAST(a) */ p.v
+            |FROM allow_bl_6008 a JOIN probe_bl_6008 p ON a.value = p.key
+            |""".stripMargin))
+        assert(
+          collect(cometPlan) { case s: CometSparkToColumnarExec => s 
}.nonEmpty,
+          s"expected the text build side to be 
bridged:\n${cometPlan.treeString}")
+        assert(
+          collect(cometPlan) { case j: CometBroadcastHashJoinExec => j 
}.nonEmpty,
+          s"expected a CometBroadcastHashJoinExec:\n${cometPlan.treeString}")
+      }
+    }
+  }
+
+  test("broadcast nested loop join build side with a text source goes native 
(#6008)") {
+    // Exercises the BroadcastNestedLoopJoinExec arm of 
tagBroadcastBuildSideLeaves: an inequality
+    // join forces a BNLJ; the text build side is bridged so the BNLJ runs 
natively.
+    withTempDir { dir =>
+      val probePath = s"${dir.getAbsolutePath}/probe.parquet"
+      val allowPath = s"${dir.getAbsolutePath}/allow.txt"
+      withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
+        spark.range(0, 100).selectExpr("id AS v", "CAST(id AS INT) AS 
k").write.parquet(probePath)
+        spark.range(0, 5).selectExpr("CAST(id AS STRING) AS 
value").write.text(allowPath)
+      }
+      withSQLConf(SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false") {
+        spark.read.text(allowPath).createOrReplaceTempView("allow_bnlj_6008")
+        
spark.read.parquet(probePath).createOrReplaceTempView("probe_bnlj_6008")
+        val (_, cometPlan) = checkSparkAnswer(sql("""
+            |SELECT /*+ BROADCAST(a) */ p.v
+            |FROM probe_bnlj_6008 p JOIN allow_bnlj_6008 a ON p.k > 
CAST(a.value AS INT)
+            |""".stripMargin))
+        assert(
+          collect(cometPlan) { case s: CometSparkToColumnarExec => s 
}.nonEmpty,
+          s"expected the text build side to be 
bridged:\n${cometPlan.treeString}")
+        assert(
+          collect(cometPlan) { case j: CometBroadcastNestedLoopJoinExec => j 
}.nonEmpty,
+          s"expected a 
CometBroadcastNestedLoopJoinExec:\n${cometPlan.treeString}")
+      }
+    }
+  }
+
+  test("AQE DPP with a text dimension on the broadcast build side runs 
natively (#6008)") {
+    // A Text dimension on a broadcast build side joined to a PARTITIONED 
Parquet fact triggers DPP.
+    // The build-side bridge makes the join a CometBroadcastHashJoinExec; the 
DPP subquery's own
+    // build (the text scan) is not bridged, so 
CometPlanAdaptiveDynamicPruningFilters must NOT wrap
+    // it in a CometBroadcastExchangeExec (that crashed with "Comet execution 
only takes Arrow
+    // Arrays"). It falls back to a Spark DPP broadcast while the join stays 
native. See issue #6008.
+    assume(isSpark35Plus, "Native AQE DPP requires Spark 3.5+")
+    withSQLConf(
+      SQLConf.USE_V1_SOURCE_LIST.key -> "parquet",
+      SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "true",
+      SQLConf.DYNAMIC_PARTITION_PRUNING_ENABLED.key -> "true",
+      SQLConf.DYNAMIC_PARTITION_PRUNING_REUSE_BROADCAST_ONLY.key -> "true") {
+      withTempDir { dir =>
+        val factPath = s"${dir.getAbsolutePath}/fact.parquet"
+        val dimPath = s"${dir.getAbsolutePath}/dim.txt"
+        withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
+          spark
+            .range(0, 100)
+            .selectExpr("CAST(id % 10 AS STRING) AS fk", "id AS v")
+            .write
+            .partitionBy("fk")
+            .parquet(factPath)
+          spark.range(0, 10).selectExpr("CAST(id AS STRING) AS 
value").write.text(dimPath)
+        }
+        spark.read.parquet(factPath).createOrReplaceTempView("dpp_fact_txt")
+        spark.read.text(dimPath).createOrReplaceTempView("dpp_dim_txt")
+        // CAST(d.value AS INT) % 2 = 0 is a filter Spark cannot infer onto 
f.fk, so the fact is
+        // pruned only via a live DPP subquery (a filter Spark can push, like 
d.value < '5', would
+        // prune the fact statically and the test would pass even with DPP 
disabled).
+        val df = sql("""
+            |SELECT /*+ BROADCAST(d) */ f.v
+            |FROM dpp_fact_txt f JOIN dpp_dim_txt d ON f.fk = d.value
+            |WHERE CAST(d.value AS INT) % 2 = 0
+            |""".stripMargin)
+        // Must not crash, and must match Spark.
+        val (_, cometPlan) = checkSparkAnswer(df)
+        // The join still goes native over the bridged build side.
+        assert(
+          collect(cometPlan) { case j: CometBroadcastHashJoinExec => j 
}.nonEmpty,
+          s"expected the join to run natively:\n${cometPlan.treeString}")
+        // DPP is still live - the fact scan keeps a broadcast pruning 
subquery (Spark-side here,
+        // because the unbridged text dimension build is not native).
+        val dppSubqueries = collectWithSubqueries(cometPlan) {
+          case s: SubqueryBroadcastExec => s
+          case s: CometSubqueryBroadcastExec => s
+        }
+        assert(
+          dppSubqueries.nonEmpty,
+          s"expected a live DPP broadcast subquery on the fact 
scan:\n${cometPlan.treeString}")
+      }
+    }
+  }
+
+  test("broadcast join that stays on Spark leaves no build-side bridge 
(#6008)") {
+    // A non-equi FULL OUTER broadcast join is a BroadcastNestedLoopJoinExec 
whose join type Comet
+    // declines (getSupportLevel is Unsupported for FullOuter). The probe is 
native Parquet, so the
+    // build-side gate still tags and bridges the text build - but because the 
join cannot go
+    // native, clearWastedBuildSideBridges must undo that bridge after the 
transform, leaving the
+    // plan exactly as it would be without the feature: a Spark BNLJ over a 
Spark broadcast with no
+    // CometSparkToColumnarExec. Regression test for the first-round revert 
that never fired. #6008.
+    withTempDir { dir =>
+      val probePath = s"${dir.getAbsolutePath}/probe.parquet"
+      val buildPath = s"${dir.getAbsolutePath}/build.txt"
+      withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
+        spark.range(0, 100).selectExpr("CAST(id AS INT) AS k", "id AS 
v").write.parquet(probePath)
+        spark.range(0, 5).selectExpr("CAST(id AS STRING) AS 
value").write.text(buildPath)
+      }
+      for (aqe <- Seq(false, true)) {
+        withSQLConf(SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> aqe.toString) {
+          
spark.read.parquet(probePath).createOrReplaceTempView("probe_fo_6008")
+          spark.read.text(buildPath).createOrReplaceTempView("build_fo_6008")
+          val (_, cometPlan) = checkSparkAnswer(sql("""
+              |SELECT /*+ BROADCAST(a) */ p.v, a.value
+              |FROM probe_fo_6008 p
+              |FULL OUTER JOIN (SELECT value FROM build_fo_6008 WHERE 
LENGTH(value) > 0) a
+              |ON p.k > CAST(a.value AS INT)
+              |""".stripMargin))
+          assert(
+            collect(cometPlan) { case s: CometSparkToColumnarExec => s 
}.isEmpty,
+            s"aqe=$aqe: a Spark-staying join must leave no build-side 
bridge:\n" +
+              cometPlan.treeString)
+          assert(
+            collect(cometPlan) { case b: CometBroadcastExchangeExec => b 
}.isEmpty,
+            s"aqe=$aqe: the broadcast must stay on 
Spark:\n${cometPlan.treeString}")
+          assert(
+            collect(cometPlan) { case j: BroadcastNestedLoopJoinExec => j 
}.nonEmpty,
+            s"aqe=$aqe: full outer join must stay a Spark 
BNLJ:\n${cometPlan.treeString}")
+        }
+      }
+    }
+  }
+
+  test("Spark-staying broadcast join with an aggregate build side leaves no 
bridge (#6008)") {
+    // Same declined FULL OUTER BNLJ, but the text build side is aggregated. 
The bridge is inserted
+    // under the aggregate (and its CometSinkPlaceHolder) and then cleared. 
The first-round revert
+    // rebuilt the Spark nodes from originalPlan and crashed here with a 
ClassCastException; the
+    // tag-clear-and-retransform approach must give a clean Spark plan and the 
right answer. #6008.
+    withTempDir { dir =>
+      val probePath = s"${dir.getAbsolutePath}/probe.parquet"
+      val buildPath = s"${dir.getAbsolutePath}/build.txt"
+      withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
+        spark.range(0, 100).selectExpr("CAST(id AS INT) AS k", "id AS 
v").write.parquet(probePath)
+        spark.range(0, 20).selectExpr("CAST(id % 5 AS STRING) AS 
value").write.text(buildPath)
+      }
+      for (aqe <- Seq(false, true)) {
+        withSQLConf(SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> aqe.toString) {
+          
spark.read.parquet(probePath).createOrReplaceTempView("probe_foa_6008")
+          spark.read.text(buildPath).createOrReplaceTempView("build_foa_6008")
+          val (_, cometPlan) = checkSparkAnswer(sql("""
+              |SELECT /*+ BROADCAST(a) */ p.v, a.n
+              |FROM probe_foa_6008 p
+              |FULL OUTER JOIN
+              |  (SELECT CAST(value AS INT) AS gk, COUNT(*) AS n
+              |   FROM build_foa_6008 GROUP BY value) a
+              |ON p.k > a.gk
+              |""".stripMargin))
+          assert(
+            collect(cometPlan) { case s: CometSparkToColumnarExec => s 
}.isEmpty,
+            s"aqe=$aqe: aggregate build side must leave no 
bridge:\n${cometPlan.treeString}")
+          assert(
+            collect(cometPlan) { case b: CometBroadcastExchangeExec => b 
}.isEmpty,
+            s"aqe=$aqe: the broadcast must stay on 
Spark:\n${cometPlan.treeString}")
+        }
+      }
+    }
+  }
+
+  test("Spark-staying broadcast join with a union build side leaves no bridge 
(#6008)") {
+    // Same declined FULL OUTER BNLJ, but the build side is a UNION ALL of two 
text scans. The
+    // first-round revert rebuilt each Comet node from originalPlan and failed 
here with "Incorrect
+    // number of children"; tag-clear-and-retransform must produce a clean 
Spark plan. See #6008.
+    withTempDir { dir =>
+      val probePath = s"${dir.getAbsolutePath}/probe.parquet"
+      val buildPath1 = s"${dir.getAbsolutePath}/build1.txt"
+      val buildPath2 = s"${dir.getAbsolutePath}/build2.txt"
+      withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
+        spark.range(0, 100).selectExpr("CAST(id AS INT) AS k", "id AS 
v").write.parquet(probePath)
+        spark.range(0, 5).selectExpr("CAST(id AS STRING) AS 
value").write.text(buildPath1)
+        spark.range(5, 10).selectExpr("CAST(id AS STRING) AS 
value").write.text(buildPath2)
+      }
+      for (aqe <- Seq(false, true)) {
+        withSQLConf(SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> aqe.toString) {
+          
spark.read.parquet(probePath).createOrReplaceTempView("probe_fou_6008")
+          
spark.read.text(buildPath1).createOrReplaceTempView("build_fou1_6008")
+          
spark.read.text(buildPath2).createOrReplaceTempView("build_fou2_6008")
+          val (_, cometPlan) = checkSparkAnswer(sql("""
+              |SELECT /*+ BROADCAST(a) */ p.v, a.value
+              |FROM probe_fou_6008 p
+              |FULL OUTER JOIN
+              |  (SELECT value FROM build_fou1_6008 UNION ALL SELECT value 
FROM build_fou2_6008) a
+              |ON p.k > CAST(a.value AS INT)
+              |""".stripMargin))
+          assert(
+            collect(cometPlan) { case s: CometSparkToColumnarExec => s 
}.isEmpty,
+            s"aqe=$aqe: union build side must leave no 
bridge:\n${cometPlan.treeString}")
+          assert(
+            collect(cometPlan) { case b: CometBroadcastExchangeExec => b 
}.isEmpty,
+            s"aqe=$aqe: the broadcast must stay on 
Spark:\n${cometPlan.treeString}")
+        }
+      }
+    }
+  }
+
+  test("Spark 3.4 AQE DPP with a text dimension on the broadcast build side 
(#6008)") {

Review Comment:
   Agreed — on 3.4 the V1 fact stays on Spark so nothing is tagged. Renamed and 
recommented it as the V1-fallback case (feature stays out of the way, DPP stays 
live), and noted the Iceberg test in CometBroadcastBuildSideSuite covers the 
skip-tag path with a native fact.



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