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

andygrove pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-comet.git


The following commit(s) were added to refs/heads/main by this push:
     new 5b8a45e061 refactor: replace hand-coded rollup of expression fallback 
reasons onto operators (#5236)
5b8a45e061 is described below

commit 5b8a45e061691a076fefcf5911aa1dcdb8ed93e5
Author: Andy Grove <[email protected]>
AuthorDate: Tue Aug 4 13:42:36 2026 -0600

    refactor: replace hand-coded rollup of expression fallback reasons onto 
operators (#5236)
    
    * refactor: replace hand-coded fallback-reason roll-up with a central 
traversal
    
    Comet records fallback reasons in a `TreeNodeTag` side channel. Extended
    explain output only walks plan nodes, so an expression-level reason is
    invisible unless something lifts it onto the enclosing operator. That
    lifting was hand-written at ~200 call sites, and nothing forced it, so
    forgetting the roll-up argument silently produced a plausible-looking
    generic message instead of the real reason (fixed twice before, in #2323
    and #2716).
    
    Steps 1 and 2 of #5230:
    
    Strict mode. `CometExecRule.reportUnexplainedFallback` replaces the
    masking behaviour: when Comet declines an operator whose children are all
    already native, and neither the operator nor any of its expressions
    carries a reason, `spark.comet.explain.fallback.strict.enabled` (internal,
    default off) throws instead of tagging `"<operator> is not supported"`.
    `CometTestBase` enables it, so the whole test corpus now enforces it. The
    check is scoped to operators a serde actually attempted; an operator with
    no registered handler was never attempted and still gets the generic
    message.
    
    Central traversal. `CometExecRule.rollUpFallbackReasons` collects
    `FALLBACK_REASONS` from `op.expressions` and tags them on the operator at
    the single point where Comet decides to keep the Spark operator, mirroring
    the existing `rollUpInfoMessages`. It only runs on the operator that
    failed conversion, which contains the shared-instance problem for
    `AttributeReference`s and DPP subquery expressions. `hasFallbackReason`
    still reads only the node's own tag: it is a planning control signal and
    must not observe the traversal.
    
    With that in place the roll-up parameters are dead, so they are gone from
    the API: `withFallbackReason(node, info)` and
    `withFallbackReasons(node, info)` no longer take varargs, and the
    pure-roll-up overload and `optExprWithFallbackReason` are deleted. The
    compiler now rejects any attempt to reintroduce a hand-rolled roll-up, and
    the old signature's lack of type safety (issue point 4) goes with it. Also
    drops the `var allProjExprs` accumulator in `CometExpandExec`, which
    existed only to feed the roll-up.
    
    Tested: CometExecSuite, CometExpressionSuite, CometAggregateSuite,
    CometJoinSuite, CometWindowExecSuite, CometGenerateExecSuite,
    CometExecRuleSuite, CometScanRuleSuite, CometSparkSessionExtensionsSuite,
    CometFuzzTestSuite, CometFuzzAggregateSuite, CometCastSuite,
    CometArrayExpressionSuite, CometStringExpressionSuite, CometShuffleSuite,
    CometNativeShuffleSuite, CometShuffleFallbackStickinessSuite,
    CometDppFallbackRepro3949Suite, CometCodegenSuite - all pass with strict
    mode on. Compiles clean on spark-3.4, 3.5, 4.0 and 4.1.
    
    * fix: remove unused val left by the roll-up removal in CometIn
    
    scalafix RemoveUnused flagged `val allExprs = list ++ Seq(value)`, which
    only existed to feed the deleted roll-up call.
    
    * fix: lift fallback reasons off the rewritten tree in exprToProto
    
    Strict mode caught a real pre-existing hole. `exprToProto` runs
    `DecimalPrecision.promote`, and `transformUp` rebuilds every node on the
    path to a rewritten one, so for decimal arithmetic the nodes that serde
    actually converts are copies rather than the nodes in the plan. Any reason
    recorded during conversion landed on a copy, where neither extended
    explain nor the operator roll-up could ever see it.
    
    The old hand-written roll-up did not find these either - it read the
    original `projectList`, so the reason was equally lost - which is why this
    only surfaced now: previously the empty tag still rendered as a bare
    `[COMET: ]` and nobody noticed. TPC-DS q9's approved plan records exactly
    that, and is updated here to carry the real reason instead.
    
    Copy the reasons from the rewritten tree onto the original node, the same
    copy-back the `Invoke` / `StaticInvoke` rewrites in `Spark4xCometExprShim`
    already do.
    
    Repro (all Spark versions, [exec] and [expressions] CI shards):
      INSERT INTO t SELECT CAST(id AS decimal(18,4)) + 0.0001 FROM range(20000)
    threw "Comet did not convert Project but recorded no fallback reason".
    
    Also drops an empty `if (r.isEmpty) {}` block left in CometAlias by the
    roll-up removal.
    
    Verified: CometSqlFileTestSuite, the full [expressions] shard (1127 tests)
    and [exec] shard (508 tests) on Spark 4.0, and both TPC-DS plan stability
    suites (129 tests) on Spark 3.4, 3.5, 4.0 and 4.1 - all pass. Plan
    stability needs spark.test.home pointed at the Comet repo root to run.
    
    * test: cover the strict fallback check directly, document its ordering 
dependency
    
    Addresses review feedback on #5236:
    
    - add a test that drives `reportUnexplainedFallback` with the exact shape a
      serde produces when it returns None without recording a reason (a handled
      operator over native children, no tag anywhere), asserting the strict-mode
      throw and the generic message when strict mode is off. No serde in the 
tree
      reaches that state - which is what the check enforces - so the operator is
      constructed by hand and the method is now package-visible.
    - document that `reportUnexplainedFallback` must run after
      `rollUpFallbackReasons`, since it reads only the operator's own tag.
---
 .../main/scala/org/apache/comet/CometConf.scala    |  14 +++
 .../apache/comet/CometSparkSessionExtensions.scala |  62 +++++--------
 .../org/apache/comet/expressions/CometCast.scala   |   1 -
 .../org/apache/comet/rules/CometExecRule.scala     |  86 +++++++++++++++++-
 .../comet/serde/CometBloomFilterMightContain.scala |   2 -
 .../apache/comet/serde/CometScalarFunction.scala   |   4 +-
 .../org/apache/comet/serde/CometSortOrder.scala    |   2 -
 .../org/apache/comet/serde/QueryPlanSerde.scala    |  59 ++++++------
 .../scala/org/apache/comet/serde/aggregates.scala  |  50 +++--------
 .../scala/org/apache/comet/serde/arithmetic.scala  |   8 +-
 .../main/scala/org/apache/comet/serde/arrays.scala |  57 +++++-------
 .../scala/org/apache/comet/serde/bitwise.scala     |   4 +-
 .../scala/org/apache/comet/serde/conditional.scala |   6 --
 .../apache/comet/serde/contraintExpressions.scala  |   8 +-
 .../scala/org/apache/comet/serde/datetime.scala    |  33 +++----
 .../apache/comet/serde/decimalExpressions.scala    |   6 +-
 .../main/scala/org/apache/comet/serde/json.scala   |   4 +-
 .../main/scala/org/apache/comet/serde/maps.scala   |  10 +--
 .../main/scala/org/apache/comet/serde/math.scala   |  26 +++---
 .../org/apache/comet/serde/namedExpressions.scala  |   6 +-
 .../scala/org/apache/comet/serde/predicates.scala  |   6 +-
 .../scala/org/apache/comet/serde/statics.scala     |  11 ++-
 .../scala/org/apache/comet/serde/strings.scala     |  20 ++---
 .../scala/org/apache/comet/serde/structs.scala     |   5 +-
 .../scala/org/apache/comet/serde/unixtime.scala    |   5 +-
 .../main/scala/org/apache/comet/serde/url.scala    |   4 +-
 .../apache/spark/sql/comet/CometWindowExec.scala   |  32 +++----
 .../org/apache/spark/sql/comet/operators.scala     |  34 ++-----
 .../apache/comet/serde/CometToPrettyString.scala   |   2 -
 .../org/apache/comet/shims/CometExprShim.scala     |   8 +-
 .../org/apache/comet/serde/CometMapSort.scala      |   4 +-
 .../apache/comet/serde/CometToPrettyString.scala   |   2 -
 .../org/apache/comet/shims/CometExprShim4x.scala   |   8 +-
 .../approved-plans-v1_4/q9/extended.txt            |   2 +-
 .../org/apache/comet/CometExpressionSuite.scala    |  14 ++-
 .../apache/comet/rules/CometExecRuleSuite.scala    | 100 ++++++++++++++++++++-
 .../scala/org/apache/spark/sql/CometTestBase.scala |   4 +
 37 files changed, 392 insertions(+), 317 deletions(-)

diff --git a/spark/src/main/scala/org/apache/comet/CometConf.scala 
b/spark/src/main/scala/org/apache/comet/CometConf.scala
index e9cf8ca010..1bb524349d 100644
--- a/spark/src/main/scala/org/apache/comet/CometConf.scala
+++ b/spark/src/main/scala/org/apache/comet/CometConf.scala
@@ -668,6 +668,20 @@ object CometConf extends ShimCometConf {
       .booleanConf
       .createWithDefault(false)
 
+  val COMET_STRICT_FALLBACK_REASONS: ConfigEntry[Boolean] =
+    conf("spark.comet.explain.fallback.strict.enabled")
+      .category(CATEGORY_TESTING)
+      .doc(
+        "Test-only. When enabled, Comet throws if it declines to convert an 
operator that it " +
+          "could otherwise have converted (all children are already native) 
without recording a " +
+          "fallback reason on the operator or on any of its expressions. 
Without this check, a " +
+          "serde that returns `None` and forgets to state a reason silently 
produces a generic " +
+          "'<operator> is not supported' message instead of a visible failure. 
Enabled for all " +
+          "Comet test suites via `CometTestBase`.")
+      .internal()
+      .booleanConf
+      .createWithDefault(false)
+
   val COMET_ONHEAP_ENABLED: ConfigEntry[Boolean] =
     conf("spark.comet.exec.onHeap.enabled")
       .category(CATEGORY_TESTING)
diff --git 
a/spark/src/main/scala/org/apache/comet/CometSparkSessionExtensions.scala 
b/spark/src/main/scala/org/apache/comet/CometSparkSessionExtensions.scala
index ca4e1e4e79..3bcb3f98e1 100644
--- a/spark/src/main/scala/org/apache/comet/CometSparkSessionExtensions.scala
+++ b/spark/src/main/scala/org/apache/comet/CometSparkSessionExtensions.scala
@@ -274,41 +274,41 @@ object CometSparkSessionExtensions extends Logging {
    * Record a fallback reason on a `TreeNode` (a Spark operator or expression) 
explaining why
    * Comet cannot accelerate it. Reasons recorded here are surfaced in 
extended explain output
    * (see `ExtendedExplainInfo`) and, when 
`COMET_EXPLAIN_FALLBACK_LOG_ENABLED` is enabled, logged
-   * as warnings. The reasons are also rolled up from child nodes so that the 
operator that
-   * remains in the Spark plan carries the reasons from its converted-away 
subtree.
+   * as warnings.
    *
    * Call this in any code path where Comet decides not to convert a given 
node - serde `convert`
    * methods returning `None`, unsupported data types, disabled configs, etc. 
Do not use this for
    * informational messages that are not fallback reasons: anything tagged 
here is treated by the
    * rules as a signal that the node falls back to Spark.
    *
+   * Tag only the node that actually failed, and state a real reason. There is 
deliberately no way
+   * to copy reasons from child nodes onto a parent: extended explain only 
walks plan nodes, so an
+   * expression-level reason is lifted onto the enclosing operator centrally by
+   * `CometExecRule.rollUpFallbackReasons` when that operator is left in the 
Spark plan. See
+   * https://github.com/apache/datafusion-comet/issues/5230.
+   *
    * @param node
    *   The Spark operator or expression that is falling back to Spark.
    * @param info
-   *   The fallback reason. Optional, may be null or empty - pass empty only 
when the call is used
-   *   purely to roll up reasons from `exprs`.
-   * @param exprs
-   *   Child nodes whose own fallback reasons should be rolled up into `node`. 
Pass the
-   *   sub-expressions or child operators whose failure caused `node` to fall 
back.
+   *   The fallback reason. Newline-delimited to record more than one reason.
    * @tparam T
    *   The type of the TreeNode. Typically `SparkPlan`, `AggregateExpression`, 
or `Expression`.
    * @return
-   *   `node` with fallback reasons attached (as a side effect on its tag map).
+   *   `node` with the fallback reason attached (as a side effect on its tag 
map).
    */
-  def withFallbackReason[T <: TreeNode[_]](node: T, info: String, exprs: T*): 
T = {
+  def withFallbackReason[T <: TreeNode[_]](node: T, info: String): T = {
     // support existing approach of passing in multiple infos in a 
newline-delimited string
     val infoSet = if (info == null || info.isEmpty) {
       Set.empty[String]
     } else {
       info.split("\n").toSet
     }
-    withFallbackReasons(node, infoSet, exprs: _*)
+    withFallbackReasons(node, infoSet)
   }
 
   /**
-   * Record one or more fallback reasons on a `TreeNode` and roll up reasons 
from any child nodes.
-   * This is the set-valued form of [[withFallbackReason]]; see that overload 
for the full
-   * contract.
+   * Record one or more fallback reasons on a `TreeNode`. This is the 
set-valued form of
+   * [[withFallbackReason]]; see that overload for the full contract.
    *
    * Reasons are accumulated (never overwritten) on the node's 
`FALLBACK_REASONS` tag and are
    * surfaced in extended explain output. When 
`COMET_EXPLAIN_FALLBACK_LOG_ENABLED` is enabled,
@@ -317,50 +317,32 @@ object CometSparkSessionExtensions extends Logging {
    * @param node
    *   The Spark operator or expression that is falling back to Spark.
    * @param info
-   *   The fallback reasons for this node. May be empty when the call is used 
purely to roll up
-   *   child reasons.
-   * @param exprs
-   *   Child nodes whose own fallback reasons should be rolled up into `node`.
+   *   The fallback reasons for this node.
    * @tparam T
    *   The type of the TreeNode. Typically `SparkPlan`, `AggregateExpression`, 
or `Expression`.
    * @return
    *   `node` with fallback reasons attached (as a side effect on its tag map).
    */
-  def withFallbackReasons[T <: TreeNode[_]](node: T, info: Set[String], exprs: 
T*): T = {
+  def withFallbackReasons[T <: TreeNode[_]](node: T, info: Set[String]): T = {
     if (CometConf.COMET_EXPLAIN_FALLBACK_LOG_ENABLED.get()) {
       for (reason <- info) {
         logWarning(s"Comet cannot accelerate ${node.getClass.getSimpleName} 
because: $reason")
       }
     }
-    val existingNodeInfos = node.getTagValue(CometExplainInfo.FALLBACK_REASONS)
-    val newNodeInfo = (existingNodeInfos ++ exprs
-      .flatMap(_.getTagValue(CometExplainInfo.FALLBACK_REASONS))).flatten.toSet
-    node.setTagValue(CometExplainInfo.FALLBACK_REASONS, newNodeInfo ++ info)
+    val existingNodeInfos =
+      
node.getTagValue(CometExplainInfo.FALLBACK_REASONS).getOrElse(Set.empty[String])
+    node.setTagValue(CometExplainInfo.FALLBACK_REASONS, existingNodeInfos ++ 
info)
     node
   }
 
-  /**
-   * Roll up fallback reasons from `exprs` onto `node` without adding a new 
reason of its own. Use
-   * this when a parent operator is itself falling back and wants to preserve 
the reasons recorded
-   * on its child expressions/operators so they appear together in explain 
output.
-   *
-   * @param node
-   *   The parent operator or expression falling back to Spark.
-   * @param exprs
-   *   Child nodes whose fallback reasons should be aggregated onto `node`.
-   * @tparam T
-   *   The type of the TreeNode. Typically `SparkPlan`, `AggregateExpression`, 
or `Expression`.
-   * @return
-   *   `node` with the rolled-up reasons attached (as a side effect on its tag 
map).
-   */
-  def withFallbackReason[T <: TreeNode[_]](node: T, exprs: T*): T = {
-    withFallbackReasons(node, Set.empty, exprs: _*)
-  }
-
   /**
    * True if any fallback reason has been recorded on `node` (via 
[[withFallbackReason]] /
    * [[withFallbackReasons]]). Callers that need to short-circuit when a prior 
rule pass has
    * already decided a node falls back can use this as the sticky signal.
+   *
+   * This deliberately reads only the node's own tag. It is a planning control 
signal, not explain
+   * output, so it must not observe reasons that merely exist somewhere in the 
node's expression
+   * trees - see `CometExecRule.rollUpFallbackReasons`.
    */
   def hasFallbackReason(node: TreeNode[_]): Boolean = {
     node.getTagValue(CometExplainInfo.FALLBACK_REASONS).exists(_.nonEmpty)
diff --git a/spark/src/main/scala/org/apache/comet/expressions/CometCast.scala 
b/spark/src/main/scala/org/apache/comet/expressions/CometCast.scala
index 78abf54c8d..df4140078e 100644
--- a/spark/src/main/scala/org/apache/comet/expressions/CometCast.scala
+++ b/spark/src/main/scala/org/apache/comet/expressions/CometCast.scala
@@ -118,7 +118,6 @@ object CometCast
           if (childExpr.isDefined) {
             castToProto(cast, cast.timeZoneId, cast.dataType, childExpr.get, 
cometEvalMode)
           } else {
-            withFallbackReason(cast, cast.child)
             None
           }
         }
diff --git a/spark/src/main/scala/org/apache/comet/rules/CometExecRule.scala 
b/spark/src/main/scala/org/apache/comet/rules/CometExecRule.scala
index a709129bac..ef2f37371c 100644
--- a/spark/src/main/scala/org/apache/comet/rules/CometExecRule.scala
+++ b/spark/src/main/scala/org/apache/comet/rules/CometExecRule.scala
@@ -382,7 +382,11 @@ case class CometExecRule(session: SparkSession)
             // when COMET_EXPLAIN_FALLBACK_LOG_ENABLED=true) even when the 
write is fully native.
             op
           case _ =>
-            // The operator was not converted to a Comet plan. Possible 
reasons for this happening:
+            // The operator was not converted to a Comet plan and no serde 
handler claimed it, so
+            // Comet simply has no support for it. (Operators that do have a 
handler are reported
+            // by `reportUnexplainedFallback` inside `convertToComet`, which 
is also where the
+            // strict check lives - it would be wrong to demand a specific 
reason here, because
+            // nothing ever attempted this operator.) Possible reasons for 
reaching this point:
             // 1. Comet does not support this operator.
             // 2. The operator could not be supported based on query context 
and current
             //    configs. In this case, it should have already been tagged 
with fallback
@@ -698,6 +702,23 @@ case class CometExecRule(session: SparkSession)
 
   /** Convert a Spark plan to a Comet plan using the specified serde handler */
   private def convertToComet(op: SparkPlan, handler: CometOperatorSerde[_]): 
Option[SparkPlan] = {
+    val converted = tryConvertToComet(op, handler)
+    if (converted.isEmpty) {
+      // Comet looked at this operator and declined it, so it stays in the 
Spark plan. Lift any
+      // reasons recorded on its expressions onto the operator itself - see
+      // `rollUpFallbackReasons` for why this is needed - and then make sure 
something was
+      // recorded. The order is required, not incidental: 
`reportUnexplainedFallback` inspects only
+      // the operator's own tag, so a reason still sitting on an expression 
would look like no
+      // reason at all and trip the strict check.
+      rollUpFallbackReasons(op)
+      reportUnexplainedFallback(op)
+    }
+    converted
+  }
+
+  private def tryConvertToComet(
+      op: SparkPlan,
+      handler: CometOperatorSerde[_]): Option[SparkPlan] = {
     val serde = handler.asInstanceOf[CometOperatorSerde[SparkPlan]]
     if (isOperatorEnabled(serde, op)) {
       // For operators that require native children (like writes), check if 
all data-producing
@@ -741,6 +762,69 @@ case class CometExecRule(session: SparkSession)
     None
   }
 
+  /**
+   * Lift fallback reasons recorded on `op`'s expression trees onto `op` 
itself.
+   *
+   * Extended explain output only walks plan nodes 
(`ExtendedExplainInfo.sortup` follows
+   * `children` / `innerChildren`, never `expressions`), so a reason tagged on 
an expression is
+   * invisible unless something lifts it onto the enclosing operator. This 
mirrors what
+   * [[rollUpInfoMessages]] already does for the informational tags, and 
replaces the roll-up that
+   * used to be hand-written at every serde call site (see
+   * https://github.com/apache/datafusion-comet/issues/5230).
+   *
+   * Only child *expressions* are collected, not child operators: reasons on a 
child operator are
+   * already reachable by the explain traversal via `children`.
+   *
+   * Called only when `op` was left in the Spark plan, which scopes the 
roll-up to the operator
+   * that actually failed conversion. That matters because some expression 
instances
+   * (`AttributeReference`s, DPP subquery expressions) are shared across 
operators, so an unscoped
+   * roll-up could surface one expression's reason under several unrelated 
operators.
+   *
+   * [[reportUnexplainedFallback]] relies on this having run first; the two 
must not be separated.
+   */
+  private def rollUpFallbackReasons(op: SparkPlan): Unit = {
+    val reasons = op.expressions
+      .flatMap(_.collect { case e: Expression => e })
+      .flatMap(_.getTagValue(CometExplainInfo.FALLBACK_REASONS))
+      .flatten
+      .toSet
+    if (reasons.nonEmpty) {
+      withFallbackReasons(op, reasons)
+    }
+  }
+
+  /**
+   * Handle an operator that Comet declined without stating why.
+   *
+   * When every child is already native, Comet had a real opportunity to 
convert `op`, so the
+   * absence of any reason - on `op` or anywhere in its expression trees - 
means a serde returned
+   * `None` and forgot to record one. Under `COMET_STRICT_FALLBACK_REASONS` 
(enabled for Comet's
+   * own test suites) that is a hard failure; otherwise fall back to a generic 
message so users
+   * still see something. The generic message is what used to mask this whole 
class of bug, which
+   * is why the strict check exists.
+   *
+   * Must run *after* [[rollUpFallbackReasons]] for the same operator. The 
check reads only `op`'s
+   * own tag, because `hasFallbackReason` deliberately does not traverse 
expressions (it is a
+   * planning control signal, not explain output), so an expression-level 
reason that has not been
+   * lifted yet would be mistaken for no reason at all. [[convertToComet]] is 
the only production
+   * caller and keeps the two calls together.
+   *
+   * Package-visible so `CometExecRuleSuite` can drive the strict failure 
directly: no serde in
+   * the tree reaches this state, which is exactly what the check enforces, so 
the only way to
+   * test it is to construct the shape by hand.
+   */
+  private[comet] def reportUnexplainedFallback(op: SparkPlan): Unit = {
+    if (op.children.forall(_.isInstanceOf[CometNativeExec]) && 
!hasFallbackReason(op)) {
+      if (CometConf.COMET_STRICT_FALLBACK_REASONS.get(op.conf)) {
+        throw new IllegalStateException(
+          s"Comet did not convert ${op.nodeName} but recorded no fallback 
reason on the " +
+            "operator or any of its expressions. Add a withFallbackReason call 
stating why " +
+            s"conversion failed. Operator:\n$op")
+      }
+      withFallbackReason(op, s"${op.nodeName} is not supported")
+    }
+  }
+
   /**
    * Lift informational (non-fallback) messages tagged on an operator and its 
expressions onto the
    * converted Comet plan node so they appear in verbose extended explain 
output. Expression-level
diff --git 
a/spark/src/main/scala/org/apache/comet/serde/CometBloomFilterMightContain.scala
 
b/spark/src/main/scala/org/apache/comet/serde/CometBloomFilterMightContain.scala
index 75e0f532e3..9620f2d8fc 100644
--- 
a/spark/src/main/scala/org/apache/comet/serde/CometBloomFilterMightContain.scala
+++ 
b/spark/src/main/scala/org/apache/comet/serde/CometBloomFilterMightContain.scala
@@ -21,7 +21,6 @@ package org.apache.comet.serde
 
 import org.apache.spark.sql.catalyst.expressions.{Attribute, 
BloomFilterMightContain}
 
-import org.apache.comet.CometSparkSessionExtensions.withFallbackReason
 import org.apache.comet.serde.QueryPlanSerde.exprToProtoInternal
 
 object CometBloomFilterMightContain extends 
CometExpressionSerde[BloomFilterMightContain] {
@@ -45,7 +44,6 @@ object CometBloomFilterMightContain extends 
CometExpressionSerde[BloomFilterMigh
           .setBloomFilterMightContain(builder)
           .build())
     } else {
-      withFallbackReason(expr, bloomFilter, value)
       None
     }
   }
diff --git 
a/spark/src/main/scala/org/apache/comet/serde/CometScalarFunction.scala 
b/spark/src/main/scala/org/apache/comet/serde/CometScalarFunction.scala
index 2e056ae886..e5f14f8b01 100644
--- a/spark/src/main/scala/org/apache/comet/serde/CometScalarFunction.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/CometScalarFunction.scala
@@ -22,13 +22,13 @@ package org.apache.comet.serde
 import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression}
 
 import org.apache.comet.serde.ExprOuterClass.Expr
-import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
optExprWithFallbackReason, scalarFunctionExprToProto}
+import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
scalarFunctionExprToProto}
 
 /** Serde for scalar function. */
 case class CometScalarFunction[T <: Expression](name: String) extends 
CometExpressionSerde[T] {
   override def convert(expr: T, inputs: Seq[Attribute], binding: Boolean): 
Option[Expr] = {
     val childExpr = expr.children.map(exprToProtoInternal(_, inputs, binding))
     val optExpr = scalarFunctionExprToProto(name, childExpr: _*)
-    optExprWithFallbackReason(optExpr, expr, expr.children: _*)
+    optExpr
   }
 }
diff --git a/spark/src/main/scala/org/apache/comet/serde/CometSortOrder.scala 
b/spark/src/main/scala/org/apache/comet/serde/CometSortOrder.scala
index 346bb454eb..838a229bb9 100644
--- a/spark/src/main/scala/org/apache/comet/serde/CometSortOrder.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/CometSortOrder.scala
@@ -22,7 +22,6 @@ package org.apache.comet.serde
 import org.apache.spark.sql.catalyst.expressions.{Ascending, Attribute, 
Descending, NullsFirst, NullsLast, SortOrder}
 
 import org.apache.comet.CometConf
-import org.apache.comet.CometSparkSessionExtensions.withFallbackReason
 import org.apache.comet.serde.QueryPlanSerde.exprToProtoInternal
 
 object CometSortOrder extends CometExpressionSerde[SortOrder] {
@@ -66,7 +65,6 @@ object CometSortOrder extends CometExpressionSerde[SortOrder] 
{
           .setSortOrder(sortOrderBuilder)
           .build())
     } else {
-      withFallbackReason(expr, expr.child)
       None
     }
   }
diff --git a/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala 
b/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala
index 9e867aad2d..cad5da6746 100644
--- a/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala
@@ -38,7 +38,7 @@ import org.apache.spark.sql.types._
 
 import org.apache.comet.CometConf
 import org.apache.comet.CometExplainInfo
-import org.apache.comet.CometSparkSessionExtensions.{appendTagValues, 
withFallbackReason, withInfo, withNativeExpr}
+import org.apache.comet.CometSparkSessionExtensions.{appendTagValues, 
withFallbackReason, withFallbackReasons, withInfo, withNativeExpr}
 import org.apache.comet.expressions._
 import org.apache.comet.parquet.CometParquetUtils
 import org.apache.comet.serde.ExprOuterClass.{AggExpr, Expr, ScalarFunc}
@@ -725,10 +725,7 @@ object QueryPlanSerde extends Logging with CometExprShim 
with CometTypeShim {
             aggHandler.convert(aggExpr, fn, inputs, binding, conf)
         }
       case _ =>
-        withFallbackReason(
-          aggExpr,
-          s"unsupported Spark aggregate function: ${fn.prettyName}",
-          fn.children: _*)
+        withFallbackReason(aggExpr, s"unsupported Spark aggregate function: 
${fn.prettyName}")
         None
     }
 
@@ -745,7 +742,6 @@ object QueryPlanSerde extends Logging with CometExprShim 
with CometTypeShim {
       if (aggExpr.filter.isDefined && aggExpr.mode == Partial) {
         val filterProto = exprToProto(aggExpr.filter.get, inputs, binding)
         if (filterProto.isEmpty) {
-          withFallbackReason(aggExpr, aggExpr.filter.get)
           return None
         }
         builder.setFilter(filterProto.get)
@@ -793,11 +789,14 @@ object QueryPlanSerde extends Logging with CometExprShim 
with CometTypeShim {
     val newExpr = DecimalPrecision.promote(expr)
     val result = exprToProtoInternal(newExpr, inputs, binding)
     if (!(newExpr eq expr)) {
-      // `promote` rebuilt the tree, so the coverage tags landed on copies 
that the operator does
-      // not hold. Lift them onto `expr` so 
`CometExecRule.rollUpInfoMessages`, which walks the
-      // operator's own expressions, still sees them. Skipped in the common 
case where `promote`
-      // returned the same tree and there is nothing to lift.
+      // `promote` rebuilt the tree, so the tags landed on copies that the 
operator does not hold.
+      // Lift them onto `expr` so the roll-ups in `CometExecRule`, which walk 
the operator's own
+      // expressions, still see them. Skipped in the common case where 
`promote` returned the same
+      // tree and there is nothing to lift.
       liftCoverageTags(newExpr, expr)
+      if (result.isEmpty) {
+        liftFallbackReasons(newExpr, expr)
+      }
     }
     result
   }
@@ -813,6 +812,27 @@ object QueryPlanSerde extends Logging with CometExprShim 
with CometTypeShim {
     appendTagValues(to, CometExplainInfo.CODEGEN_DISPATCH_EXPRS, 
dispatched.toSet)
   }
 
+  /**
+   * Lift fallback reasons recorded anywhere in the rewritten tree onto the 
original node, so that
+   * `CometExecRule.rollUpFallbackReasons` and extended explain can see them. 
Without this the
+   * reason is attached to a copy that is not in the plan and is lost entirely 
- see
+   * https://github.com/apache/datafusion-comet/issues/5230. Same copy-back 
that the `Invoke` /
+   * `StaticInvoke` rewrites in `Spark4xCometExprShim` do.
+   *
+   * Only called when conversion failed: a fallback reason states why an 
expression could not be
+   * converted, so lifting one off a tree that converted fine would attribute 
a stale reason to an
+   * operator that has no problem.
+   */
+  private def liftFallbackReasons(from: Expression, to: Expression): Unit = {
+    val reasons = mutable.Set.empty[String]
+    from.foreach { e =>
+      e.getTagValue(CometExplainInfo.FALLBACK_REASONS).foreach(reasons ++= _)
+    }
+    if (reasons.nonEmpty) {
+      withFallbackReasons(to, reasons.toSet)
+    }
+  }
+
   /**
    * Convert a Spark expression to a protocol-buffer representation of a 
native Comet/DataFusion
    * expression.
@@ -917,7 +937,7 @@ object QueryPlanSerde extends Logging with CometExprShim 
with CometTypeShim {
             case Some(handler) =>
               convert(expr, 
handler.asInstanceOf[CometExpressionSerde[Expression]])
             case _ =>
-              withFallbackReason(expr, s"${expr.prettyName} is not supported", 
expr.children: _*)
+              withFallbackReason(expr, s"${expr.prettyName} is not supported")
               None
           }
       })
@@ -986,7 +1006,6 @@ object QueryPlanSerde extends Logging with CometExprShim 
with CometTypeShim {
             .newBuilder(),
           inner).build())
     } else {
-      withFallbackReason(expr, child)
       None
     }
   }
@@ -1016,7 +1035,6 @@ object QueryPlanSerde extends Logging with CometExprShim 
with CometTypeShim {
             .newBuilder(),
           inner).build())
     } else {
-      withFallbackReason(expr, left, right)
       None
     }
   }
@@ -1045,7 +1063,6 @@ object QueryPlanSerde extends Logging with CometExprShim 
with CometTypeShim {
       : Option[ExprOuterClass.Expr] = {
     val protos = operands.map(exprToProtoInternal(_, inputs, binding))
     if (protos.exists(_.isEmpty)) {
-      withFallbackReason(expr, operands: _*)
       None
     } else {
       val leaves = protos.map(_.get).toIndexedSeq
@@ -1128,20 +1145,6 @@ object QueryPlanSerde extends Logging with CometExprShim 
with CometTypeShim {
     Some(ExprOuterClass.Expr.newBuilder().setScalarFunc(builder).build())
   }
 
-  // Utility method. Adds fallback reason if the result of calling exprToProto 
is None
-  def optExprWithFallbackReason(
-      optExpr: Option[Expr],
-      expr: Expression,
-      childExpr: Expression*): Option[Expr] = {
-    optExpr match {
-      case None =>
-        withFallbackReason(expr, childExpr: _*)
-        None
-      case o => o
-    }
-
-  }
-
   /**
    * If `handler` is a `CodegenDispatchFallback`, run `expr` through the JVM 
codegen dispatcher
    * and return `Some((handler, proto))` on success; otherwise return `None`. 
Shared by the
diff --git a/spark/src/main/scala/org/apache/comet/serde/aggregates.scala 
b/spark/src/main/scala/org/apache/comet/serde/aggregates.scala
index 316744be1f..db435e5b5d 100644
--- a/spark/src/main/scala/org/apache/comet/serde/aggregates.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/aggregates.scala
@@ -61,10 +61,9 @@ object CometMin extends CometAggregateExpressionSerde[Min] {
           .setMin(builder)
           .build())
     } else if (dataType.isEmpty) {
-      withFallbackReason(aggExpr, s"datatype ${expr.dataType} is not 
supported", child)
+      withFallbackReason(aggExpr, s"datatype ${expr.dataType} is not 
supported")
       None
     } else {
-      withFallbackReason(aggExpr, child)
       None
     }
   }
@@ -98,10 +97,9 @@ object CometMax extends CometAggregateExpressionSerde[Max] {
           .setMax(builder)
           .build())
     } else if (dataType.isEmpty) {
-      withFallbackReason(aggExpr, s"datatype ${expr.dataType} is not 
supported", child)
+      withFallbackReason(aggExpr, s"datatype ${expr.dataType} is not 
supported")
       None
     } else {
-      withFallbackReason(aggExpr, child)
       None
     }
   }
@@ -124,7 +122,6 @@ object CometCount extends 
CometAggregateExpressionSerde[Count] {
           .setCount(builder)
           .build())
     } else {
-      withFallbackReason(aggExpr, expr.children: _*)
       None
     }
   }
@@ -182,10 +179,9 @@ object CometAverage extends 
CometAggregateExpressionSerde[Average] {
           .setAvg(builder)
           .build())
     } else if (dataType.isEmpty) {
-      withFallbackReason(aggExpr, s"datatype ${avg.dataType} is not 
supported", child)
+      withFallbackReason(aggExpr, s"datatype ${avg.dataType} is not supported")
       None
     } else {
-      withFallbackReason(aggExpr, child)
       None
     }
   }
@@ -232,9 +228,7 @@ object CometSum extends CometAggregateExpressionSerde[Sum] {
           .build())
     } else {
       if (dataType.isEmpty) {
-        withFallbackReason(aggExpr, s"datatype ${sum.dataType} is not 
supported", sum.child)
-      } else {
-        withFallbackReason(aggExpr, sum.child)
+        withFallbackReason(aggExpr, s"datatype ${sum.dataType} is not 
supported")
       }
       None
     }
@@ -268,10 +262,9 @@ object CometFirst extends 
CometAggregateExpressionSerde[First] {
           .setFirst(builder)
           .build())
     } else if (dataType.isEmpty) {
-      withFallbackReason(aggExpr, s"datatype ${first.dataType} is not 
supported", child)
+      withFallbackReason(aggExpr, s"datatype ${first.dataType} is not 
supported")
       None
     } else {
-      withFallbackReason(aggExpr, child)
       None
     }
   }
@@ -304,10 +297,9 @@ object CometLast extends 
CometAggregateExpressionSerde[Last] {
           .setLast(builder)
           .build())
     } else if (dataType.isEmpty) {
-      withFallbackReason(aggExpr, s"datatype ${last.dataType} is not 
supported", child)
+      withFallbackReason(aggExpr, s"datatype ${last.dataType} is not 
supported")
       None
     } else {
-      withFallbackReason(aggExpr, child)
       None
     }
   }
@@ -343,10 +335,9 @@ object CometBitAndAgg extends 
CometAggregateExpressionSerde[BitAndAgg] {
           .setBitAndAgg(builder)
           .build())
     } else if (dataType.isEmpty) {
-      withFallbackReason(aggExpr, s"datatype ${bitAnd.dataType} is not 
supported", child)
+      withFallbackReason(aggExpr, s"datatype ${bitAnd.dataType} is not 
supported")
       None
     } else {
-      withFallbackReason(aggExpr, child)
       None
     }
   }
@@ -382,10 +373,9 @@ object CometBitOrAgg extends 
CometAggregateExpressionSerde[BitOrAgg] {
           .setBitOrAgg(builder)
           .build())
     } else if (dataType.isEmpty) {
-      withFallbackReason(aggExpr, s"datatype ${bitOr.dataType} is not 
supported", child)
+      withFallbackReason(aggExpr, s"datatype ${bitOr.dataType} is not 
supported")
       None
     } else {
-      withFallbackReason(aggExpr, child)
       None
     }
   }
@@ -421,10 +411,9 @@ object CometBitXOrAgg extends 
CometAggregateExpressionSerde[BitXorAgg] {
           .setBitXorAgg(builder)
           .build())
     } else if (dataType.isEmpty) {
-      withFallbackReason(aggExpr, s"datatype ${bitXor.dataType} is not 
supported", child)
+      withFallbackReason(aggExpr, s"datatype ${bitXor.dataType} is not 
supported")
       None
     } else {
-      withFallbackReason(aggExpr, child)
       None
     }
   }
@@ -523,7 +512,6 @@ trait CometVariance {
           .setVariance(builder)
           .build())
     } else {
-      withFallbackReason(aggExpr, expr.child)
       None
     }
   }
@@ -578,7 +566,6 @@ trait CometStddev {
           .setStddev(builder)
           .build())
     } else {
-      withFallbackReason(aggExpr, child)
       None
     }
   }
@@ -671,7 +658,6 @@ object CometPercentile extends 
CometAggregateExpressionSerde[Percentile] {
           .setPercentile(builder)
           .build())
     } else {
-      withFallbackReason(aggExpr, percentile.child)
       None
     }
   }
@@ -719,14 +705,14 @@ object CometApproxPercentile extends 
CometAggregateExpressionSerde[ApproximatePe
       case d: Double => (Seq(d), false)
       case arr: ArrayData => (arr.toDoubleArray().toSeq, true)
       case other =>
-        withFallbackReason(aggExpr, s"Unsupported percentage literal: $other", 
expr.child)
+        withFallbackReason(aggExpr, s"Unsupported percentage literal: $other")
         return None
     }
     val accuracy = expr.accuracyExpression.eval() match {
       case i: Int => i.toLong
       case l: Long => l
       case other =>
-        withFallbackReason(aggExpr, s"Unsupported accuracy literal: $other", 
expr.child)
+        withFallbackReason(aggExpr, s"Unsupported accuracy literal: $other")
         return None
     }
 
@@ -743,7 +729,6 @@ object CometApproxPercentile extends 
CometAggregateExpressionSerde[ApproximatePe
           .setApproxPercentile(builder)
           .build())
     } else {
-      withFallbackReason(aggExpr, expr.child)
       None
     }
   }
@@ -773,7 +758,6 @@ object CometCorr extends 
CometAggregateExpressionSerde[Corr] {
           .setCorrelation(builder)
           .build())
     } else {
-      withFallbackReason(aggExpr, corr.x, corr.y)
       None
     }
   }
@@ -839,11 +823,6 @@ object CometBloomFilterAggregate extends 
CometAggregateExpressionSerde[BloomFilt
           .setBloomFilterAgg(builder)
           .build())
     } else {
-      withFallbackReason(
-        aggExpr,
-        bloomFilter.child,
-        bloomFilter.estimatedNumItemsExpression,
-        bloomFilter.numBitsExpression)
       None
     }
   }
@@ -897,10 +876,9 @@ object CometCollectSet extends 
CometAggregateExpressionSerde[CollectSet] {
           .setCollectSet(builder)
           .build())
     } else if (dataType.isEmpty) {
-      withFallbackReason(aggExpr, s"datatype ${expr.dataType} is not 
supported", child)
+      withFallbackReason(aggExpr, s"datatype ${expr.dataType} is not 
supported")
       None
     } else {
-      withFallbackReason(aggExpr, child)
       None
     }
   }
@@ -943,10 +921,9 @@ object CometCollectList extends 
CometAggregateExpressionSerde[CollectList] {
           .setCollectList(builder)
           .build())
     } else if (dataType.isEmpty) {
-      withFallbackReason(aggExpr, s"datatype ${expr.dataType} is not 
supported", child)
+      withFallbackReason(aggExpr, s"datatype ${expr.dataType} is not 
supported")
       None
     } else {
-      withFallbackReason(aggExpr, child)
       None
     }
   }
@@ -1009,7 +986,6 @@ object CometApproxCountDistinct extends 
CometAggregateExpressionSerde[HyperLogLo
           .setHllpp(builder)
           .build())
     } else {
-      withFallbackReason(aggExpr, expr.child)
       None
     }
   }
diff --git a/spark/src/main/scala/org/apache/comet/serde/arithmetic.scala 
b/spark/src/main/scala/org/apache/comet/serde/arithmetic.scala
index 8023231b35..058ff2ea5f 100644
--- a/spark/src/main/scala/org/apache/comet/serde/arithmetic.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/arithmetic.scala
@@ -24,9 +24,8 @@ import scala.math.min
 import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, Cast, 
Divide, EmptyRow, EqualTo, EvalMode, Expression, If, IntegralDivide, Literal, 
Multiply, Remainder, Round, Subtract, UnaryMinus}
 import org.apache.spark.sql.types.{ByteType, DataType, DecimalType, 
DoubleType, FloatType, IntegerType, LongType, ShortType}
 
-import org.apache.comet.CometSparkSessionExtensions.withFallbackReason
 import org.apache.comet.expressions.{CometCast, CometEvalMode}
-import org.apache.comet.serde.QueryPlanSerde.{evalModeToProto, 
exprToProtoInternal, flattenAssociative, optExprWithFallbackReason, 
scalarFunctionExprToProtoWithReturnType, serializeDataType}
+import org.apache.comet.serde.QueryPlanSerde.{evalModeToProto, 
exprToProtoInternal, flattenAssociative, 
scalarFunctionExprToProtoWithReturnType, serializeDataType}
 import org.apache.comet.shims.CometEvalModeUtil
 
 trait MathBase {
@@ -62,7 +61,6 @@ trait MathBase {
             .newBuilder(),
           inner).build())
     } else {
-      withFallbackReason(expr, left, right)
       None
     }
   }
@@ -127,7 +125,6 @@ trait MathBase {
       : Option[ExprOuterClass.Expr] = {
     val protos = operands.map(exprToProtoInternal(_, inputs, binding))
     if (protos.exists(_.isEmpty)) {
-      withFallbackReason(expr, operands: _*)
       None
     } else {
       val returnType = serializeDataType(dataType)
@@ -426,7 +423,7 @@ object CometRound extends CometExpressionSerde[Round] {
             r.ansiEnabled,
             childExpr,
             scaleExpr)
-        optExprWithFallbackReason(optExpr, r, r.child)
+        optExpr
     }
 
   }
@@ -451,7 +448,6 @@ object CometUnaryMinus extends 
CometExpressionSerde[UnaryMinus] with MathBase {
           .setUnaryMinus(builder)
           .build())
     } else {
-      withFallbackReason(expr, expr.child)
       None
     }
   }
diff --git a/spark/src/main/scala/org/apache/comet/serde/arrays.scala 
b/spark/src/main/scala/org/apache/comet/serde/arrays.scala
index dfaecd3786..748b1cee23 100644
--- a/spark/src/main/scala/org/apache/comet/serde/arrays.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/arrays.scala
@@ -95,7 +95,6 @@ object CometArrayAppend extends 
CometExpressionSerde[ArrayAppend] {
           .setCaseWhen(caseWhenExpr)
           .build())
     } else {
-      withFallbackReason(expr, expr.children: _*)
       None
     }
   }
@@ -171,7 +170,7 @@ object CometSortArray extends 
CometExpressionSerde[SortArray] with CodegenDispat
         arrayExprProto,
         sortDirectionExprProto,
         nullOrderingExprProto)
-    optExprWithFallbackReason(sortArrayScalarExpr, expr, expr.children: _*)
+    sortArrayScalarExpr
   }
 }
 
@@ -212,7 +211,7 @@ object CometArrayIntersect
 
     val arraysIntersectScalarExpr =
       scalarFunctionExprToProto("array_intersect", leftArrayExprProto, 
rightArrayExprProto)
-    optExprWithFallbackReason(arraysIntersectScalarExpr, expr, expr.children: 
_*)
+    arraysIntersectScalarExpr
   }
 }
 
@@ -225,7 +224,7 @@ object CometArrayMax extends CometExpressionSerde[ArrayMax] 
{
 
     val arrayMaxScalarExpr =
       scalarFunctionExprToProto("array_max", arrayExprProto)
-    optExprWithFallbackReason(arrayMaxScalarExpr, expr)
+    arrayMaxScalarExpr
   }
 }
 
@@ -237,7 +236,7 @@ object CometArrayMin extends CometExpressionSerde[ArrayMin] 
{
     val arrayExprProto = exprToProto(expr.children.head, inputs, binding)
 
     val arrayMinScalarExpr = scalarFunctionExprToProto("array_min", 
arrayExprProto)
-    optExprWithFallbackReason(arrayMinScalarExpr, expr)
+    arrayMinScalarExpr
   }
 }
 
@@ -255,7 +254,7 @@ object CometArraysOverlap extends 
CometExpressionSerde[ArraysOverlap] {
       false,
       leftArrayExprProto,
       rightArrayExprProto)
-    optExprWithFallbackReason(arraysOverlapScalarExpr, expr, expr.children: _*)
+    arraysOverlapScalarExpr
   }
 }
 
@@ -269,7 +268,7 @@ object CometArrayCompact extends 
CometExpressionSerde[Expression] {
     val arrayExprProto = exprToProto(child, inputs, binding)
 
     val arrayCompactScalarExpr = scalarFunctionExprToProto("array_compact", 
arrayExprProto)
-    optExprWithFallbackReason(arrayCompactScalarExpr, expr, expr.children: _*)
+    arrayCompactScalarExpr
   }
 }
 
@@ -327,7 +326,7 @@ object CometArrayExcept
 
     val arrayExceptScalarExpr =
       scalarFunctionExprToProto("array_except", leftArrayExprProto, 
rightArrayExprProto)
-    optExprWithFallbackReason(arrayExceptScalarExpr, expr, expr.children: _*)
+    arrayExceptScalarExpr
   }
 }
 
@@ -375,17 +374,12 @@ object CometArrayJoin
           delimiterExprProto,
           nullReplacementExprProto)
 
-        optExprWithFallbackReason(
-          arrayJoinScalarExpr,
-          expr,
-          arrayExpr,
-          arrayExpr.delimiter,
-          nullReplacementExpr)
+        arrayJoinScalarExpr
       case None =>
         val arrayJoinScalarExpr =
           scalarFunctionExprToProto("array_to_string", arrayExprProto, 
delimiterExprProto)
 
-        optExprWithFallbackReason(arrayJoinScalarExpr, expr, arrayExpr, 
arrayExpr.delimiter)
+        arrayJoinScalarExpr
     }
   }
 }
@@ -417,12 +411,7 @@ object CometArrayInsert extends 
CometExpressionSerde[ArrayInsert] {
           .setArrayInsert(arrayInsertBuilder)
           .build())
     } else {
-      withFallbackReason(
-        expr,
-        "unsupported arguments for ArrayInsert",
-        expr.children.head,
-        expr.children(1),
-        expr.children(2))
+      withFallbackReason(expr, "unsupported arguments for ArrayInsert")
       None
     }
   }
@@ -448,7 +437,7 @@ object CometSlice extends CometExpressionSerde[Slice] {
         arrayExprProto,
         startExprProto,
         lengthExprProto)
-    optExprWithFallbackReason(sliceScalarExpr, expr, expr.children: _*)
+    sliceScalarExpr
   }
 }
 
@@ -462,7 +451,7 @@ object CometArrayUnion extends 
CometExpressionSerde[ArrayUnion] {
 
     val arraysUnionScalarExpr =
       scalarFunctionExprToProto("array_union", leftArrayExprProto, 
rightArrayExprProto)
-    optExprWithFallbackReason(arraysUnionScalarExpr, expr, expr.children: _*)
+    arraysUnionScalarExpr
   }
 }
 
@@ -498,8 +487,7 @@ object CometCreateArray extends 
CometExpressionSerde[CreateArray] {
       withFallbackReason(
         expr,
         "CreateArray children have mismatched data types: " +
-          children.map(_.dataType).distinct.mkString(", "),
-        children: _*)
+          children.map(_.dataType).distinct.mkString(", "))
       return None
     }
 
@@ -508,7 +496,7 @@ object CometCreateArray extends 
CometExpressionSerde[CreateArray] {
     if (childExprs.forall(_.isDefined)) {
       scalarFunctionExprToProto("make_array", childExprs: _*)
     } else {
-      withFallbackReason(expr, "unsupported arguments for CreateArray", 
children: _*)
+      withFallbackReason(expr, "unsupported arguments for CreateArray")
       None
     }
   }
@@ -557,7 +545,7 @@ object CometGetArrayItem extends 
CometExpressionSerde[GetArrayItem] {
           .setListExtract(listExtractBuilder)
           .build())
     } else {
-      withFallbackReason(expr, "unsupported arguments for GetArrayItem", 
expr.child, expr.ordinal)
+      withFallbackReason(expr, "unsupported arguments for GetArrayItem")
       None
     }
   }
@@ -595,7 +583,7 @@ object CometArrayReverse extends 
CometExpressionSerde[Reverse] with ArraysBase {
     }
     val reverseExprProto = exprToProto(expr.child, inputs, binding)
     val reverseScalarExpr = scalarFunctionExprToProto("array_reverse", 
reverseExprProto)
-    optExprWithFallbackReason(reverseScalarExpr, expr, expr.children: _*)
+    reverseScalarExpr
   }
 
 }
@@ -620,7 +608,7 @@ object CometElementAt extends 
CometExpressionSerde[ElementAt] {
     expr.left.dataType match {
       case _: MapType =>
         val mapExtractExpr = scalarFunctionExprToProto("map_extract", 
childExpr, ordinalExpr)
-        optExprWithFallbackReason(mapExtractExpr, expr, expr.left, expr.right)
+        mapExtractExpr
       case _ =>
         val defaultExpr =
           expr.defaultValueOutOfBound.flatMap(exprToProtoInternal(_, inputs, 
binding))
@@ -642,7 +630,7 @@ object CometElementAt extends 
CometExpressionSerde[ElementAt] {
               .setListExtract(arrayExtractBuilder)
               .build())
         } else {
-          withFallbackReason(expr, "unsupported arguments for ElementAt", 
expr.left, expr.right)
+          withFallbackReason(expr, "unsupported arguments for ElementAt")
           None
         }
     }
@@ -659,7 +647,7 @@ object CometFlatten extends CometExpressionSerde[Flatten] 
with ArraysBase {
       binding: Boolean): Option[ExprOuterClass.Expr] = {
     val flattenExprProto = exprToProto(expr.child, inputs, binding)
     val flattenScalarExpr = scalarFunctionExprToProto("flatten", 
flattenExprProto)
-    optExprWithFallbackReason(flattenScalarExpr, expr, expr.children: _*)
+    flattenScalarExpr
   }
 }
 
@@ -762,7 +750,7 @@ object CometArrayPosition extends 
CometExpressionSerde[ArrayPosition] with Array
     // (matching Spark's behavior)
     val optExpr =
       scalarFunctionExprToProto("spark_array_position", arrayExprProto, 
elementExprProto)
-    optExprWithFallbackReason(optExpr, expr, expr.left, expr.right)
+    optExpr
   }
 }
 
@@ -829,10 +817,7 @@ object CometArraysZip extends 
CometExpressionSerde[ArraysZip] {
           .build())
 
     } else {
-      withFallbackReason(
-        expr,
-        "unsupported arguments for ArraysZip",
-        expr.children ++ expr.names: _*)
+      withFallbackReason(expr, "unsupported arguments for ArraysZip")
       None
     }
   }
diff --git a/spark/src/main/scala/org/apache/comet/serde/bitwise.scala 
b/spark/src/main/scala/org/apache/comet/serde/bitwise.scala
index db773cd541..115bd80422 100644
--- a/spark/src/main/scala/org/apache/comet/serde/bitwise.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/bitwise.scala
@@ -53,7 +53,7 @@ object CometBitwiseNot extends 
CometExpressionSerde[BitwiseNot] {
     val childProto = exprToProto(expr.child, inputs, binding)
     val bitNotScalarExpr =
       scalarFunctionExprToProto("bitwise_not", childProto)
-    optExprWithFallbackReason(bitNotScalarExpr, expr, expr.children: _*)
+    bitNotScalarExpr
   }
 }
 
@@ -148,7 +148,7 @@ object CometBitwiseGet extends 
CometExpressionSerde[BitwiseGet] {
     val posProto = exprToProto(expr.right, inputs, binding)
     val bitGetScalarExpr =
       scalarFunctionExprToProtoWithReturnType("bit_get", ByteType, false, 
argProto, posProto)
-    optExprWithFallbackReason(bitGetScalarExpr, expr, expr.children: _*)
+    bitGetScalarExpr
   }
 }
 
diff --git a/spark/src/main/scala/org/apache/comet/serde/conditional.scala 
b/spark/src/main/scala/org/apache/comet/serde/conditional.scala
index cd07730890..df94be8c93 100644
--- a/spark/src/main/scala/org/apache/comet/serde/conditional.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/conditional.scala
@@ -23,7 +23,6 @@ import scala.jdk.CollectionConverters._
 
 import org.apache.spark.sql.catalyst.expressions.{Attribute, CaseWhen, 
Coalesce, Expression, If, IsNotNull}
 
-import org.apache.comet.CometSparkSessionExtensions.withFallbackReason
 import org.apache.comet.serde.QueryPlanSerde.exprToProtoInternal
 
 object CometIf extends CometExpressionSerde[If] {
@@ -45,7 +44,6 @@ object CometIf extends CometExpressionSerde[If] {
           .setIf(builder)
           .build())
     } else {
-      withFallbackReason(expr, expr.predicate, expr.trueValue, expr.falseValue)
       None
     }
   }
@@ -76,7 +74,6 @@ object CometCaseWhen extends CometExpressionSerde[CaseWhen] {
         if (elseValueExpr.isDefined) {
           builder.setElseExpr(elseValueExpr.get)
         } else {
-          withFallbackReason(expr, expr.elseValue.get)
           return None
         }
       }
@@ -86,7 +83,6 @@ object CometCaseWhen extends CometExpressionSerde[CaseWhen] {
           .setCaseWhen(builder)
           .build())
     } else {
-      withFallbackReason(expr, allBranches: _*)
       None
     }
   }
@@ -116,7 +112,6 @@ object CometCoalesce extends CometExpressionSerde[Coalesce] 
{
       if (elseValueExpr.isDefined) {
         builder.setElseExpr(elseValueExpr.get)
       } else {
-        withFallbackReason(expr, elseValue)
         return None
       }
       Some(
@@ -125,7 +120,6 @@ object CometCoalesce extends CometExpressionSerde[Coalesce] 
{
           .setCaseWhen(builder)
           .build())
     } else {
-      withFallbackReason(expr, branches.map(_._2): _*)
       None
     }
   }
diff --git 
a/spark/src/main/scala/org/apache/comet/serde/contraintExpressions.scala 
b/spark/src/main/scala/org/apache/comet/serde/contraintExpressions.scala
index 9b30357f9f..460f23485d 100644
--- a/spark/src/main/scala/org/apache/comet/serde/contraintExpressions.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/contraintExpressions.scala
@@ -22,7 +22,7 @@ package org.apache.comet.serde
 import org.apache.spark.sql.catalyst.expressions.{Attribute, 
KnownFloatingPointNormalized, KnownNullable}
 import org.apache.spark.sql.catalyst.optimizer.NormalizeNaNAndZero
 
-import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
optExprWithFallbackReason, serializeDataType}
+import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
serializeDataType}
 
 object CometKnownFloatingPointNormalized
     extends CometExpressionSerde[KnownFloatingPointNormalized] {
@@ -63,7 +63,7 @@ object CometKnownFloatingPointNormalized
             .setDatatype(dataType)
           
ExprOuterClass.Expr.newBuilder().setNormalizeNanAndZero(builder).build()
         }
-        optExprWithFallbackReason(optExpr, expr, wrapped)
+        optExpr
 
       case child =>
         // Nested normalization (array / struct / map). Spark 4.2 normalizes 
the inputs to
@@ -72,7 +72,7 @@ object CometKnownFloatingPointNormalized
         // `KnownFloatingPointNormalized` is a runtime no-op tag, so serialize 
the child directly
         // and let its serde (e.g. the ArrayTransform codegen dispatcher) 
carry the normalization.
         val optExpr = exprToProtoInternal(child, inputs, binding)
-        optExprWithFallbackReason(optExpr, expr, child)
+        optExpr
     }
   }
 }
@@ -89,6 +89,6 @@ object CometKnownNullable extends 
CometExpressionSerde[KnownNullable] {
       inputs: Seq[Attribute],
       binding: Boolean): Option[ExprOuterClass.Expr] = {
     val optExpr = exprToProtoInternal(expr.child, inputs, binding)
-    optExprWithFallbackReason(optExpr, expr, expr.child)
+    optExpr
   }
 }
diff --git a/spark/src/main/scala/org/apache/comet/serde/datetime.scala 
b/spark/src/main/scala/org/apache/comet/serde/datetime.scala
index a2600bf688..abb39a8ea7 100644
--- a/spark/src/main/scala/org/apache/comet/serde/datetime.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/datetime.scala
@@ -79,7 +79,7 @@ trait CometExprGetDateField[T <: GetDateField] {
               .build())
           .build()
       })
-    optExprWithFallbackReason(optExpr, expr, expr.child)
+    optExpr
   }
 }
 
@@ -137,7 +137,7 @@ object CometDayOfWeek
           .build()
       }
       .headOption
-    optExprWithFallbackReason(optExpr, expr, expr.child)
+    optExpr
   }
 }
 
@@ -166,7 +166,7 @@ object CometWeekDay extends CometExpressionSerde[WeekDay] 
with CometExprGetDateF
           .build()
       }
       .headOption
-    optExprWithFallbackReason(optExpr, expr, expr.child)
+    optExpr
   }
 }
 
@@ -222,7 +222,6 @@ object CometHour extends CometExpressionSerde[Hour] {
           .setHour(builder)
           .build())
     } else {
-      withFallbackReason(expr, expr.child)
       None
     }
   }
@@ -249,7 +248,6 @@ object CometMinute extends CometExpressionSerde[Minute] {
           .setMinute(builder)
           .build())
     } else {
-      withFallbackReason(expr, expr.child)
       None
     }
   }
@@ -276,7 +274,6 @@ object CometSecond extends CometExpressionSerde[Second] {
           .setSecond(builder)
           .build())
     } else {
-      withFallbackReason(expr, expr.child)
       None
     }
   }
@@ -348,7 +345,6 @@ object CometUnixTimestamp extends 
CometExpressionSerde[UnixTimestamp] {
           .setUnixTimestamp(builder)
           .build())
     } else {
-      withFallbackReason(expr, expr.children.head)
       None
     }
   }
@@ -383,7 +379,7 @@ object CometFromUTCTimestamp
       binding: Boolean): Option[ExprOuterClass.Expr] = {
     val childExprs = expr.children.map(exprToProtoInternal(_, inputs, binding))
     val optExpr = scalarFunctionExprToProto("from_utc_timestamp", childExprs: 
_*)
-    optExprWithFallbackReason(optExpr, expr, expr.children: _*)
+    optExpr
   }
 }
 
@@ -403,7 +399,7 @@ object CometToUTCTimestamp
       binding: Boolean): Option[ExprOuterClass.Expr] = {
     val childExprs = expr.children.map(exprToProtoInternal(_, inputs, binding))
     val optExpr = scalarFunctionExprToProto("to_utc_timestamp", childExprs: _*)
-    optExprWithFallbackReason(optExpr, expr, expr.children: _*)
+    optExpr
   }
 }
 
@@ -434,7 +430,7 @@ object CometConvertTimezone
     val ts = exprToProtoInternal(expr.sourceTs, inputs, binding)
     val toUtc = scalarFunctionExprToProto("to_utc_timestamp", ts, srcTz)
     val fromUtc = scalarFunctionExprToProto("from_utc_timestamp", toUtc, tgtTz)
-    optExprWithFallbackReason(fromUtc, expr, expr.children: _*)
+    fromUtc
   }
 }
 
@@ -470,7 +466,7 @@ object CometNextDay extends CometExpressionSerde[NextDay] {
       DateType,
       expr.failOnError,
       childExpr: _*)
-    optExprWithFallbackReason(optExpr, expr, expr.children: _*)
+    optExpr
   }
 }
 
@@ -496,7 +492,7 @@ object CometMakeDate extends CometExpressionSerde[MakeDate] 
{
       DateType,
       expr.failOnError,
       childExpr: _*)
-    optExprWithFallbackReason(optExpr, expr, expr.children: _*)
+    optExpr
   }
 }
 
@@ -543,7 +539,7 @@ object CometUnixDate extends CometExpressionSerde[UnixDate] 
{
             .build())
         .build()
     }
-    optExprWithFallbackReason(optExpr, expr, expr.child)
+    optExpr
   }
 }
 
@@ -597,7 +593,7 @@ object CometTruncDate extends 
CometExpressionSerde[TruncDate] with CodegenDispat
         false,
         childExpr,
         formatExpr)
-    optExprWithFallbackReason(optExpr, expr, expr.date, expr.format)
+    optExpr
   }
 }
 
@@ -687,7 +683,6 @@ object CometTruncTimestamp
           .setTruncTimestamp(builder)
           .build())
     } else {
-      withFallbackReason(expr, expr.timestamp, expr.format)
       None
     }
   }
@@ -811,7 +806,7 @@ object CometDateFormat
         false,
         childExpr,
         formatExpr)
-      optExprWithFallbackReason(optExpr, expr, expr.left, expr.right)
+      optExpr
     } else {
       // Hand the full `DateFormatClass` (with `timeZoneId` already stamped by 
`ResolveTimeZone`)
       // to the codegen dispatcher. It closure-serializes the bound tree, so 
non-UTC timezones
@@ -851,7 +846,7 @@ object CometHours extends CometExpressionSerde[Hours] {
         .setHoursTransform(builder)
         .build()
     }
-    optExprWithFallbackReason(optExpr, expr, expr.child)
+    optExpr
   }
 }
 
@@ -908,7 +903,7 @@ object CometDays extends CometExpressionSerde[Days] {
         .build()
     }
 
-    optExprWithFallbackReason(optExpr, expr, expr.child)
+    optExpr
   }
 }
 
@@ -1017,6 +1012,6 @@ object CometPreciseTimestampConversion extends 
CometExpressionSerde[PreciseTimes
         .setDatatype(dataType)
       
ExprOuterClass.Expr.newBuilder().setPreciseTimestampConversion(builder).build()
     }
-    optExprWithFallbackReason(optExpr, expr, expr.child)
+    optExpr
   }
 }
diff --git 
a/spark/src/main/scala/org/apache/comet/serde/decimalExpressions.scala 
b/spark/src/main/scala/org/apache/comet/serde/decimalExpressions.scala
index f41e0fafaa..b2bfe6ee06 100644
--- a/spark/src/main/scala/org/apache/comet/serde/decimalExpressions.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/decimalExpressions.scala
@@ -22,7 +22,7 @@ package org.apache.comet.serde
 import org.apache.spark.sql.catalyst.expressions.{Attribute, MakeDecimal, 
UnscaledValue}
 import org.apache.spark.sql.types.{DecimalType, LongType}
 
-import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
optExprWithFallbackReason, scalarFunctionExprToProtoWithReturnType}
+import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
scalarFunctionExprToProtoWithReturnType}
 
 object CometUnscaledValue extends CometExpressionSerde[UnscaledValue] {
   override def convert(
@@ -32,7 +32,7 @@ object CometUnscaledValue extends 
CometExpressionSerde[UnscaledValue] {
     val childExpr = exprToProtoInternal(expr.child, inputs, binding)
     val optExpr =
       scalarFunctionExprToProtoWithReturnType("unscaled_value", LongType, 
false, childExpr)
-    optExprWithFallbackReason(optExpr, expr, expr.child)
+    optExpr
 
   }
 }
@@ -58,7 +58,7 @@ object CometMakeDecimal extends 
CometExpressionSerde[MakeDecimal] {
       DecimalType(expr.precision, expr.scale),
       failOnError = !expr.nullOnOverflow,
       childExpr)
-    optExprWithFallbackReason(optExpr, expr, expr.child)
+    optExpr
 
   }
 }
diff --git a/spark/src/main/scala/org/apache/comet/serde/json.scala 
b/spark/src/main/scala/org/apache/comet/serde/json.scala
index 02a9e14eb5..3a486ce71a 100644
--- a/spark/src/main/scala/org/apache/comet/serde/json.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/json.scala
@@ -23,7 +23,7 @@ import org.apache.spark.sql.catalyst.expressions.{Attribute, 
JsonObjectKeys, Len
 
 import org.apache.comet.CometConf
 import org.apache.comet.serde.ExprOuterClass.Expr
-import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
optExprWithFallbackReason, scalarFunctionExprToProto}
+import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
scalarFunctionExprToProto}
 
 /**
  * `json_array_length` runs Spark's own implementation through the codegen 
dispatcher by default,
@@ -56,7 +56,7 @@ object CometLengthOfJsonArray
     if (CometConf.isExprAllowIncompat(getExprConfigName(expr))) {
       val childExpr = expr.children.map(exprToProtoInternal(_, inputs, 
binding))
       val optExpr = scalarFunctionExprToProto("json_array_length", childExpr: 
_*)
-      optExprWithFallbackReason(optExpr, expr, expr.children: _*)
+      optExpr
     } else {
       super.convert(expr, inputs, binding)
     }
diff --git a/spark/src/main/scala/org/apache/comet/serde/maps.scala 
b/spark/src/main/scala/org/apache/comet/serde/maps.scala
index 817792e1d2..c889284129 100644
--- a/spark/src/main/scala/org/apache/comet/serde/maps.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/maps.scala
@@ -23,7 +23,7 @@ import org.apache.spark.sql.catalyst.expressions._
 import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.types._
 
-import org.apache.comet.serde.QueryPlanSerde.{createBinaryExpr, 
exprToProtoInternal, optExprWithFallbackReason, scalarFunctionExprToProto}
+import org.apache.comet.serde.QueryPlanSerde.{createBinaryExpr, 
exprToProtoInternal, scalarFunctionExprToProto}
 import org.apache.comet.shims.CometTypeShim
 
 object CometMapKeys extends CometExpressionSerde[MapKeys] {
@@ -34,7 +34,7 @@ object CometMapKeys extends CometExpressionSerde[MapKeys] {
       binding: Boolean): Option[ExprOuterClass.Expr] = {
     val childExpr = exprToProtoInternal(expr.child, inputs, binding)
     val mapKeysScalarExpr = scalarFunctionExprToProto("map_keys", childExpr)
-    optExprWithFallbackReason(mapKeysScalarExpr, expr, expr.children: _*)
+    mapKeysScalarExpr
   }
 }
 
@@ -46,7 +46,7 @@ object CometMapEntries extends 
CometExpressionSerde[MapEntries] {
       binding: Boolean): Option[ExprOuterClass.Expr] = {
     val childExpr = exprToProtoInternal(expr.child, inputs, binding)
     val mapEntriesScalarExpr = scalarFunctionExprToProto("map_entries", 
childExpr)
-    optExprWithFallbackReason(mapEntriesScalarExpr, expr, expr.children: _*)
+    mapEntriesScalarExpr
   }
 }
 
@@ -58,7 +58,7 @@ object CometMapValues extends CometExpressionSerde[MapValues] 
{
       binding: Boolean): Option[ExprOuterClass.Expr] = {
     val childExpr = exprToProtoInternal(expr.child, inputs, binding)
     val mapValuesScalarExpr = scalarFunctionExprToProto("map_values", 
childExpr)
-    optExprWithFallbackReason(mapValuesScalarExpr, expr, expr.children: _*)
+    mapValuesScalarExpr
   }
 }
 
@@ -71,7 +71,7 @@ object CometMapExtract extends 
CometExpressionSerde[GetMapValue] {
     val mapExpr = exprToProtoInternal(expr.child, inputs, binding)
     val keyExpr = exprToProtoInternal(expr.key, inputs, binding)
     val mapExtractExpr = scalarFunctionExprToProto("map_extract", mapExpr, 
keyExpr)
-    optExprWithFallbackReason(mapExtractExpr, expr, expr.children: _*)
+    mapExtractExpr
   }
 }
 
diff --git a/spark/src/main/scala/org/apache/comet/serde/math.scala 
b/spark/src/main/scala/org/apache/comet/serde/math.scala
index e70794faa3..a0ea76cc06 100644
--- a/spark/src/main/scala/org/apache/comet/serde/math.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/math.scala
@@ -22,8 +22,7 @@ package org.apache.comet.serde
 import org.apache.spark.sql.catalyst.expressions.{Abs, Add, Atan2, Attribute, 
BRound, Ceil, CheckOverflow, Conv, Expression, Floor, Hex, Hypot, If, 
LessThanOrEqual, Literal, Log, Log10, Log1p, Log2, Logarithm, NaNvl, Pmod, Pow, 
UnaryPositive, Unhex, WidthBucket}
 import org.apache.spark.sql.types.{DecimalType, DoubleType, NumericType}
 
-import org.apache.comet.CometSparkSessionExtensions.withFallbackReason
-import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
optExprWithFallbackReason, scalarFunctionExprToProto, 
scalarFunctionExprToProtoWithReturnType, serializeDataType}
+import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
scalarFunctionExprToProto, scalarFunctionExprToProtoWithReturnType, 
serializeDataType}
 
 object CometAtan2 extends CometExpressionSerde[Atan2] {
   override def convert(
@@ -36,7 +35,7 @@ object CometAtan2 extends CometExpressionSerde[Atan2] {
     val leftExpr = exprToProtoInternal(left, inputs, binding)
     val rightExpr = exprToProtoInternal(right, inputs, binding)
     val optExpr = scalarFunctionExprToProto("atan2", leftExpr, rightExpr)
-    optExprWithFallbackReason(optExpr, expr, expr.left, expr.right)
+    optExpr
   }
 }
 
@@ -59,7 +58,7 @@ object CometCeil extends CometExpressionSerde[Ceil] {
       case _ =>
         val optExpr =
           scalarFunctionExprToProtoWithReturnType("ceil", expr.dataType, 
false, childExpr)
-        optExprWithFallbackReason(optExpr, expr, expr.child)
+        optExpr
     }
   }
 }
@@ -83,7 +82,7 @@ object CometFloor extends CometExpressionSerde[Floor] {
       case _ =>
         val optExpr =
           scalarFunctionExprToProtoWithReturnType("floor", expr.dataType, 
false, childExpr)
-        optExprWithFallbackReason(optExpr, expr, expr.child)
+        optExpr
     }
   }
 }
@@ -98,7 +97,7 @@ object CometLog extends CometExpressionSerde[Log] with 
MathExprBase {
       binding: Boolean): Option[ExprOuterClass.Expr] = {
     val childExpr = exprToProtoInternal(nullIfNegative(expr.child), inputs, 
binding)
     val optExpr = scalarFunctionExprToProto("ln", childExpr)
-    optExprWithFallbackReason(optExpr, expr, expr.child)
+    optExpr
   }
 }
 
@@ -109,7 +108,7 @@ object CometLog10 extends CometExpressionSerde[Log10] with 
MathExprBase {
       binding: Boolean): Option[ExprOuterClass.Expr] = {
     val childExpr = exprToProtoInternal(nullIfNegative(expr.child), inputs, 
binding)
     val optExpr = scalarFunctionExprToProto("log10", childExpr)
-    optExprWithFallbackReason(optExpr, expr, expr.child)
+    optExpr
   }
 }
 
@@ -120,7 +119,7 @@ object CometLog2 extends CometExpressionSerde[Log2] with 
MathExprBase {
       binding: Boolean): Option[ExprOuterClass.Expr] = {
     val childExpr = exprToProtoInternal(nullIfNegative(expr.child), inputs, 
binding)
     val optExpr = scalarFunctionExprToProto("log2", childExpr)
-    optExprWithFallbackReason(optExpr, expr, expr.child)
+    optExpr
 
   }
 }
@@ -136,7 +135,7 @@ object CometLogarithm extends 
CometExpressionSerde[Logarithm] {
     val rightExpr = exprToProtoInternal(expr.right, inputs, binding)
     val optExpr =
       scalarFunctionExprToProtoWithReturnType("spark_log", DoubleType, false, 
leftExpr, rightExpr)
-    optExprWithFallbackReason(optExpr, expr, expr.left, expr.right)
+    optExpr
   }
 }
 
@@ -147,7 +146,7 @@ object CometHex extends CometExpressionSerde[Hex] with 
MathExprBase {
       binding: Boolean): Option[ExprOuterClass.Expr] = {
     val childExpr = exprToProtoInternal(expr.child, inputs, binding)
     val optExpr = scalarFunctionExprToProtoWithReturnType("hex", 
expr.dataType, false, childExpr)
-    optExprWithFallbackReason(optExpr, expr, expr.child)
+    optExpr
   }
 }
 
@@ -166,7 +165,7 @@ object CometUnhex extends CometExpressionSerde[Unhex] with 
MathExprBase {
         false,
         childExpr,
         failOnErrorExpr)
-    optExprWithFallbackReason(optExpr, expr, expr.child)
+    optExpr
   }
 }
 
@@ -200,7 +199,7 @@ object CometAbs extends CometExpressionSerde[Abs] with 
MathExprBase {
         false,
         childExpr,
         failOnErrorExpr)
-    optExprWithFallbackReason(optExpr, expr, expr.child)
+    optExpr
   }
 }
 
@@ -213,7 +212,7 @@ object CometPow extends CometExpressionSerde[Pow] {
     val leftExpr = exprToProtoInternal(expr.left, inputs, binding)
     val rightExpr = exprToProtoInternal(expr.right, inputs, binding)
     val optExpr = scalarFunctionExprToProto("pow", leftExpr, rightExpr)
-    optExprWithFallbackReason(optExpr, expr, expr.left, expr.right)
+    optExpr
   }
 }
 
@@ -258,7 +257,6 @@ object CometCheckOverflow extends 
CometExpressionSerde[CheckOverflow] {
           .setCheckOverflow(builder)
           .build())
     } else {
-      withFallbackReason(expr, expr.child)
       None
     }
   }
diff --git a/spark/src/main/scala/org/apache/comet/serde/namedExpressions.scala 
b/spark/src/main/scala/org/apache/comet/serde/namedExpressions.scala
index fb910ec3ac..edd083c282 100644
--- a/spark/src/main/scala/org/apache/comet/serde/namedExpressions.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/namedExpressions.scala
@@ -29,11 +29,7 @@ object CometAlias extends CometExpressionSerde[Alias] {
       a: Alias,
       inputs: Seq[Attribute],
       binding: Boolean): Option[ExprOuterClass.Expr] = {
-    val r = exprToProtoInternal(a.child, inputs, binding)
-    if (r.isEmpty) {
-      withFallbackReason(a, a.child)
-    }
-    r
+    exprToProtoInternal(a.child, inputs, binding)
   }
 }
 
diff --git a/spark/src/main/scala/org/apache/comet/serde/predicates.scala 
b/spark/src/main/scala/org/apache/comet/serde/predicates.scala
index af865b410c..0e7bb02cd6 100644
--- a/spark/src/main/scala/org/apache/comet/serde/predicates.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/predicates.scala
@@ -26,7 +26,7 @@ import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.types.BooleanType
 
 import org.apache.comet.CometConf
-import org.apache.comet.CometSparkSessionExtensions.{isSpark35Plus, 
isSpark40Plus, withFallbackReason}
+import org.apache.comet.CometSparkSessionExtensions.{isSpark35Plus, 
isSpark40Plus}
 import org.apache.comet.serde.ExprOuterClass.Expr
 import org.apache.comet.serde.QueryPlanSerde._
 
@@ -244,7 +244,7 @@ object CometIsNaN extends CometExpressionSerde[IsNaN] {
     val childExpr = exprToProtoInternal(expr.child, inputs, binding)
     val optExpr = scalarFunctionExprToProtoWithReturnType("isnan", 
BooleanType, false, childExpr)
 
-    optExprWithFallbackReason(optExpr, expr, expr.child)
+    optExpr
   }
 }
 
@@ -385,8 +385,6 @@ object ComparisonUtils {
           .setIn(builder)
           .build())
     } else {
-      val allExprs = list ++ Seq(value)
-      withFallbackReason(expr, allExprs: _*)
       None
     }
   }
diff --git a/spark/src/main/scala/org/apache/comet/serde/statics.scala 
b/spark/src/main/scala/org/apache/comet/serde/statics.scala
index f415c373a1..6501a1d46c 100644
--- a/spark/src/main/scala/org/apache/comet/serde/statics.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/statics.scala
@@ -25,7 +25,7 @@ import 
org.apache.spark.sql.catalyst.util.CharVarcharCodegenUtils
 import org.apache.spark.sql.types.StringType
 
 import org.apache.comet.CometSparkSessionExtensions.withFallbackReason
-import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
optExprWithFallbackReason, scalarFunctionExprToProto, 
scalarFunctionExprToProtoWithReturnType}
+import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
scalarFunctionExprToProto, scalarFunctionExprToProtoWithReturnType}
 
 object CometStaticInvoke extends CometExpressionSerde[StaticInvoke] {
 
@@ -61,8 +61,7 @@ object CometStaticInvoke extends 
CometExpressionSerde[StaticInvoke] {
       case None =>
         withFallbackReason(
           expr,
-          s"Static invoke expression: ${expr.functionName} is not supported",
-          expr.children: _*)
+          s"Static invoke expression: ${expr.functionName} is not supported")
         None
     }
   }
@@ -75,7 +74,7 @@ object CometUrlEncodeStaticInvoke extends 
CometExpressionSerde[StaticInvoke] {
       binding: Boolean): Option[ExprOuterClass.Expr] = {
     val childExpr = exprToProtoInternal(expr.children.head, inputs, binding)
     val optExpr = scalarFunctionExprToProto("url_encode", childExpr)
-    optExprWithFallbackReason(optExpr, expr, expr.children: _*)
+    optExpr
   }
 }
 
@@ -91,7 +90,7 @@ object CometUrlDecodeStaticInvoke extends 
CometExpressionSerde[StaticInvoke] {
     val funcName = if (failOnError) "url_decode" else "try_url_decode"
     val childExpr = exprToProtoInternal(expr.children.head, inputs, binding)
     val optExpr = scalarFunctionExprToProto(funcName, childExpr)
-    optExprWithFallbackReason(optExpr, expr, expr.children: _*)
+    optExpr
   }
 }
 
@@ -113,7 +112,7 @@ object CometBase64StaticInvoke extends 
CometExpressionSerde[StaticInvoke] {
       failOnError = false,
       childExpr,
       chunkExpr)
-    optExprWithFallbackReason(optExpr, expr, expr.arguments: _*)
+    optExpr
   }
 }
 
diff --git a/spark/src/main/scala/org/apache/comet/serde/strings.scala 
b/spark/src/main/scala/org/apache/comet/serde/strings.scala
index 7422dadd97..ebf4508988 100644
--- a/spark/src/main/scala/org/apache/comet/serde/strings.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/strings.scala
@@ -24,7 +24,7 @@ import org.apache.spark.sql.types.{BinaryType, DataTypes, 
IntegerType, LongType,
 
 import org.apache.comet.CometConf
 import org.apache.comet.serde.ExprOuterClass.Expr
-import org.apache.comet.serde.QueryPlanSerde.{createBinaryExpr, 
exprToProtoInternal, optExprWithFallbackReason, scalarFunctionExprToProto, 
scalarFunctionExprToProtoWithReturnType}
+import org.apache.comet.serde.QueryPlanSerde.{createBinaryExpr, 
exprToProtoInternal, scalarFunctionExprToProto, 
scalarFunctionExprToProtoWithReturnType}
 import org.apache.comet.shims.CometTypeShim
 
 object CometStringRepeat extends CometExpressionSerde[StringRepeat] {
@@ -43,7 +43,7 @@ object CometStringRepeat extends 
CometExpressionSerde[StringRepeat] {
     val leftExpr = exprToProtoInternal(leftCast, inputs, binding)
     val rightExpr = exprToProtoInternal(rightCast, inputs, binding)
     val optExpr = scalarFunctionExprToProto("repeat", leftExpr, rightExpr)
-    optExprWithFallbackReason(optExpr, expr, leftCast, rightCast)
+    optExpr
   }
 }
 
@@ -139,7 +139,7 @@ object CometLevenshtein extends 
CometExpressionSerde[Levenshtein] {
     val childExprs = expr.children.map(exprToProtoInternal(_, inputs, binding))
     val optExpr =
       scalarFunctionExprToProtoWithReturnType("levenshtein", IntegerType, 
false, childExprs: _*)
-    optExprWithFallbackReason(optExpr, expr, expr.children: _*)
+    optExpr
   }
 }
 
@@ -220,7 +220,7 @@ object CometSubstringIndex extends 
CometExpressionSerde[SubstringIndex] {
     val countExpr = exprToProtoInternal(countCast, inputs, binding)
     val optExpr =
       scalarFunctionExprToProto("substring_index", strExpr, delimExpr, 
countExpr)
-    optExprWithFallbackReason(optExpr, expr, expr.strExpr, expr.delimExpr, 
expr.countExpr)
+    optExpr
   }
 }
 
@@ -489,7 +489,7 @@ object CometRegExpExtract extends 
CometExpressionSerde[RegExpExtract] {
         subjectExpr,
         patternExpr,
         idxExpr)
-      optExprWithFallbackReason(optExpr, expr, expr.subject, expr.regexp, 
expr.idx)
+      optExpr
     } else {
       // Default: route through the codegen dispatcher so Spark's own 
doGenCode runs inside the
       // Comet pipeline. Falls back to Spark when the dispatcher is disabled.
@@ -527,7 +527,7 @@ object CometRegExpExtractAll extends 
CometExpressionSerde[RegExpExtractAll] {
         subjectExpr,
         patternExpr,
         idxExpr)
-      optExprWithFallbackReason(optExpr, expr, expr.subject, expr.regexp, 
expr.idx)
+      optExpr
     } else {
       // Default: route through the codegen dispatcher so Spark's own 
doGenCode runs inside the
       // Comet pipeline. Falls back to Spark when the dispatcher is disabled.
@@ -577,7 +577,7 @@ object CometRegExpReplace extends 
CometExpressionSerde[RegExpReplace] with Nativ
         patternExpr,
         replacementExpr,
         flagsExpr)
-      optExprWithFallbackReason(optExpr, expr, expr.subject, expr.regexp, 
expr.rep, expr.pos)
+      optExpr
     } else {
       // Default: route through the codegen dispatcher so Spark's own 
doGenCode runs inside the
       // Comet pipeline. Falls back to Spark when the dispatcher is disabled.
@@ -623,7 +623,7 @@ object CometStringSplit extends 
CometExpressionSerde[StringSplit] with NativeOpt
         strExpr,
         regexExpr,
         limitExpr)
-      optExprWithFallbackReason(optExpr, expr, expr.str, expr.regex, 
expr.limit)
+      optExpr
     } else {
       // Default: route through the codegen dispatcher so Spark's own 
doGenCode runs inside the
       // Comet pipeline. Falls back to Spark when the dispatcher is disabled.
@@ -673,7 +673,7 @@ object CometGetJsonObject extends 
CometCodegenDispatch[GetJsonObject] with Nativ
         false,
         jsonExpr,
         pathExpr)
-      optExprWithFallbackReason(optExpr, expr, expr.json, expr.path)
+      optExpr
     } else {
       super.convert(expr, inputs, binding)
     }
@@ -709,7 +709,7 @@ object CometBase64 extends CometExpressionSerde[Base64] {
         failOnError = false,
         childExpr,
         chunkExpr)
-    optExprWithFallbackReason(optExpr, expr, expr.child)
+    optExpr
   }
 }
 
diff --git a/spark/src/main/scala/org/apache/comet/serde/structs.scala 
b/spark/src/main/scala/org/apache/comet/serde/structs.scala
index 409ef38b4f..2f9619d491 100644
--- a/spark/src/main/scala/org/apache/comet/serde/structs.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/structs.scala
@@ -63,7 +63,7 @@ object CometCreateNamedStruct extends 
CometExpressionSerde[CreateNamedStruct] {
           .setCreateNamedStruct(structBuilder)
           .build())
     } else {
-      withFallbackReason(expr, "unsupported arguments for CreateNamedStruct", 
expr.valExprs: _*)
+      withFallbackReason(expr, "unsupported arguments for CreateNamedStruct")
       None
     }
 
@@ -108,7 +108,7 @@ object CometGetArrayStructFields extends 
CometExpressionSerde[GetArrayStructFiel
           .setGetArrayStructFields(arrayStructFieldsBuilder)
           .build())
     } else {
-      withFallbackReason(expr, "unsupported arguments for 
GetArrayStructFields", expr.child)
+      withFallbackReason(expr, "unsupported arguments for 
GetArrayStructFields")
       None
     }
   }
@@ -160,7 +160,6 @@ object CometStructsToJson extends 
CometCodegenDispatch[StructsToJson] with Nativ
               .setToJson(toJson)
               .build())
         case _ =>
-          withFallbackReason(expr, expr.child)
           None
       }
     } else {
diff --git a/spark/src/main/scala/org/apache/comet/serde/unixtime.scala 
b/spark/src/main/scala/org/apache/comet/serde/unixtime.scala
index f88528c468..08285c2cf1 100644
--- a/spark/src/main/scala/org/apache/comet/serde/unixtime.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/unixtime.scala
@@ -23,7 +23,7 @@ import org.apache.spark.sql.catalyst.expressions.{Attribute, 
FromUnixTime, Liter
 import org.apache.spark.sql.catalyst.util.TimestampFormatter
 
 import org.apache.comet.CometSparkSessionExtensions.withFallbackReason
-import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
optExprWithFallbackReason, scalarFunctionExprToProto}
+import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
scalarFunctionExprToProto}
 
 // TODO: DataFusion supports only -8334601211038 <= sec <= 8210266876799
 // https://github.com/apache/datafusion/issues/16594
@@ -80,9 +80,8 @@ object CometFromUnixTime extends 
CometExpressionSerde[FromUnixTime] with Codegen
       val timestampExpr =
         scalarFunctionExprToProto("from_unixtime", Seq(secExpr, timeZone): _*)
       val optExpr = scalarFunctionExprToProto("to_char", Seq(timestampExpr, 
formatExpr): _*)
-      optExprWithFallbackReason(optExpr, expr, expr.sec, expr.format)
+      optExpr
     } else {
-      withFallbackReason(expr, expr.sec, expr.format)
       None
     }
   }
diff --git a/spark/src/main/scala/org/apache/comet/serde/url.scala 
b/spark/src/main/scala/org/apache/comet/serde/url.scala
index b54d78ba25..891d12f333 100644
--- a/spark/src/main/scala/org/apache/comet/serde/url.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/url.scala
@@ -21,7 +21,7 @@ package org.apache.comet.serde
 
 import org.apache.spark.sql.catalyst.expressions.{Attribute, ParseUrl}
 
-import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
optExprWithFallbackReason, scalarFunctionExprToProto}
+import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
scalarFunctionExprToProto}
 
 object CometParseUrl extends CometExpressionSerde[ParseUrl] {
 
@@ -32,6 +32,6 @@ object CometParseUrl extends CometExpressionSerde[ParseUrl] {
     val funcName = if (expr.failOnError) "parse_url" else "try_parse_url"
     val childExprs = expr.children.map(exprToProtoInternal(_, inputs, binding))
     val optExpr = scalarFunctionExprToProto(funcName, childExprs: _*)
-    optExprWithFallbackReason(optExpr, expr, expr.children: _*)
+    optExpr
   }
 }
diff --git 
a/spark/src/main/scala/org/apache/spark/sql/comet/CometWindowExec.scala 
b/spark/src/main/scala/org/apache/spark/sql/comet/CometWindowExec.scala
index 97ef6417d6..4d4abf3d2e 100644
--- a/spark/src/main/scala/org/apache/spark/sql/comet/CometWindowExec.scala
+++ b/spark/src/main/scala/org/apache/spark/sql/comet/CometWindowExec.scala
@@ -51,7 +51,7 @@ object CometWindowExec extends CometOperatorSerde[WindowExec] 
{
 
     val winExprs: Seq[WindowExpressionInfo] = op.windowExpression.map { expr =>
       extractWindowExpression(expr).getOrElse {
-        withFallbackReason(op, s"Unsupported window expression: $expr", expr)
+        withFallbackReason(op, s"Unsupported window expression: $expr")
         return None
       }
     }
@@ -74,16 +74,8 @@ object CometWindowExec extends 
CometOperatorSerde[WindowExec] {
       windowBuilder.addAllOrderByList(sortOrders.map(_.get).asJava)
       Some(builder.setWindow(windowBuilder).build())
     } else {
-      // Roll up reasons already attached to per-expression nodes so the Window
-      // operator itself carries a fallback attribution. Without this, the plan
-      // prints a bare `Window` and the real reason lives on a sub-expression
-      // that isn't obvious in the standard explain output.
-      val failing = winExprs.toSeq.zip(windowExprProto).collect { case (we, 
None) =>
-        we.windowExpression
-      } ++
-        op.partitionSpec.zip(partitionExprs).collect { case (e, None) => e } ++
-        op.orderSpec.zip(sortOrders).collect { case (e, None) => e }
-      withFallbackReason(op, failing: _*)
+      // Whichever of the window / partition / order expressions failed has 
already recorded its
+      // own reason; `CometExecRule.rollUpFallbackReasons` lifts it onto this 
operator.
       None
     }
   }
@@ -204,28 +196,28 @@ object CometWindowExec extends 
CometOperatorSerde[WindowExec] {
               if (AggSerde.minMaxDataTypeSupported(min.dataType)) {
                 Some(agg)
               } else {
-                withFallbackReason(windowExpr, s"datatype ${min.dataType} is 
not supported", expr)
+                withFallbackReason(windowExpr, s"datatype ${min.dataType} is 
not supported")
                 None
               }
             case max: Max =>
               if (AggSerde.minMaxDataTypeSupported(max.dataType)) {
                 Some(agg)
               } else {
-                withFallbackReason(windowExpr, s"datatype ${max.dataType} is 
not supported", expr)
+                withFallbackReason(windowExpr, s"datatype ${max.dataType} is 
not supported")
                 None
               }
             case s: Sum =>
               if (AggSerde.sumDataTypeSupported(s.dataType)) {
                 Some(agg)
               } else {
-                withFallbackReason(windowExpr, s"datatype ${s.dataType} is not 
supported", expr)
+                withFallbackReason(windowExpr, s"datatype ${s.dataType} is not 
supported")
                 None
               }
             case a: Average =>
               if (AggSerde.avgDataTypeSupported(a.dataType)) {
                 Some(agg)
               } else {
-                withFallbackReason(windowExpr, s"datatype ${a.dataType} is not 
supported", expr)
+                withFallbackReason(windowExpr, s"datatype ${a.dataType} is not 
supported")
                 None
               }
             case _: First =>
@@ -236,8 +228,7 @@ object CometWindowExec extends 
CometOperatorSerde[WindowExec] {
               withFallbackReason(
                 windowExpr,
                 s"aggregate ${agg.aggregateFunction}" +
-                  " is not supported for window function",
-                expr)
+                  " is not supported for window function")
               None
           }
         case _ =>
@@ -268,7 +259,7 @@ object CometWindowExec extends 
CometOperatorSerde[WindowExec] {
       windowExpr.windowFunction match {
         case lag: Lag if !lag.default.isInstanceOf[Literal] =>
           // https://github.com/apache/datafusion-comet/issues/4268
-          withFallbackReason(windowExpr, "Lag default value must be a 
literal", lag.default)
+          withFallbackReason(windowExpr, "Lag default value must be a literal")
           (None, None, false)
         case lag: Lag =>
           val inputExpr = exprToProto(lag.input, output)
@@ -278,7 +269,7 @@ object CometWindowExec extends 
CometOperatorSerde[WindowExec] {
           (None, func, lag.ignoreNulls)
         case lead: Lead if !lead.default.isInstanceOf[Literal] =>
           // https://github.com/apache/datafusion-comet/issues/4268
-          withFallbackReason(windowExpr, "Lead default value must be a 
literal", lead.default)
+          withFallbackReason(windowExpr, "Lead default value must be a 
literal")
           (None, None, false)
         case lead: Lead =>
           val inputExpr = exprToProto(lead.input, output)
@@ -317,8 +308,7 @@ object CometWindowExec extends 
CometOperatorSerde[WindowExec] {
         case other =>
           withFallbackReason(
             windowExpr,
-            s"window function ${other.getClass.getSimpleName} is not 
supported",
-            other)
+            s"window function ${other.getClass.getSimpleName} is not 
supported")
           (None, None, false)
       }
     }
diff --git a/spark/src/main/scala/org/apache/spark/sql/comet/operators.scala 
b/spark/src/main/scala/org/apache/spark/sql/comet/operators.scala
index 05439a16e3..74ef3af91c 100644
--- a/spark/src/main/scala/org/apache/spark/sql/comet/operators.scala
+++ b/spark/src/main/scala/org/apache/spark/sql/comet/operators.scala
@@ -1056,7 +1056,6 @@ object CometProjectExec extends 
CometOperatorSerde[ProjectExec] {
         .addAllProjectList(exprs.map(_.get).asJava)
       Some(builder.setProjection(projectBuilder).build())
     } else {
-      withFallbackReason(op, op.projectList: _*)
       None
     }
   }
@@ -1116,7 +1115,6 @@ object CometFilterExec extends 
CometOperatorSerde[FilterExec] {
         .setPredicate(cond.get)
       Some(builder.setFilter(filterBuilder).build())
     } else {
-      withFallbackReason(op, op.condition, op.child)
       None
     }
   }
@@ -1189,7 +1187,7 @@ object CometSortExec extends CometOperatorSerde[SortExec] 
{
         .addAllSortOrders(sortOrders.map(_.get).asJava)
       Some(builder.setSort(sortBuilder).build())
     } else {
-      withFallbackReason(op, "sort order not supported", op.sortOrder: _*)
+      withFallbackReason(op, "sort order not supported")
       None
     }
   }
@@ -1379,11 +1377,7 @@ object CometExpandExec extends 
CometOperatorSerde[ExpandExec] {
       op: ExpandExec,
       builder: Operator.Builder,
       childOp: OperatorOuterClass.Operator*): 
Option[OperatorOuterClass.Operator] = {
-    var allProjExprs: Seq[Expression] = Seq()
-    val projExprs = op.projections.flatMap(_.map(e => {
-      allProjExprs = allProjExprs :+ e
-      exprToProto(e, op.child.output)
-    }))
+    val projExprs = op.projections.flatMap(_.map(e => exprToProto(e, 
op.child.output)))
 
     if (projExprs.forall(_.isDefined) && childOp.nonEmpty) {
       val expandBuilder = OperatorOuterClass.Expand
@@ -1392,7 +1386,6 @@ object CometExpandExec extends 
CometOperatorSerde[ExpandExec] {
         .setNumExprPerProject(op.projections.head.size)
       Some(builder.setExpand(expandBuilder).build())
     } else {
-      withFallbackReason(op, allProjExprs: _*)
       None
     }
   }
@@ -1472,7 +1465,6 @@ object CometExplodeExec extends 
CometOperatorSerde[GenerateExec] {
     val childExprProto = exprToProto(childExpr, op.child.output)
 
     if (childExprProto.isEmpty) {
-      withFallbackReason(op, childExpr)
       return None
     }
 
@@ -1484,7 +1476,6 @@ object CometExplodeExec extends 
CometOperatorSerde[GenerateExec] {
     }
 
     if (projectExprs.exists(_.isEmpty) || childOp.isEmpty) {
-      withFallbackReason(op, op.output: _*)
       return None
     }
 
@@ -1804,10 +1795,7 @@ trait CometBaseAggregate {
       }
 
       if (aggExprs.exists(_.isEmpty)) {
-        withFallbackReason(
-          aggregate,
-          "Unsupported aggregate expression(s)",
-          aggregateExpressions ++ 
aggregateExpressions.map(_.aggregateFunction): _*)
+        withFallbackReason(aggregate, "Unsupported aggregate expression(s)")
         return None
       }
 
@@ -1848,9 +1836,6 @@ trait CometBaseAggregate {
           Some(builder.setHashAgg(hashAggBuilder).build())
         }
       } else {
-        val allChildren: Seq[Expression] =
-          groupingExpressions ++ aggregateExpressions ++ aggregateAttributes
-        withFallbackReason(aggregate, allChildren: _*)
         None
       }
     }
@@ -1879,8 +1864,7 @@ trait CometBaseAggregate {
     if (resultExprs.exists(_.isEmpty)) {
       withFallbackReason(
         aggregate,
-        s"Unsupported result expressions found in: $resultExpressions",
-        resultExpressions: _*)
+        s"Unsupported result expressions found in: $resultExpressions")
       return None
     }
     val planId = builder.getPlanId
@@ -2163,7 +2147,6 @@ trait CometHashJoin {
     val condition = join.condition.map { cond =>
       val condProto = exprToProto(cond, join.left.output ++ join.right.output)
       if (condProto.isEmpty) {
-        withFallbackReason(join, cond)
         return None
       }
       condProto.get
@@ -2202,8 +2185,6 @@ trait CometHashJoin {
       condition.foreach(joinBuilder.setCondition)
       Some(builder.setHashJoin(joinBuilder).build())
     } else {
-      val allExprs: Seq[Expression] = joinKeys
-      withFallbackReason(join, allExprs: _*)
       None
     }
   }
@@ -2330,7 +2311,6 @@ object CometBroadcastNestedLoopJoinExec extends 
CometOperatorSerde[BroadcastNest
     val joinCondition = op.condition.map({ cond =>
       val condProto = exprToProto(cond, op.left.output ++ op.right.output)
       if (condProto.isEmpty) {
-        withFallbackReason(op, cond)
         return None
       }
       condProto.get
@@ -2648,15 +2628,13 @@ object CometSortMergeJoinExec extends 
CometOperatorSerde[SortMergeJoinExec] {
         .get(join.conf)) {
       withFallbackReason(
         join,
-        s"${CometConf.COMET_EXEC_SORT_MERGE_JOIN_WITH_JOIN_FILTER_ENABLED.key} 
is not enabled",
-        join.condition.get)
+        s"${CometConf.COMET_EXEC_SORT_MERGE_JOIN_WITH_JOIN_FILTER_ENABLED.key} 
is not enabled")
       return None
     }
 
     val condition = join.condition.map { cond =>
       val condProto = exprToProto(cond, join.left.output ++ join.right.output)
       if (condProto.isEmpty) {
-        withFallbackReason(join, cond)
         return None
       }
       condProto.get
@@ -2717,8 +2695,6 @@ object CometSortMergeJoinExec extends 
CometOperatorSerde[SortMergeJoinExec] {
       condition.map(joinBuilder.setCondition)
       Some(builder.setSortMergeJoin(joinBuilder).build())
     } else {
-      val allExprs: Seq[Expression] = joinKeys
-      withFallbackReason(join, allExprs: _*)
       None
     }
   }
diff --git 
a/spark/src/main/spark-3.5/org/apache/comet/serde/CometToPrettyString.scala 
b/spark/src/main/spark-3.5/org/apache/comet/serde/CometToPrettyString.scala
index 9926102553..147eeb1e29 100644
--- a/spark/src/main/spark-3.5/org/apache/comet/serde/CometToPrettyString.scala
+++ b/spark/src/main/spark-3.5/org/apache/comet/serde/CometToPrettyString.scala
@@ -22,7 +22,6 @@ package org.apache.comet.serde
 import org.apache.spark.sql.catalyst.expressions.{Attribute, ToPrettyString}
 import org.apache.spark.sql.types.DataTypes
 
-import org.apache.comet.CometSparkSessionExtensions.withFallbackReason
 import org.apache.comet.expressions.{CometCast, CometEvalMode}
 import org.apache.comet.serde.QueryPlanSerde.{binaryOutputStyle, 
exprToProtoInternal}
 
@@ -57,7 +56,6 @@ object CometToPrettyString extends 
CometExpressionSerde[ToPrettyString] {
           .build()
         Some(ExprOuterClass.Expr.newBuilder().setToPrettyString(tps).build())
       case _ =>
-        withFallbackReason(expr, expr.child)
         None
     }
   }
diff --git 
a/spark/src/main/spark-4.1+/org/apache/comet/shims/CometExprShim.scala 
b/spark/src/main/spark-4.1+/org/apache/comet/shims/CometExprShim.scala
index 64474291e7..a2380e4008 100644
--- a/spark/src/main/spark-4.1+/org/apache/comet/shims/CometExprShim.scala
+++ b/spark/src/main/spark-4.1+/org/apache/comet/shims/CometExprShim.scala
@@ -28,7 +28,7 @@ import org.apache.spark.sql.types.TimeType
 
 import org.apache.comet.expressions.CometEvalMode
 import org.apache.comet.serde.ExprOuterClass.{BinaryOutputStyle, Expr}
-import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
optExprWithFallbackReason, scalarFunctionExprToProtoWithReturnType}
+import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
scalarFunctionExprToProtoWithReturnType}
 
 /**
  * `CometExprShim` acts as a shim for parsing expressions from different Spark 
versions.
@@ -64,7 +64,7 @@ trait CometExprShim extends Spark4xCometExprShim {
         val childExprs = s.arguments.map(exprToProtoInternal(_, inputs, 
binding))
         val optExpr =
           scalarFunctionExprToProtoWithReturnType("make_time", s.dataType, 
true, childExprs: _*)
-        optExprWithFallbackReason(optExpr, expr, s.arguments: _*)
+        optExpr
 
       case i: Invoke =>
         (i.targetObject, i.functionName, i.arguments) match {
@@ -73,7 +73,7 @@ trait CometExprShim extends Spark4xCometExprShim {
             val childExprs = args.map(exprToProtoInternal(_, inputs, binding))
             val optExpr =
               scalarFunctionExprToProtoWithReturnType("to_time", i.dataType, 
true, childExprs: _*)
-            optExprWithFallbackReason(optExpr, i, args: _*)
+            optExpr
           case _ =>
             super.sparkVersionSpecificExprToProtoInternal(expr, inputs, 
binding)
         }
@@ -89,7 +89,7 @@ trait CometExprShim extends Spark4xCometExprShim {
               i.dataType,
               false,
               childExprs: _*)
-            optExprWithFallbackReason(optExpr, expr, args: _*)
+            optExpr
           case _ =>
             super.sparkVersionSpecificExprToProtoInternal(expr, inputs, 
binding)
         }
diff --git a/spark/src/main/spark-4.x/org/apache/comet/serde/CometMapSort.scala 
b/spark/src/main/spark-4.x/org/apache/comet/serde/CometMapSort.scala
index 341c8fc24e..4fad289a08 100644
--- a/spark/src/main/spark-4.x/org/apache/comet/serde/CometMapSort.scala
+++ b/spark/src/main/spark-4.x/org/apache/comet/serde/CometMapSort.scala
@@ -23,7 +23,7 @@ import org.apache.spark.sql.catalyst.expressions.{Attribute, 
MapSort}
 import org.apache.spark.sql.types.MapType
 
 import org.apache.comet.CometConf
-import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
optExprWithFallbackReason, scalarFunctionExprToProtoWithReturnType, 
supportedScalarSortElementType}
+import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
scalarFunctionExprToProtoWithReturnType, supportedScalarSortElementType}
 
 object CometMapSort extends CometExpressionSerde[MapSort] {
 
@@ -57,6 +57,6 @@ object CometMapSort extends CometExpressionSerde[MapSort] {
       expr.dataType,
       failOnError = false,
       childExpr)
-    optExprWithFallbackReason(mapSortExpr, expr, expr.child)
+    mapSortExpr
   }
 }
diff --git 
a/spark/src/main/spark-4.x/org/apache/comet/serde/CometToPrettyString.scala 
b/spark/src/main/spark-4.x/org/apache/comet/serde/CometToPrettyString.scala
index 9926102553..147eeb1e29 100644
--- a/spark/src/main/spark-4.x/org/apache/comet/serde/CometToPrettyString.scala
+++ b/spark/src/main/spark-4.x/org/apache/comet/serde/CometToPrettyString.scala
@@ -22,7 +22,6 @@ package org.apache.comet.serde
 import org.apache.spark.sql.catalyst.expressions.{Attribute, ToPrettyString}
 import org.apache.spark.sql.types.DataTypes
 
-import org.apache.comet.CometSparkSessionExtensions.withFallbackReason
 import org.apache.comet.expressions.{CometCast, CometEvalMode}
 import org.apache.comet.serde.QueryPlanSerde.{binaryOutputStyle, 
exprToProtoInternal}
 
@@ -57,7 +56,6 @@ object CometToPrettyString extends 
CometExpressionSerde[ToPrettyString] {
           .build()
         Some(ExprOuterClass.Expr.newBuilder().setToPrettyString(tps).build())
       case _ =>
-        withFallbackReason(expr, expr.child)
         None
     }
   }
diff --git 
a/spark/src/main/spark-4.x/org/apache/comet/shims/CometExprShim4x.scala 
b/spark/src/main/spark-4.x/org/apache/comet/shims/CometExprShim4x.scala
index 234be4dc54..e15db29660 100644
--- a/spark/src/main/spark-4.x/org/apache/comet/shims/CometExprShim4x.scala
+++ b/spark/src/main/spark-4.x/org/apache/comet/shims/CometExprShim4x.scala
@@ -28,7 +28,7 @@ import 
org.apache.spark.sql.catalyst.expressions.xml.{XmlExpressionEvalUtils, XP
 import org.apache.comet.CometSparkSessionExtensions.withFallbackReason
 import org.apache.comet.serde.CometScalaUDF
 import org.apache.comet.serde.ExprOuterClass.Expr
-import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
hasNonDefaultStringCollation, optExprWithFallbackReason, 
scalarFunctionExprToProtoWithReturnType}
+import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
hasNonDefaultStringCollation, scalarFunctionExprToProtoWithReturnType}
 
 /**
  * Expression conversions shared across all Spark 4.x minor versions, compiled 
from the
@@ -58,12 +58,12 @@ trait CometExprShim4x {
       val childExpr = exprToProtoInternal(d.child, inputs, binding)
       val nameExpr =
         scalarFunctionExprToProtoWithReturnType("dayname", d.dataType, false, 
childExpr)
-      optExprWithFallbackReason(nameExpr, d, d.child)
+      nameExpr
     case m: MonthName =>
       val childExpr = exprToProtoInternal(m.child, inputs, binding)
       val nameExpr =
         scalarFunctionExprToProtoWithReturnType("monthname", m.dataType, 
false, childExpr)
-      optExprWithFallbackReason(nameExpr, m, m.child)
+      nameExpr
     case _ => None
   }
 
@@ -90,7 +90,7 @@ trait CometExprShim4x {
       false,
       strExpr,
       delimiterExpr)
-    optExprWithFallbackReason(splitExpr, expr, expr.str, expr.delimiter)
+    splitExpr
   }
 
   // Spark 4.x lowers the RuntimeReplaceable structured-text functions to an 
evaluator-backed
diff --git 
a/spark/src/test/resources/tpcds-plan-stability/approved-plans-v1_4/q9/extended.txt
 
b/spark/src/test/resources/tpcds-plan-stability/approved-plans-v1_4/q9/extended.txt
index 762d754ec4..a6278c7383 100644
--- 
a/spark/src/test/resources/tpcds-plan-stability/approved-plans-v1_4/q9/extended.txt
+++ 
b/spark/src/test/resources/tpcds-plan-stability/approved-plans-v1_4/q9/extended.txt
@@ -1,4 +1,4 @@
- Project [COMET: ]
+ Project [COMET: Unsupported data type: 
StructType(StructField(count(1),LongType,false),StructField(avg(ss_ext_discount_amt),DecimalType(11,6),true),StructField(avg(ss_net_paid),DecimalType(11,6),true))]
 :  :- Subquery
 :  :  +- CometColumnarToRow
 :  :     +- CometProject
diff --git a/spark/src/test/scala/org/apache/comet/CometExpressionSuite.scala 
b/spark/src/test/scala/org/apache/comet/CometExpressionSuite.scala
index 8091d9dd0a..50161d46c7 100644
--- a/spark/src/test/scala/org/apache/comet/CometExpressionSuite.scala
+++ b/spark/src/test/scala/org/apache/comet/CometExpressionSuite.scala
@@ -1014,14 +1014,12 @@ class CometExpressionSuite extends CometTestBase with 
AdaptiveSparkPlanHelper {
       val query = sql(s"select cast(id as string) from $table")
       val (_, cometPlan) = checkSparkAnswerAndOperator(query)
       val project = stripAQEPlan(cometPlan).collectFirst { case p: 
CometProjectExec => p }.get
-      val id = project.expressions.head
-      CometSparkSessionExtensions.withFallbackReason(id, "reason 1")
-      CometSparkSessionExtensions.withFallbackReason(project, "reason 2")
-      CometSparkSessionExtensions.withFallbackReason(project, "reason 3", id)
-      CometSparkSessionExtensions.withFallbackReason(project, id)
-      CometSparkSessionExtensions.withFallbackReason(project, "reason 4")
-      CometSparkSessionExtensions.withFallbackReason(project, "reason 5", id)
-      CometSparkSessionExtensions.withFallbackReason(project, id)
+      // Reasons accumulate on the node they are recorded against, and are 
never overwritten.
+      // There is no roll-up here: a reason tagged on an expression is lifted 
onto the enclosing
+      // operator centrally by CometExecRule, not by withFallbackReason.
+      CometSparkSessionExtensions.withFallbackReason(project, "reason 1")
+      CometSparkSessionExtensions.withFallbackReason(project, "reason 
2\nreason 3")
+      CometSparkSessionExtensions.withFallbackReasons(project, Set("reason 4", 
"reason 5"))
       CometSparkSessionExtensions.withFallbackReason(project, "reason 6")
       val explain = new ExtendedExplainInfo().generateExtendedInfo(project)
       for (i <- 1 until 7) {
diff --git 
a/spark/src/test/scala/org/apache/comet/rules/CometExecRuleSuite.scala 
b/spark/src/test/scala/org/apache/comet/rules/CometExecRuleSuite.scala
index 8825b8af09..5444a89fa3 100644
--- a/spark/src/test/scala/org/apache/comet/rules/CometExecRuleSuite.scala
+++ b/spark/src/test/scala/org/apache/comet/rules/CometExecRuleSuite.scala
@@ -33,7 +33,7 @@ import 
org.apache.spark.sql.execution.aggregate.{HashAggregateExec, ObjectHashAg
 import org.apache.spark.sql.execution.exchange.{BroadcastExchangeExec, 
ShuffleExchangeExec}
 import org.apache.spark.sql.types.{DataTypes, StructField, StructType}
 
-import org.apache.comet.CometConf
+import org.apache.comet.{CometConf, CometExplainInfo}
 import org.apache.comet.CometSparkSessionExtensions.{isSpark35Plus, 
isSpark40Plus, isSpark42Plus}
 import org.apache.comet.testing.{DataGenOptions, FuzzDataGenerator}
 
@@ -77,6 +77,104 @@ class CometExecRuleSuite extends CometTestBase {
     }.sum
   }
 
+  test("expression-level fallback reasons are rolled up onto the operator that 
falls back") {
+    // Extended explain only walks plan nodes, so a reason recorded on a 
sub-expression is
+    // invisible unless CometExecRule lifts it onto the enclosing operator. 
Disabling a single
+    // expression makes the Project fall back with the reason living on the 
Multiply node.
+    // See https://github.com/apache/datafusion-comet/issues/5230.
+    // This also pins the ordering inside `convertToComet`: strict mode is on 
in CometTestBase, so
+    // if the roll-up stopped running before the strict check, planning here 
would throw.
+    withTempView("test_data") {
+      createTestDataFrame.createOrReplaceTempView("test_data")
+
+      val sparkPlan = createSparkPlan(spark, "SELECT id * 2 as doubled FROM 
test_data")
+      assert(countOperators(sparkPlan, classOf[ProjectExec]) == 1)
+
+      withSQLConf(
+        CometConf.getExprEnabledConfigKey("Multiply") -> "false",
+        CometConf.COMET_EXEC_LOCAL_TABLE_SCAN_ENABLED.key -> "true") {
+        val transformedPlan = applyCometExecRule(sparkPlan)
+        val project = stripAQEPlan(transformedPlan).collectFirst { case p: 
ProjectExec => p }.get
+
+        val reasons = project
+          .getTagValue(CometExplainInfo.FALLBACK_REASONS)
+          .getOrElse(Set.empty[String])
+        assert(
+          reasons.exists(_.contains("Multiply")),
+          s"expected the Multiply reason on the ProjectExec, got: $reasons")
+        // The generic catch-all message must not appear: a real reason was 
available.
+        assert(
+          !reasons.exists(_.contains("is not supported")),
+          s"a real reason was available but the generic message was used too: 
$reasons")
+      }
+    }
+  }
+
+  test("strict mode fails an operator that Comet declined without recording a 
reason") {
+    // The bug this guards against is a serde returning None and forgetting to 
say why, which the
+    // generic "<operator> is not supported" message used to hide. No serde in 
the tree is in that
+    // state (the whole test corpus runs with strict mode on, which is what 
enforces it), so drive
+    // the check directly with the shape such a serde produces: a handled 
operator whose children
+    // are all native and which carries no reason on itself or its expressions.
+    withTempView("test_data") {
+      createTestDataFrame.createOrReplaceTempView("test_data")
+
+      val sparkPlan = createSparkPlan(spark, "SELECT id * 2 as doubled FROM 
test_data")
+      withSQLConf(CometConf.COMET_EXEC_LOCAL_TABLE_SCAN_ENABLED.key -> "true") 
{
+        val nativeChild = 
stripAQEPlan(applyCometExecRule(sparkPlan)).collectFirst {
+          case op: CometNativeExec => op
+        }.get
+        val rule = CometExecRule(spark)
+
+        // ProjectExec has a registered serde, so Comet did attempt this 
operator.
+        val strictOp = ProjectExec(nativeChild.output, nativeChild)
+        assert(CometExecRule.allExecs.contains(strictOp.getClass))
+        val e = intercept[IllegalStateException] {
+          rule.reportUnexplainedFallback(strictOp)
+        }
+        assert(e.getMessage.contains("recorded no fallback reason"))
+        assert(e.getMessage.contains(strictOp.nodeName))
+
+        // Production default: no throw, and the generic message so users 
still see something.
+        withSQLConf(CometConf.COMET_STRICT_FALLBACK_REASONS.key -> "false") {
+          val lenientOp = ProjectExec(nativeChild.output, nativeChild)
+          rule.reportUnexplainedFallback(lenientOp)
+          val reasons = lenientOp
+            .getTagValue(CometExplainInfo.FALLBACK_REASONS)
+            .getOrElse(Set.empty[String])
+          assert(reasons == Set(s"${lenientOp.nodeName} is not supported"))
+        }
+      }
+    }
+  }
+
+  test("strict fallback reason checking is off by default and on for Comet's 
own suites") {
+    // The strict check turns "a serde returned None without saying why" into 
a hard failure. It
+    // must stay off in production, where the generic "<operator> is not 
supported" message is the
+    // right user-facing behaviour, and on for every Comet suite so the bug 
class cannot ship
+    // again. Enabling it in CometTestBase is what actually exercises it: the 
whole test corpus
+    // runs with it on. See 
https://github.com/apache/datafusion-comet/issues/5230.
+    assert(!CometConf.COMET_STRICT_FALLBACK_REASONS.defaultValue.get)
+    
assert(CometConf.COMET_STRICT_FALLBACK_REASONS.get(spark.sessionState.conf))
+  }
+
+  test("strict mode does not fire for operators Comet never attempted to 
convert") {
+    // Strict mode must only fire when a serde actually attempted the operator 
and declined. An
+    // operator Comet has no handler for was never attempted, so demanding a 
specific reason
+    // would be wrong - it keeps the generic message.
+    withTempView("test_data") {
+      createTestDataFrame.createOrReplaceTempView("test_data")
+
+      val sparkPlan = createSparkPlan(spark, "SELECT id FROM test_data")
+      withSQLConf(CometConf.COMET_EXEC_LOCAL_TABLE_SCAN_ENABLED.key -> 
"false") {
+        // With local table scan disabled the leaf has no Comet handler 
applied, and planning
+        // must complete rather than throw.
+        val transformedPlan = applyCometExecRule(sparkPlan)
+        assert(transformedPlan != null)
+      }
+    }
+  }
+
   test(
     "CometExecRule should apply basic operator transformations, but only when 
Comet is enabled") {
     withTempView("test_data") {
diff --git a/spark/src/test/scala/org/apache/spark/sql/CometTestBase.scala 
b/spark/src/test/scala/org/apache/spark/sql/CometTestBase.scala
index b4442d5a23..f59ffa7d63 100644
--- a/spark/src/test/scala/org/apache/spark/sql/CometTestBase.scala
+++ b/spark/src/test/scala/org/apache/spark/sql/CometTestBase.scala
@@ -87,6 +87,10 @@ abstract class CometTestBase
     
conf.set(CometConf.COMET_SCAN_ALLOW_DISABLED_PARQUET_VECTORIZED_READER.key, 
"true")
     conf.set(CometConf.COMET_ONHEAP_MEMORY_OVERHEAD.key, "2g")
     
conf.set(CometConf.COMET_EXEC_SORT_MERGE_JOIN_WITH_JOIN_FILTER_ENABLED.key, 
"true")
+    // Fail loudly if a serde declines an operator without stating why, rather 
than letting the
+    // generic "<operator> is not supported" message mask the missing reason.
+    // See https://github.com/apache/datafusion-comet/issues/5230.
+    conf.set(CometConf.COMET_STRICT_FALLBACK_REASONS.key, "true")
     // SortOrder is incompatible for mixed zero and negative zero floating 
point values, but
     // this is an edge case, and we expect most users to allow sorts on 
floating point, so we
     // enable this for the tests


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

Reply via email to