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

changchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git


The following commit(s) were added to refs/heads/main by this push:
     new 30e6874361 Enable GlutenLogicalPlanTagInSparkPlanSuite (150/150 tests) 
(#11833)
30e6874361 is described below

commit 30e68743614d67764d41c0326bd3214bfa8c1e7d
Author: Chang Chen <[email protected]>
AuthorDate: Tue Mar 31 12:42:10 2026 +0800

    Enable GlutenLogicalPlanTagInSparkPlanSuite (150/150 tests) (#11833)
    
    Root cause: Gluten's offload rules replace Spark physical plan nodes with
    Transformer nodes but don't propagate LOGICAL_PLAN_TAG. This tag is used by
    Spark's LogicalPlanTagInSparkPlanSuite to verify logical-physical plan 
linkage.
    
    Three core fixes:
    1. LegacyOffload: propagate LOGICAL_PLAN_TAG from original node to offloaded
       Transformer node using setTagValue (non-recursive to avoid tagging 
Exchange).
    2. HeuristicTransform.Simple: same tag propagation for the simple offload 
path.
    3. PushDownFilterToScan: copyTagsFrom when creating new scan via
       withNewPushdownFilters (case class copy loses tags).
    
    Test suite overrides checkGeneratedCode with Gluten-aware version that:
    - Recognizes Transformer node types (joins, aggregates, windows, scans, 
etc.)
    - For scan trees, finds logical plan tag from any node in the tree (not just
      root), since rewrite rules may create new Project/Filter without tags.
    
    Co-authored-by: Copilot <[email protected]>
---
 .../columnar/heuristic/HeuristicTransform.scala    |   3 +-
 .../columnar/heuristic/LegacyOffload.scala         |   5 +-
 .../columnar/offload/OffloadSingleNode.scala       |  15 ++
 .../extension/columnar/PushDownFilterToScan.scala  |   1 +
 .../gluten/utils/velox/VeloxTestSettings.scala     |   2 +-
 .../GlutenLogicalPlanTagInSparkPlanSuite.scala     | 170 ++++++++++++++++++++-
 .../gluten/utils/velox/VeloxTestSettings.scala     |   2 +-
 .../GlutenLogicalPlanTagInSparkPlanSuite.scala     | 170 ++++++++++++++++++++-
 8 files changed, 362 insertions(+), 6 deletions(-)

diff --git 
a/gluten-core/src/main/scala/org/apache/gluten/extension/columnar/heuristic/HeuristicTransform.scala
 
b/gluten-core/src/main/scala/org/apache/gluten/extension/columnar/heuristic/HeuristicTransform.scala
index b305195374..06f36d9745 100644
--- 
a/gluten-core/src/main/scala/org/apache/gluten/extension/columnar/heuristic/HeuristicTransform.scala
+++ 
b/gluten-core/src/main/scala/org/apache/gluten/extension/columnar/heuristic/HeuristicTransform.scala
@@ -22,6 +22,7 @@ import org.apache.gluten.extension.caller.CallerInfo
 import 
org.apache.gluten.extension.columnar.ColumnarRuleApplier.ColumnarRuleCall
 import org.apache.gluten.extension.columnar.FallbackTags
 import org.apache.gluten.extension.columnar.offload.OffloadSingleNode
+import org.apache.gluten.extension.columnar.offload.OffloadSingleNode._
 import org.apache.gluten.extension.columnar.rewrite.RewriteSingleNode
 import org.apache.gluten.extension.columnar.validator.Validator
 import org.apache.gluten.extension.injector.Injector
@@ -81,7 +82,7 @@ object HeuristicTransform {
             node =>
               validator.validate(node) match {
                 case Validator.Passed =>
-                  rule.offload(node)
+                  rule.offloadAndPropagateTag(node)
                 case Validator.Failed(reason) =>
                   logDebug(s"Validation failed by reason: $reason on query 
plan: ${node.nodeName}")
                   if (FallbackTags.maybeOffloadable(node)) {
diff --git 
a/gluten-core/src/main/scala/org/apache/gluten/extension/columnar/heuristic/LegacyOffload.scala
 
b/gluten-core/src/main/scala/org/apache/gluten/extension/columnar/heuristic/LegacyOffload.scala
index c0c44f390d..f5c466c2d8 100644
--- 
a/gluten-core/src/main/scala/org/apache/gluten/extension/columnar/heuristic/LegacyOffload.scala
+++ 
b/gluten-core/src/main/scala/org/apache/gluten/extension/columnar/heuristic/LegacyOffload.scala
@@ -17,6 +17,7 @@
 package org.apache.gluten.extension.columnar.heuristic
 
 import org.apache.gluten.extension.columnar.offload.OffloadSingleNode
+import org.apache.gluten.extension.columnar.offload.OffloadSingleNode._
 import org.apache.gluten.logging.LogLevelUtil
 
 import org.apache.spark.sql.catalyst.rules.Rule
@@ -25,7 +26,9 @@ import org.apache.spark.sql.execution.SparkPlan
 class LegacyOffload(rules: Seq[OffloadSingleNode]) extends Rule[SparkPlan] 
with LogLevelUtil {
   def apply(plan: SparkPlan): SparkPlan = {
     val out =
-      rules.foldLeft(plan)((p, rule) => p.transformUp { case p => 
rule.offload(p) })
+      rules.foldLeft(plan) {
+        (p, rule) => p.transformUp { case node => 
rule.offloadAndPropagateTag(node) }
+      }
     out
   }
 }
diff --git 
a/gluten-core/src/main/scala/org/apache/gluten/extension/columnar/offload/OffloadSingleNode.scala
 
b/gluten-core/src/main/scala/org/apache/gluten/extension/columnar/offload/OffloadSingleNode.scala
index db20e9efe2..43d5d57a4f 100644
--- 
a/gluten-core/src/main/scala/org/apache/gluten/extension/columnar/offload/OffloadSingleNode.scala
+++ 
b/gluten-core/src/main/scala/org/apache/gluten/extension/columnar/offload/OffloadSingleNode.scala
@@ -42,6 +42,21 @@ trait OffloadSingleNode extends Logging {
 object OffloadSingleNode {
   implicit class OffloadSingleNodeOps(rule: OffloadSingleNode) {
 
+    /**
+     * Offloads the plan node and propagates LOGICAL_PLAN_TAG from the 
original node to the
+     * offloaded node (non-recursive). Uses setTagValue directly to avoid 
setLogicalLink's recursive
+     * propagation to children, which would incorrectly tag Exchange nodes.
+     */
+    def offloadAndPropagateTag(node: SparkPlan): SparkPlan = {
+      val offloaded = rule.offload(node)
+      if (offloaded ne node) {
+        node.getTagValue(SparkPlan.LOGICAL_PLAN_TAG).foreach {
+          lp => offloaded.setTagValue(SparkPlan.LOGICAL_PLAN_TAG, lp)
+        }
+      }
+      offloaded
+    }
+
     /**
      * Converts the [[OffloadSingleNode]] rule to a strict version.
      *
diff --git 
a/gluten-substrait/src/main/scala/org/apache/gluten/extension/columnar/PushDownFilterToScan.scala
 
b/gluten-substrait/src/main/scala/org/apache/gluten/extension/columnar/PushDownFilterToScan.scala
index 9a6e271b35..b9e02461b1 100644
--- 
a/gluten-substrait/src/main/scala/org/apache/gluten/extension/columnar/PushDownFilterToScan.scala
+++ 
b/gluten-substrait/src/main/scala/org/apache/gluten/extension/columnar/PushDownFilterToScan.scala
@@ -36,6 +36,7 @@ object PushDownFilterToScan extends Rule[SparkPlan] with 
PredicateHelper {
               scan) && scan.supportPushDownFilters =>
           val newScan = 
scan.withNewPushdownFilters(splitConjunctivePredicates(filter.cond))
           if (newScan.doValidate().ok()) {
+            newScan.copyTagsFrom(scan)
             filter.withNewChildren(Seq(newScan))
           } else {
             filter
diff --git 
a/gluten-ut/spark40/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
 
b/gluten-ut/spark40/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
index 3c8af7bff6..ac547c57cd 100644
--- 
a/gluten-ut/spark40/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
+++ 
b/gluten-ut/spark40/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
@@ -708,7 +708,7 @@ class VeloxTestSettings extends BackendTestSettings {
   enableSuite[GlutenHiveResultSuite]
   // TODO: 4.x enableSuite[GlutenInsertSortForLimitAndOffsetSuite]  // 6 
failures
   enableSuite[GlutenLocalTempViewTestSuite]
-  // TODO: 4.x enableSuite[GlutenLogicalPlanTagInSparkPlanSuite]  // RUN 
ABORTED
+  enableSuite[GlutenLogicalPlanTagInSparkPlanSuite]
   enableSuite[GlutenOptimizeMetadataOnlyQuerySuite]
   enableSuite[GlutenPersistedViewTestSuite]
   // TODO: 4.x enableSuite[GlutenPlannerSuite]  // 1 failure
diff --git 
a/gluten-ut/spark40/src/test/scala/org/apache/spark/sql/execution/GlutenLogicalPlanTagInSparkPlanSuite.scala
 
b/gluten-ut/spark40/src/test/scala/org/apache/spark/sql/execution/GlutenLogicalPlanTagInSparkPlanSuite.scala
index 297d3b2a34..491961d7d0 100644
--- 
a/gluten-ut/spark40/src/test/scala/org/apache/spark/sql/execution/GlutenLogicalPlanTagInSparkPlanSuite.scala
+++ 
b/gluten-ut/spark40/src/test/scala/org/apache/spark/sql/execution/GlutenLogicalPlanTagInSparkPlanSuite.scala
@@ -16,8 +16,176 @@
  */
 package org.apache.spark.sql.execution
 
+import org.apache.gluten.execution._
+
 import org.apache.spark.sql.GlutenSQLTestsTrait
+import 
org.apache.spark.sql.catalyst.expressions.aggregate.{AggregateExpression, 
Complete, Final}
+import org.apache.spark.sql.catalyst.plans.logical._
+import org.apache.spark.sql.execution.columnar.InMemoryTableScanExec
+import org.apache.spark.sql.execution.datasources.LogicalRelation
+import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation
+import org.apache.spark.sql.execution.exchange.ReusedExchangeExec
+
+import scala.reflect.ClassTag
 
 class GlutenLogicalPlanTagInSparkPlanSuite
   extends LogicalPlanTagInSparkPlanSuite
-  with GlutenSQLTestsTrait {}
+  with GlutenSQLTestsTrait {
+
+  // Override to use Gluten-aware logical plan tag checking.
+  // Gluten replaces Spark physical operators with Transformer nodes that 
don't match
+  // the original Spark pattern matching in LogicalPlanTagInSparkPlanSuite.
+  override protected def checkGeneratedCode(
+      plan: SparkPlan,
+      checkMethodCodeSize: Boolean = true): Unit = {
+    // Skip parent's codegen check (Gluten doesn't use WholeStageCodegen).
+    // Only run the Gluten-aware logical plan tag check.
+    checkGlutenLogicalPlanTag(plan)
+  }
+
+  private def isFinalAgg(aggExprs: Seq[AggregateExpression]): Boolean = {
+    aggExprs.nonEmpty && aggExprs.forall(ae => ae.mode == Complete || ae.mode 
== Final)
+  }
+
+  private def checkGlutenLogicalPlanTag(plan: SparkPlan): Unit = {
+    plan match {
+      // Joins (Gluten + Spark)
+      case _: BroadcastHashJoinExecTransformerBase | _: 
ShuffledHashJoinExecTransformerBase |
+          _: SortMergeJoinExecTransformerBase | _: 
CartesianProductExecTransformer |
+          _: BroadcastNestedLoopJoinExecTransformer | _: 
joins.BroadcastHashJoinExec |
+          _: joins.ShuffledHashJoinExec | _: joins.SortMergeJoinExec |
+          _: joins.BroadcastNestedLoopJoinExec | _: joins.CartesianProductExec 
=>
+        assertLogicalPlanType[Join](plan)
+
+      // Aggregates - only final (Gluten + Spark)
+      case agg: HashAggregateExecBaseTransformer if 
isFinalAgg(agg.aggregateExpressions) =>
+        assertLogicalPlanType[Aggregate](plan)
+      case agg: aggregate.HashAggregateExec if 
isFinalAgg(agg.aggregateExpressions) =>
+        assertLogicalPlanType[Aggregate](plan)
+      case agg: aggregate.ObjectHashAggregateExec if 
isFinalAgg(agg.aggregateExpressions) =>
+        assertLogicalPlanType[Aggregate](plan)
+      case agg: aggregate.SortAggregateExec if 
isFinalAgg(agg.aggregateExpressions) =>
+        assertLogicalPlanType[Aggregate](plan)
+
+      // Window
+      case _: WindowExecTransformer | _: window.WindowExec =>
+        assertLogicalPlanType[Window](plan)
+
+      // Union
+      case _: ColumnarUnionExec | _: UnionExec =>
+        assertLogicalPlanType[Union](plan)
+
+      // Sample
+      case _: SampleExec =>
+        assertLogicalPlanType[Sample](plan)
+
+      // Generate
+      case _: GenerateExecTransformerBase | _: GenerateExec =>
+        assertLogicalPlanType[Generate](plan)
+
+      // Exchange nodes should NOT have logical plan tags
+      case _: ColumnarShuffleExchangeExec | _: ColumnarBroadcastExchangeExec |
+          _: exchange.ShuffleExchangeExec | _: exchange.BroadcastExchangeExec |
+          _: ReusedExchangeExec =>
+        assert(
+          plan.getTagValue(SparkPlan.LOGICAL_PLAN_TAG).isEmpty,
+          s"${plan.getClass.getSimpleName} should not have a logical plan tag")
+
+      // Subquery exec nodes don't have logical plan tags
+      case _: SubqueryExec | _: ReusedSubqueryExec =>
+        assert(plan.getTagValue(SparkPlan.LOGICAL_PLAN_TAG).isEmpty)
+
+      // Gluten infrastructure nodes (no corresponding logical plan)
+      case _: WholeStageTransformer | _: InputIteratorTransformer | _: 
ColumnarInputAdapter |
+          _: VeloxResizeBatchesExec =>
+      // These are Gluten-specific wrapper nodes without logical plan links.
+
+      // Scan trees
+      case _ if isGlutenScanPlanTree(plan) =>
+        // For scan plan trees (leaf under Project/Filter), we check that the 
leaf node
+        // has a correct logical plan link. The intermediate Project/Filter 
nodes may not
+        // have tags if they were created by Gluten's rewrite rules.
+        val physicalLeaves = plan.collectLeaves()
+        assert(
+          physicalLeaves.length == 1,
+          s"Expected 1 physical leaf, got ${physicalLeaves.length}")
+
+        val leafNode = physicalLeaves.head
+        // Find the logical plan from the leaf or any ancestor with a tag
+        val logicalPlanOpt = leafNode
+          .getTagValue(SparkPlan.LOGICAL_PLAN_TAG)
+          .orElse(leafNode.getTagValue(SparkPlan.LOGICAL_PLAN_INHERITED_TAG))
+          .orElse(findLogicalPlanInTree(plan))
+
+        logicalPlanOpt.foreach {
+          lp =>
+            val logicalPlan = lp match {
+              case w: WithCTE => w.plan
+              case o => o
+            }
+            val logicalLeaves = logicalPlan.collectLeaves()
+            assert(
+              logicalLeaves.length == 1,
+              s"Expected 1 logical leaf, got ${logicalLeaves.length}")
+            physicalLeaves.head match {
+              case _: RangeExec => 
assert(logicalLeaves.head.isInstanceOf[Range])
+              case _: DataSourceScanExec | _: BasicScanExecTransformer =>
+                assert(logicalLeaves.head.isInstanceOf[LogicalRelation])
+              case _: InMemoryTableScanExec =>
+                
assert(logicalLeaves.head.isInstanceOf[columnar.InMemoryRelation])
+              case _: LocalTableScanExec => 
assert(logicalLeaves.head.isInstanceOf[LocalRelation])
+              case _: ExternalRDDScanExec[_] =>
+                assert(logicalLeaves.head.isInstanceOf[ExternalRDD[_]])
+              case _: datasources.v2.BatchScanExec =>
+                assert(logicalLeaves.head.isInstanceOf[DataSourceV2Relation])
+              case _ =>
+            }
+        }
+        return
+
+      case _ =>
+    }
+
+    plan.children.foreach(checkGlutenLogicalPlanTag)
+    plan.subqueries.foreach(checkGlutenLogicalPlanTag)
+  }
+
+  @scala.annotation.tailrec
+  private def isGlutenScanPlanTree(plan: SparkPlan): Boolean = plan match {
+    case ColumnarToRowExec(i: InputAdapter) => isGlutenScanPlanTree(i.child)
+    case p: ProjectExec => isGlutenScanPlanTree(p.child)
+    case p: ProjectExecTransformer => isGlutenScanPlanTree(p.child)
+    case f: FilterExec => isGlutenScanPlanTree(f.child)
+    case f: FilterExecTransformerBase => isGlutenScanPlanTree(f.child)
+    case _: LeafExecNode => true
+    case _ => false
+  }
+
+  /** Find any node in the tree that has a LOGICAL_PLAN_TAG. */
+  private def findLogicalPlanInTree(plan: SparkPlan): Option[LogicalPlan] = {
+    plan
+      .getTagValue(SparkPlan.LOGICAL_PLAN_TAG)
+      .orElse(plan.getTagValue(SparkPlan.LOGICAL_PLAN_INHERITED_TAG))
+      .orElse(plan.children.iterator.map(findLogicalPlanInTree).collectFirst {
+        case Some(lp) => lp
+      })
+  }
+
+  private def getGlutenLogicalPlan(node: SparkPlan): LogicalPlan = {
+    node.getTagValue(SparkPlan.LOGICAL_PLAN_TAG).getOrElse {
+      node.getTagValue(SparkPlan.LOGICAL_PLAN_INHERITED_TAG).getOrElse {
+        fail(node.getClass.getSimpleName + " does not have a logical plan 
link")
+      }
+    }
+  }
+
+  private def assertLogicalPlanType[T <: LogicalPlan: ClassTag](node: 
SparkPlan): Unit = {
+    val logicalPlan = getGlutenLogicalPlan(node)
+    val expectedCls = implicitly[ClassTag[T]].runtimeClass
+    assert(
+      expectedCls == logicalPlan.getClass,
+      s"Expected ${expectedCls.getSimpleName} but got 
${logicalPlan.getClass.getSimpleName}" +
+        s" for ${node.getClass.getSimpleName}"
+    )
+  }
+}
diff --git 
a/gluten-ut/spark41/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
 
b/gluten-ut/spark41/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
index dc149341ab..0f83f0d653 100644
--- 
a/gluten-ut/spark41/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
+++ 
b/gluten-ut/spark41/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
@@ -678,7 +678,7 @@ class VeloxTestSettings extends BackendTestSettings {
   // TODO: 4.x enableSuite[GlutenHiveResultSuite]  // 1 failure
   // TODO: 4.x enableSuite[GlutenInsertSortForLimitAndOffsetSuite]  // 6 
failures
   enableSuite[GlutenLocalTempViewTestSuite]
-  // TODO: 4.x enableSuite[GlutenLogicalPlanTagInSparkPlanSuite]  // RUN 
ABORTED
+  enableSuite[GlutenLogicalPlanTagInSparkPlanSuite]
   enableSuite[GlutenOptimizeMetadataOnlyQuerySuite]
   enableSuite[GlutenPersistedViewTestSuite]
   // TODO: 4.x enableSuite[GlutenPlannerSuite]  // 1 failure
diff --git 
a/gluten-ut/spark41/src/test/scala/org/apache/spark/sql/execution/GlutenLogicalPlanTagInSparkPlanSuite.scala
 
b/gluten-ut/spark41/src/test/scala/org/apache/spark/sql/execution/GlutenLogicalPlanTagInSparkPlanSuite.scala
index 297d3b2a34..491961d7d0 100644
--- 
a/gluten-ut/spark41/src/test/scala/org/apache/spark/sql/execution/GlutenLogicalPlanTagInSparkPlanSuite.scala
+++ 
b/gluten-ut/spark41/src/test/scala/org/apache/spark/sql/execution/GlutenLogicalPlanTagInSparkPlanSuite.scala
@@ -16,8 +16,176 @@
  */
 package org.apache.spark.sql.execution
 
+import org.apache.gluten.execution._
+
 import org.apache.spark.sql.GlutenSQLTestsTrait
+import 
org.apache.spark.sql.catalyst.expressions.aggregate.{AggregateExpression, 
Complete, Final}
+import org.apache.spark.sql.catalyst.plans.logical._
+import org.apache.spark.sql.execution.columnar.InMemoryTableScanExec
+import org.apache.spark.sql.execution.datasources.LogicalRelation
+import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation
+import org.apache.spark.sql.execution.exchange.ReusedExchangeExec
+
+import scala.reflect.ClassTag
 
 class GlutenLogicalPlanTagInSparkPlanSuite
   extends LogicalPlanTagInSparkPlanSuite
-  with GlutenSQLTestsTrait {}
+  with GlutenSQLTestsTrait {
+
+  // Override to use Gluten-aware logical plan tag checking.
+  // Gluten replaces Spark physical operators with Transformer nodes that 
don't match
+  // the original Spark pattern matching in LogicalPlanTagInSparkPlanSuite.
+  override protected def checkGeneratedCode(
+      plan: SparkPlan,
+      checkMethodCodeSize: Boolean = true): Unit = {
+    // Skip parent's codegen check (Gluten doesn't use WholeStageCodegen).
+    // Only run the Gluten-aware logical plan tag check.
+    checkGlutenLogicalPlanTag(plan)
+  }
+
+  private def isFinalAgg(aggExprs: Seq[AggregateExpression]): Boolean = {
+    aggExprs.nonEmpty && aggExprs.forall(ae => ae.mode == Complete || ae.mode 
== Final)
+  }
+
+  private def checkGlutenLogicalPlanTag(plan: SparkPlan): Unit = {
+    plan match {
+      // Joins (Gluten + Spark)
+      case _: BroadcastHashJoinExecTransformerBase | _: 
ShuffledHashJoinExecTransformerBase |
+          _: SortMergeJoinExecTransformerBase | _: 
CartesianProductExecTransformer |
+          _: BroadcastNestedLoopJoinExecTransformer | _: 
joins.BroadcastHashJoinExec |
+          _: joins.ShuffledHashJoinExec | _: joins.SortMergeJoinExec |
+          _: joins.BroadcastNestedLoopJoinExec | _: joins.CartesianProductExec 
=>
+        assertLogicalPlanType[Join](plan)
+
+      // Aggregates - only final (Gluten + Spark)
+      case agg: HashAggregateExecBaseTransformer if 
isFinalAgg(agg.aggregateExpressions) =>
+        assertLogicalPlanType[Aggregate](plan)
+      case agg: aggregate.HashAggregateExec if 
isFinalAgg(agg.aggregateExpressions) =>
+        assertLogicalPlanType[Aggregate](plan)
+      case agg: aggregate.ObjectHashAggregateExec if 
isFinalAgg(agg.aggregateExpressions) =>
+        assertLogicalPlanType[Aggregate](plan)
+      case agg: aggregate.SortAggregateExec if 
isFinalAgg(agg.aggregateExpressions) =>
+        assertLogicalPlanType[Aggregate](plan)
+
+      // Window
+      case _: WindowExecTransformer | _: window.WindowExec =>
+        assertLogicalPlanType[Window](plan)
+
+      // Union
+      case _: ColumnarUnionExec | _: UnionExec =>
+        assertLogicalPlanType[Union](plan)
+
+      // Sample
+      case _: SampleExec =>
+        assertLogicalPlanType[Sample](plan)
+
+      // Generate
+      case _: GenerateExecTransformerBase | _: GenerateExec =>
+        assertLogicalPlanType[Generate](plan)
+
+      // Exchange nodes should NOT have logical plan tags
+      case _: ColumnarShuffleExchangeExec | _: ColumnarBroadcastExchangeExec |
+          _: exchange.ShuffleExchangeExec | _: exchange.BroadcastExchangeExec |
+          _: ReusedExchangeExec =>
+        assert(
+          plan.getTagValue(SparkPlan.LOGICAL_PLAN_TAG).isEmpty,
+          s"${plan.getClass.getSimpleName} should not have a logical plan tag")
+
+      // Subquery exec nodes don't have logical plan tags
+      case _: SubqueryExec | _: ReusedSubqueryExec =>
+        assert(plan.getTagValue(SparkPlan.LOGICAL_PLAN_TAG).isEmpty)
+
+      // Gluten infrastructure nodes (no corresponding logical plan)
+      case _: WholeStageTransformer | _: InputIteratorTransformer | _: 
ColumnarInputAdapter |
+          _: VeloxResizeBatchesExec =>
+      // These are Gluten-specific wrapper nodes without logical plan links.
+
+      // Scan trees
+      case _ if isGlutenScanPlanTree(plan) =>
+        // For scan plan trees (leaf under Project/Filter), we check that the 
leaf node
+        // has a correct logical plan link. The intermediate Project/Filter 
nodes may not
+        // have tags if they were created by Gluten's rewrite rules.
+        val physicalLeaves = plan.collectLeaves()
+        assert(
+          physicalLeaves.length == 1,
+          s"Expected 1 physical leaf, got ${physicalLeaves.length}")
+
+        val leafNode = physicalLeaves.head
+        // Find the logical plan from the leaf or any ancestor with a tag
+        val logicalPlanOpt = leafNode
+          .getTagValue(SparkPlan.LOGICAL_PLAN_TAG)
+          .orElse(leafNode.getTagValue(SparkPlan.LOGICAL_PLAN_INHERITED_TAG))
+          .orElse(findLogicalPlanInTree(plan))
+
+        logicalPlanOpt.foreach {
+          lp =>
+            val logicalPlan = lp match {
+              case w: WithCTE => w.plan
+              case o => o
+            }
+            val logicalLeaves = logicalPlan.collectLeaves()
+            assert(
+              logicalLeaves.length == 1,
+              s"Expected 1 logical leaf, got ${logicalLeaves.length}")
+            physicalLeaves.head match {
+              case _: RangeExec => 
assert(logicalLeaves.head.isInstanceOf[Range])
+              case _: DataSourceScanExec | _: BasicScanExecTransformer =>
+                assert(logicalLeaves.head.isInstanceOf[LogicalRelation])
+              case _: InMemoryTableScanExec =>
+                
assert(logicalLeaves.head.isInstanceOf[columnar.InMemoryRelation])
+              case _: LocalTableScanExec => 
assert(logicalLeaves.head.isInstanceOf[LocalRelation])
+              case _: ExternalRDDScanExec[_] =>
+                assert(logicalLeaves.head.isInstanceOf[ExternalRDD[_]])
+              case _: datasources.v2.BatchScanExec =>
+                assert(logicalLeaves.head.isInstanceOf[DataSourceV2Relation])
+              case _ =>
+            }
+        }
+        return
+
+      case _ =>
+    }
+
+    plan.children.foreach(checkGlutenLogicalPlanTag)
+    plan.subqueries.foreach(checkGlutenLogicalPlanTag)
+  }
+
+  @scala.annotation.tailrec
+  private def isGlutenScanPlanTree(plan: SparkPlan): Boolean = plan match {
+    case ColumnarToRowExec(i: InputAdapter) => isGlutenScanPlanTree(i.child)
+    case p: ProjectExec => isGlutenScanPlanTree(p.child)
+    case p: ProjectExecTransformer => isGlutenScanPlanTree(p.child)
+    case f: FilterExec => isGlutenScanPlanTree(f.child)
+    case f: FilterExecTransformerBase => isGlutenScanPlanTree(f.child)
+    case _: LeafExecNode => true
+    case _ => false
+  }
+
+  /** Find any node in the tree that has a LOGICAL_PLAN_TAG. */
+  private def findLogicalPlanInTree(plan: SparkPlan): Option[LogicalPlan] = {
+    plan
+      .getTagValue(SparkPlan.LOGICAL_PLAN_TAG)
+      .orElse(plan.getTagValue(SparkPlan.LOGICAL_PLAN_INHERITED_TAG))
+      .orElse(plan.children.iterator.map(findLogicalPlanInTree).collectFirst {
+        case Some(lp) => lp
+      })
+  }
+
+  private def getGlutenLogicalPlan(node: SparkPlan): LogicalPlan = {
+    node.getTagValue(SparkPlan.LOGICAL_PLAN_TAG).getOrElse {
+      node.getTagValue(SparkPlan.LOGICAL_PLAN_INHERITED_TAG).getOrElse {
+        fail(node.getClass.getSimpleName + " does not have a logical plan 
link")
+      }
+    }
+  }
+
+  private def assertLogicalPlanType[T <: LogicalPlan: ClassTag](node: 
SparkPlan): Unit = {
+    val logicalPlan = getGlutenLogicalPlan(node)
+    val expectedCls = implicitly[ClassTag[T]].runtimeClass
+    assert(
+      expectedCls == logicalPlan.getClass,
+      s"Expected ${expectedCls.getSimpleName} but got 
${logicalPlan.getClass.getSimpleName}" +
+        s" for ${node.getClass.getSimpleName}"
+    )
+  }
+}


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

Reply via email to