This is an automated email from the ASF dual-hosted git repository.

peter-toth pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new e8b2d0e8ea92 [SPARK-60084][SQL] Fix PlanMerger losing a reused filter 
attribute before the aggregate
e8b2d0e8ea92 is described below

commit e8b2d0e8ea92b5d71920e9fcd02e4faa68a5a0b5
Author: Peter Toth <[email protected]>
AuthorDate: Thu Oct 8 21:10:45 2026 +0200

    [SPARK-60084][SQL] Fix PlanMerger losing a reused filter attribute before 
the aggregate
    
    ### What changes were proposed in this pull request?
    
    `PlanMerger.mergeNamedExpressions` appended the new plan's propagated 
filter attribute only when it was a fresh alias. A reused one (`isNew = false`) 
was assumed to be in the merged expression list already.
    
    This PR appends the new plan's filter attribute whenever the list doesn't 
output it yet. It also drops the `isNew` flag from 
`TryMergeResult.newPlanFilter`, because nothing reads it any more.
    
    The reuse lookup in the tagged `(Filter, Filter)` branch now only matches 
the filter aliases that the merged `Filter` references. Before, a computed 
column with the same expression as the new plan's filter could be reused, and 
the merged `Filter` would drop that plan's rows.
    
    ### Why are the changes needed?
    
    The assumption doesn't hold. A merge round can fold a filter attribute into 
another filter alias, and then only the new alias is carried up to the 
aggregate. A later round that reuses the original attribute ends up with an 
aggregate `FILTER` on an attribute its child doesn't output:
    
    ```sql
    SET 
spark.sql.optimizer.mergeSubplans.filterPropagation.symmetricFilterPropagation.enabled=true;
    SET 
spark.sql.optimizer.mergeSubplans.filterPropagation.throughJoin.enabled=true;
    
    CREATE TABLE t USING parquet AS SELECT * FROM VALUES
      (1, 1, 10), (1, 2, 3), (2, 1, 7), (2, 2, 9), (3, 1, 9), (3, 2, 6) AS v(a, 
b, c);
    
    SELECT
      (SELECT sum(l.a) FROM t l LEFT JOIN t r ON l.a = r.a WHERE l.b = 1),
      (SELECT max(l.a) FROM t l LEFT JOIN t r ON l.a = r.a WHERE l.b = 2 AND 
coalesce(r.c, 10) > 5),
      (SELECT count(*) FROM t l LEFT JOIN t r ON l.a = r.a WHERE l.b = 2);
    ```
    
    ```
    [INTERNAL_ERROR_ATTRIBUTE_NOT_FOUND] Could not find propagatedFilter_3#179 
in 
[sum#141L,max#182,count#145L,a#6,propagatedFilter_5#181,propagatedFilter_4#180].
 SQLSTATE: XX000
    ```
    
    What happens there:
    1. Merging the 2nd subquery into the 1st aliases `l.b = 2` on the left side 
of the join.
    2. The 2nd subquery's post-join `coalesce(r.c, 10) > 5` folds that alias 
into a new `AND` alias.
    3. The 3rd subquery reuses the `l.b = 2` alias, but the `Project` under the 
aggregate only outputs the new one.
    
    This was found while reviewing #59161, which runs into the same problem 
through its new join `AND`.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. The query above now returns `12, 3, 6` instead of failing. It needs 
both configs above, which are off by default. The bug is in 4.2.0.
    
    ### How was this patch tested?
    
    New tests in `MergeSubplansSuite` (merged plans, including the computed 
column case) and `PlanMergingSuite` (end to end). They all fail without the 
fix. `MergeSubplansSuite` and `PlanMergingSuite` pass.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Claude Opus 5.5)
    
    Closes #59294 from 
peter-toth/SPARK-60084-planmerger-reused-filter-attribute.
    
    Authored-by: Peter Toth <[email protected]>
    Signed-off-by: Peter Toth <[email protected]>
    (cherry picked from commit 90568fb163775793260ecb13f2019ea10ef4e2f3)
    Signed-off-by: Peter Toth <[email protected]>
---
 .../sql/execution/planmerging/PlanMerger.scala     |  63 ++++++-----
 .../execution/planmerging/MergeSubplansSuite.scala | 116 +++++++++++++++++++++
 .../execution/planmerging/PlanMergingSuite.scala   |  40 +++++++
 3 files changed, 187 insertions(+), 32 deletions(-)

diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/planmerging/PlanMerger.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/planmerging/PlanMerger.scala
index 2eb50ce63a4e..8c7d2f28b56e 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/planmerging/PlanMerger.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/planmerging/PlanMerger.scala
@@ -311,12 +311,11 @@ class PlanMerger(
    * @param newPlanFilter A boolean [[Attribute]] in the merged plan that 
encodes the filter
    *                      condition from the new plan's side, to be applied as 
an aggregate
    *                      `FILTER (WHERE ...)` clause when the propagation 
reaches an enclosing
-   *                      [[Aggregate]] node. The boolean component is `true` 
if the attribute was
-   *                      freshly aliased and must be appended to enclosing 
[[Project]] nodes, or
-   *                      `false` if it was reused from an existing alias 
already present in the
-   *                      merged plan. `None` when no differing filter was 
propagated.
+   *                      [[Aggregate]] node. It can be an alias reused from a 
previous merge
+   *                      round. Such an alias may be missing from an 
enclosing [[Project]], which
+   *                      then has to append it. `None` when no differing 
filter was propagated.
    * @param cachedPlanFilter Like `newPlanFilter` but for the cached plan's 
side. Always a freshly
-   *                         created alias when present, so no `isNew` flag is 
needed.
+   *                         created alias when present, never a reused one.
    * @param dsv2Merged Whether an (equal-strict) DSv2 scan merge occurred 
anywhere in this merged
    *                   subtree. Unlike `dsv2DeferredScan` (consumed at the 
enclosing Filter that
    *                   builds the scan), this fact is propagated all the way 
up: it lets a Filter
@@ -327,7 +326,7 @@ class PlanMerger(
   case class TryMergeResult(
       mergedPlan: LogicalPlan,
       newPlanMapping: AttributeMap[Attribute],
-      newPlanFilter: Option[(Attribute, Boolean)] = None,
+      newPlanFilter: Option[Attribute] = None,
       cachedPlanFilter: Option[Attribute] = None,
       dsv2DeferredScan: Option[DSv2DeferredScan] = None,
       dsv2Merged: Boolean = false)
@@ -489,9 +488,8 @@ class PlanMerger(
               // A None filter means the side's aggregate expressions already 
carry their individual
               // FILTER attributes from a previous merge round and should be 
left unchanged.
               // Filter propagation is consumed here and not passed further up.
-              val filteredNPAggregateExpressions = 
npFilterOpt.fold(np.aggregateExpressions) {
-                case (f, _) => 
applyFilterToAggregateExpressions(np.aggregateExpressions, f)
-              }
+              val filteredNPAggregateExpressions = 
npFilterOpt.fold(np.aggregateExpressions)(
+                applyFilterToAggregateExpressions(np.aggregateExpressions, _))
               val filteredCPAggregateExpressions = 
cpFilterOpt.fold(cp.aggregateExpressions)(
                 applyFilterToAggregateExpressions(cp.aggregateExpressions, _))
               val (mergedAggregateExpressions, newNPMapping) =
@@ -539,27 +537,30 @@ class PlanMerger(
                   // merge rather than fail, keeping the merge best-effort.
                   mergedChild match {
                     case childProject: Project =>
-                      val newNPCondition = npFilter.fold(mappedNPCondition) {
-                        case (f, _) => And(f, mappedNPCondition)
-                      }
+                      val newNPCondition =
+                        npFilter.fold(mappedNPCondition)(And(_, 
mappedNPCondition))
                       // If newNPCondition is already aliased in the child 
Project (e.g. a third
                       // subplan whose filter matches one from a previous 
merge round), reuse the
-                      // existing attribute instead of creating a redundant 
alias.
+                      // existing attribute instead of creating a redundant 
alias. Only the aliases
+                      // the OR condition references are filter sides. A 
computed column with the
+                      // same expression is not one, because the OR doesn't 
keep its rows.
                       val existingNPFilter = 
childProject.projectList.collectFirst {
-                        case a: Alias if a.child.canonicalized == 
newNPCondition.canonicalized =>
+                        case a: Alias if 
cp.condition.references.contains(a.toAttribute) &&
+                            a.child.canonicalized == 
newNPCondition.canonicalized =>
                           a.toAttribute
                       }
                       val (newProjectList, newCondition, newNPFilterOut) =
                         existingNPFilter match {
                           case Some(reusedFilter) =>
                             // np matches an existing side: no new alias, OR 
condition unchanged.
-                            (childProject.projectList, cp.condition, 
(reusedFilter, false))
+                            (childProject.projectList, cp.condition, 
reusedFilter)
                           case None =>
                             val newNPFilterAlias =
                               Alias(newNPCondition, 
s"propagatedFilter_${PlanMerger.newId}")()
+                            val newNPFilter = newNPFilterAlias.toAttribute
                             (childProject.projectList :+ newNPFilterAlias,
-                              Or(cp.condition, newNPFilterAlias.toAttribute): 
Expression,
-                              (newNPFilterAlias.toAttribute, true))
+                              Or(cp.condition, newNPFilter): Expression,
+                              newNPFilter)
                         }
                       // Phase 2: the leaf re-merge rebuilt the scan with 
strict filters only,
                       // dropping the OR best-effort filter established in 
earlier rounds, so
@@ -589,8 +590,7 @@ class PlanMerger(
                   // Note: the new Project always uses mergedChild as its 
child (rather than
                   // flattening into an existing Project below) because 
mergedChild.output may
                   // contain previously-propagated filter attributes that 
cp.condition references.
-                  val newNPCondition =
-                    npFilter.fold(mappedNPCondition) { case (f, _) => And(f, 
mappedNPCondition) }
+                  val newNPCondition = npFilter.fold(mappedNPCondition)(And(_, 
mappedNPCondition))
                   val newCPCondition = cpFilter.fold(cp.condition)(And(_, 
cp.condition))
                   // The OR-widen moves both conditions into a boolean Project 
above the merged
                   // scan, so the scan itself would read the full table. Build 
the deferred scan
@@ -612,7 +612,7 @@ class PlanMerger(
                       val newFilter = Filter(Or(newNPFilter, newCPFilter), 
project)
                       newFilter.copyTagsFrom(cp)
                       newFilter.setTagValue(PlanMerger.MERGED_FILTER_TAG, ())
-                      TryMergeResult(newFilter, npMapping, Some((newNPFilter, 
true)),
+                      TryMergeResult(newFilter, npMapping, Some(newNPFilter),
                         Some(newCPFilter), dsv2Merged = dsv2Merged)
                     }
                 }
@@ -628,16 +628,14 @@ class PlanMerger(
             case TryMergeResult(mergedChild, npMapping, npFilter, cpFilter, _, 
dsv2Merged)
                 if cpFilter.isEmpty || symmetricFilterPropagationEnabled =>
               val mappedNPCondition = mapAttributes(np.condition, npMapping)
-              val newNPCondition = npFilter.fold(mappedNPCondition) {
-                case (f, _) => And(f, mappedNPCondition)
-              }
+              val newNPCondition = npFilter.fold(mappedNPCondition)(And(_, 
mappedNPCondition))
               val newNPFilterAlias =
                 Alias(newNPCondition, 
s"propagatedFilter_${PlanMerger.newId}")()
               val newNPFilter = newNPFilterAlias.toAttribute
               val project = Project(
                 mergedChild.output.toList :+ newNPFilterAlias,
                 mergedChild)
-              TryMergeResult(project, npMapping, Some((newNPFilter, true)), 
cpFilter,
+              TryMergeResult(project, npMapping, Some(newNPFilter), cpFilter,
                 dsv2Merged = dsv2Merged)
           }
         case (np, cp: Filter) if context.filterPropagationSupported =>
@@ -1048,14 +1046,16 @@ class PlanMerger(
   // non-matching expression from one side evaluates to null for rows that 
belong to the other side,
   // which is safe for aggregate FILTER (WHERE ...) semantics and avoids 
computing values for
   // irrelevant rows. The filter attributes themselves are appended to the 
merged expression list so
-  // they remain visible to the enclosing Aggregate that will consume them. A 
newPlanFilter with
-  // isNew=false was reused from a previous merge round and is already present 
in the merged child
-  // output, so it is not appended again.
+  // they remain visible to the enclosing Aggregate that will consume them. A 
newPlanFilter reused
+  // from a previous merge round can already be in the list, so it is appended 
only when missing.
+  // It can be missing, because an earlier round may have folded it into 
another filter alias and
+  // carried only that alias up. A cachedPlanFilter is always a fresh alias, 
so it is always
+  // appended.
   private def mergeNamedExpressions(
       newPlanExpressions: Seq[NamedExpression],
       cachedPlanExpressions: Seq[NamedExpression],
       newPlanMapping: AttributeMap[Attribute],
-      newPlanFilter: Option[(Attribute, Boolean)] = None,
+      newPlanFilter: Option[Attribute] = None,
       cachedPlanFilter: Option[Attribute] = None) = {
     val mergedExpressions = 
mutable.ArrayBuffer[NamedExpression](cachedPlanExpressions: _*)
     val matchedCachedIndices = mutable.HashSet.empty[Int]
@@ -1078,7 +1078,7 @@ class PlanMerger(
         // is only computed for rows that belong to the new plan side. Plain 
attribute references
         // are not wrapped since reading a column value is free.
         val wrappedExpr: NamedExpression = newPlanFilter match {
-          case Some((f, _)) if !withoutAlias.isInstanceOf[Attribute] =>
+          case Some(f) if !withoutAlias.isInstanceOf[Attribute] =>
             Alias(If(f, withoutAlias, Literal(null, withoutAlias.dataType)), 
mapped.name)()
           case _ => mapped
         }
@@ -1105,9 +1105,8 @@ class PlanMerger(
       }
     }
 
-    newPlanFilter.foreach {
-      case (f, true) => mergedExpressions += f
-      case _ =>
+    newPlanFilter.foreach { f =>
+      if (!mergedExpressions.exists(_.exprId == f.exprId)) mergedExpressions 
+= f
     }
     cachedPlanFilter.foreach(mergedExpressions += _)
 
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/planmerging/MergeSubplansSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/planmerging/MergeSubplansSuite.scala
index af50a1520e44..0f94ab43259c 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/planmerging/MergeSubplansSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/planmerging/MergeSubplansSuite.scala
@@ -1981,6 +1981,122 @@ class MergeSubplansSuite extends PlanTest {
     }
   }
 
+  // Merging subquery2 into subquery1 aliases `b = 2` as propagatedFilter_0 
below the join, and
+  // subquery2's post-join filter folds it into propagatedFilter_2. The 
Projects above the join only
+  // carry propagatedFilter_2, so when subquery3 reuses propagatedFilter_0 on 
its own, the merged
+  // Project under the aggregate has to append it.
+  private def checkReusedFilterAfterPostJoinFold(selectInSubquery3: Boolean): 
Unit = {
+    val subquery1 = ScalarSubquery(
+      testRelation.where($"b" === 1).join(testRelation2, LeftOuter, Some($"a" 
=== $"d"))
+        .select($"a")
+        .groupBy()(sum($"a").as("sum_a")))
+    val subquery2 = ScalarSubquery(
+      testRelation.where($"b" === 2).join(testRelation2, LeftOuter, Some($"a" 
=== $"d"))
+        .where(coalesce($"e", 10) > 5)
+        .select($"a")
+        .groupBy()(max($"a").as("max_a")))
+    val join3 =
+      testRelation.where($"b" === 2).join(testRelation2, LeftOuter, Some($"a" 
=== $"d"))
+    val subquery3 = ScalarSubquery(
+      (if (selectInSubquery3) join3.select($"a") else join3)
+        .groupBy()(count($"a").as("cnt")))
+    val originalQuery = testRelation.select(subquery1, subquery2, subquery3)
+
+    val f0Alias = Alias($"b" === 2, "propagatedFilter_0")()
+    val f0 = f0Alias.toAttribute
+    val f1Alias = Alias($"b" === 1, "propagatedFilter_1")()
+    val f1 = f1Alias.toAttribute
+    val joined = testRelation
+      .select(testRelation.output ++ Seq(f0Alias, f1Alias): _*)
+      .where(Or(f0, f1))
+      .join(testRelation2, LeftOuter, Some($"a" === $"d"))
+    val f2Alias = Alias(And(f0, coalesce($"e", 10) > 5), 
"propagatedFilter_2")()
+    val f2 = f2Alias.toAttribute
+    // Without its own Project, subquery3 merges all its join columns into the 
cached Project.
+    val mergedProjectList = if (selectInSubquery3) {
+      Seq($"a", f2, f1, f0)
+    } else {
+      Seq($"a", f2, f1, $"b", $"c", $"d", $"e", f0)
+    }
+    val mergedSubquery = joined
+      .select(joined.output :+ f2Alias: _*)
+      .select(mergedProjectList: _*)
+      .groupBy()(
+        sum($"a", Some(f1)).as("sum_a"),
+        max($"a", Some(f2)).as("max_a"),
+        count($"a", Some(f0)).as("cnt"))
+      .select(CreateNamedStruct(Seq(
+        Literal("sum_a"), $"sum_a",
+        Literal("max_a"), $"max_a",
+        Literal("cnt"), $"cnt"
+      )).as("mergedValue"))
+    val analyzedMergedSubquery = mergedSubquery.analyze
+    val correctAnswer = WithCTE(
+      testRelation.select(
+        extractorExpression(0, analyzedMergedSubquery.output, 0),
+        extractorExpression(0, analyzedMergedSubquery.output, 1),
+        extractorExpression(0, analyzedMergedSubquery.output, 2)),
+      Seq(definitionNode(analyzedMergedSubquery, 0)))
+
+    withSQLConf(
+        SQLConf.MERGE_SUBPLANS_SYMMETRIC_FILTER_PROPAGATION_ENABLED.key -> 
"true",
+        SQLConf.MERGE_SUBPLANS_FILTER_PROPAGATION_THROUGH_JOIN_ENABLED.key -> 
"true") {
+      comparePlans(Optimize.execute(originalQuery.analyze), 
correctAnswer.analyze)
+    }
+  }
+
+  test("SPARK-60084: Reuse a filter that an earlier merge folded into a 
post-join filter") {
+    checkReusedFilterAfterPostJoinFold(selectInSubquery3 = true)
+  }
+
+  test("SPARK-60084: Reuse a folded filter when the new plan has no Project 
above the join") {
+    // The `(np, cp: Project)` arm builds the merged Project from subquery3's 
join output, and has
+    // to append the reused filter there.
+    checkReusedFilterAfterPostJoinFold(selectInSubquery3 = false)
+  }
+
+  test("SPARK-60084: Do not reuse a computed column as a propagated filter") {
+    // subquery3 computes `b > 2` as a column and filters on the same 
expression. The column lands
+    // in the Project under the merged Filter, but the Filter's OR doesn't 
keep its rows, so
+    // subquery3 needs a filter alias of its own.
+    val subquery1 = ScalarSubquery(testRelation.where($"a" < 
1).groupBy()(sum($"a").as("sum_a")))
+    val subquery2 = ScalarSubquery(testRelation.where($"a" > 
5).groupBy()(max($"a").as("max_a")))
+    val subquery3 = ScalarSubquery(
+      testRelation.select($"a", $"b", ($"b" > 2).as("flag")).where($"b" > 2)
+        .groupBy()(count($"a").as("cnt")))
+    val originalQuery = testRelation.select(subquery1, subquery2, subquery3)
+
+    val f0Alias = Alias($"a" > 5, "propagatedFilter_0")()
+    val f0 = f0Alias.toAttribute
+    val f1Alias = Alias($"a" < 1, "propagatedFilter_1")()
+    val f1 = f1Alias.toAttribute
+    val f2Alias = Alias($"b" > 2, "propagatedFilter_2")()
+    val f2 = f2Alias.toAttribute
+    val mergedSubquery = testRelation
+      .select(testRelation.output ++ Seq(f0Alias, f1Alias, ($"b" > 
2).as("flag"), f2Alias): _*)
+      .where(Or(Or(f0, f1), f2))
+      .groupBy()(
+        sum($"a", Some(f1)).as("sum_a"),
+        max($"a", Some(f0)).as("max_a"),
+        count($"a", Some(f2)).as("cnt"))
+      .select(CreateNamedStruct(Seq(
+        Literal("sum_a"), $"sum_a",
+        Literal("max_a"), $"max_a",
+        Literal("cnt"), $"cnt"
+      )).as("mergedValue"))
+    val analyzedMergedSubquery = mergedSubquery.analyze
+    val correctAnswer = WithCTE(
+      testRelation.select(
+        extractorExpression(0, analyzedMergedSubquery.output, 0),
+        extractorExpression(0, analyzedMergedSubquery.output, 1),
+        extractorExpression(0, analyzedMergedSubquery.output, 2)),
+      Seq(definitionNode(analyzedMergedSubquery, 0)))
+
+    
withSQLConf(SQLConf.MERGE_SUBPLANS_SYMMETRIC_FILTER_PROPAGATION_ENABLED.key -> 
"true") {
+      comparePlans(Optimize.execute(originalQuery.analyze), 
correctAnswer.analyze)
+    }
+  }
+
   // ---- SPARK-40259: generic DSv2 scan merge ----
 
   private val v2Table = new TestV2Table(StructType(Seq(
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/planmerging/PlanMergingSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/planmerging/PlanMergingSuite.scala
index 127bef8e2e91..b1c430404a0f 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/planmerging/PlanMergingSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/planmerging/PlanMergingSuite.scala
@@ -439,4 +439,44 @@ class PlanMergingSuite extends SharedSparkSession
       }
     }
   }
+
+  test("SPARK-60084: Reuse a filter that an earlier merge folded into a 
post-join filter") {
+    // Merging the 2nd subquery into the 1st aliases `l.b = 2` below the join, 
and the 2nd
+    // subquery's post-join filter folds that alias into a new one, so only 
the new alias is
+    // carried up to the aggregate. The 3rd subquery then reuses the `l.b = 2` 
alias on its own.
+    Seq(false, true).foreach { enableAQE =>
+      withSQLConf(
+        SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> enableAQE.toString,
+        SQLConf.MERGE_SUBPLANS_SYMMETRIC_FILTER_PROPAGATION_ENABLED.key -> 
"true",
+        SQLConf.MERGE_SUBPLANS_FILTER_PROPAGATION_THROUGH_JOIN_ENABLED.key -> 
"true",
+        // ObjectSerializerPruning produces different scan shapes depending on 
whether a Filter is
+        // present. Disabling the rule makes both scans identical so 
PlanMerger can merge them.
+        SQLConf.OPTIMIZER_EXCLUDED_RULES.key ->
+          "org.apache.spark.sql.catalyst.optimizer.ObjectSerializerPruning") {
+        val df = sql(
+          """
+            |SELECT
+            |  (SELECT sum(l.a)
+            |   FROM testData2 l LEFT JOIN testData2 r ON l.a = r.a WHERE l.b 
= 1),
+            |  (SELECT max(l.a)
+            |   FROM testData2 l LEFT JOIN testData2 r ON l.a = r.a
+            |   WHERE l.b = 2 AND coalesce(r.b, 3) > 1),
+            |  (SELECT count(*)
+            |   FROM testData2 l LEFT JOIN testData2 r ON l.a = r.a WHERE l.b 
= 2)
+          """.stripMargin)
+
+        checkAnswer(df, Row(12, 3, 6) :: Nil)
+
+        val plan = df.queryExecution.executedPlan
+        val subqueryIds = collectWithSubqueries(plan) { case s: SubqueryExec 
=> s.id }
+        val reusedSubqueryIds = collectWithSubqueries(plan) {
+          case rs: ReusedSubqueryExec => rs.child.id
+        }
+
+        assert(subqueryIds.size == 1, "Missing or unexpected SubqueryExec in 
the plan")
+        assert(reusedSubqueryIds.size == 2,
+          "Missing or unexpected ReusedSubqueryExec in the plan")
+      }
+    }
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to