This is an automated email from the ASF dual-hosted git repository.
gengliangwang pushed a commit to branch branch-4.3
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.3 by this push:
new 33ccc3907a5a [SPARK-58428][SQL] Fix optimizer hang in DSv2 expression
pushdown
33ccc3907a5a is described below
commit 33ccc3907a5a88f8889a39d99599efd54e13c1ce
Author: AbhinavBattu <[email protected]>
AuthorDate: Sat Aug 22 15:39:32 2026 -0700
[SPARK-58428][SQL] Fix optimizer hang in DSv2 expression pushdown
### What changes were proposed in this pull request?
`V2ExpressionBuilder.generateExpression` handles a context independent
foldable
expression by constant folding it and recursing on the result, so that the
folded
value is translated by the `Literal` case:
```scala
case _ if expr.contextIndependentFoldable && <datasourceV2ExprFolding> =>
val constantExpr = ConstantFolding.constantFolding(expr)
generateExpression(constantExpr, isPredicate)
```
That assumes constant folding returns a literal. It does not.
`ConstantFolding`
returns an expression unchanged when it carries the `FAILED_TO_EVALUATE`
tag, which
is set when evaluation failed inside a conditional branch so that the error
is not
raised at planning time for a branch that may never be reached. The
recursive call
then re-enters the same case with the same expression and never makes
progress.
Because the call is in tail position it loops instead of raising
`StackOverflowError`, so the symptom is a query that never returns.
This PR recurses only when folding actually changed the expression.
This follows SPARK-50380, which made `ReorderAssociativeOperator` stop
assuming that
a foldable expression folds to a literal.
The guard is on the folding result rather than on the tag, so it also
covers any
future case where `constantFolding` returns its input unchanged.
### Why are the changes needed?
With default settings, a pushdown eligible query against a DSv2 source
hangs when a
filter contains an expression that fails to evaluate inside a conditional
branch:
```sql
SET
spark.sql.catalog.d=org.apache.spark.sql.execution.datasources.v2.jdbc.JDBCTableCatalog;
SET spark.sql.catalog.d.url=jdbc:derby:memory:v2loopdb;
SET spark.sql.catalog.d.driver=org.apache.derby.jdbc.EmbeddedDriver;
SET spark.sql.catalog.d.create=true;
CREATE NAMESPACE IF NOT EXISTS d.test;
CREATE TABLE d.test.t (c INT);
SELECT count(*) FROM d.test.t WHERE c = 1; -- control: 0
SELECT * FROM d.test.t WHERE coalesce(c, 1 div 0) = 1; -- never returns
```
Four conditions have to hold, which is why this was not hit earlier:
- ANSI mode, so `1 div 0` throws while the planner folds it
- the failing expression inside a conditional, so the error is deferred and
tagged
rather than raised
- a DSv2 source that pushes predicates, since `V2ExpressionBuilder` is only
used on
that path
- `spark.sql.optimizer.datasourceV2ExprFolding`, which defaults to true and
was
added in 4.1.0
Setting `spark.sql.optimizer.datasourceV2ExprFolding` to false avoids the
hang.
### Does this PR introduce _any_ user-facing change?
Yes. A query of the shape above previously hung and now completes. Such an
expression is no longer translated, so the predicate is not pushed to the
source and
is evaluated by Spark instead. Query results are unchanged.
### How was this patch tested?
Added a test to `DataSourceV2StrategySuite`, next to the existing
`datasourceV2ExprFolding` test. It builds `coalesce(c, 1 div 0) = 1`,
asserts that
constant folding tagged the failing branch, and then asserts that
translation
produces no V2 expression. The tag assertion is there so that the test
cannot pass
for the wrong reason if the tagging behaviour changes.
Without this change the test does not terminate. The recursion is in tail
position
and does not block, so it cannot be interrupted by a time limit, which is
why the
test asserts the result instead of using `failAfter`. This matches existing
tests
for similar issues, for example "SPARK-48843: Prevent infinite loop with
BindParameters" in `ParametersSuite`.
```
build/sbt 'sql/testOnly *DataSourceV2StrategySuite *JDBCV2Suite'
```
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Opus 5)
Closes #57974 from AbhinavBattu/SPARK-58428.
Authored-by: AbhinavBattu <[email protected]>
Signed-off-by: Gengliang Wang <[email protected]>
(cherry picked from commit 247748230419b2729ad753063e8df5a40647b4dc)
Signed-off-by: Gengliang Wang <[email protected]>
---
.../sql/catalyst/util/V2ExpressionBuilder.scala | 8 +++++++-
.../datasources/v2/DataSourceV2StrategySuite.scala | 20 ++++++++++++++++++++
2 files changed, 27 insertions(+), 1 deletion(-)
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/V2ExpressionBuilder.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/V2ExpressionBuilder.scala
index 3fb20dcd6420..d3cd4c3956bf 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/V2ExpressionBuilder.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/V2ExpressionBuilder.scala
@@ -98,8 +98,14 @@ class V2ExpressionBuilder(e: Expression, isPredicate:
Boolean = false) extends L
&&
SQLConf.get.getConfByKeyStrict[Boolean]("spark.sql.optimizer.datasourceV2ExprFolding")
=>
// If the expression is context independent foldable, we can convert it
to a literal.
// This is useful for increasing the coverage of V2 expressions.
+ // Folding returns the expression unchanged when it failed to evaluate
inside a conditional
+ // branch, and recursing on an unchanged expression would loop forever.
val constantExpr = ConstantFolding.constantFolding(expr)
- generateExpression(constantExpr, isPredicate)
+ if (constantExpr.fastEquals(expr)) {
+ None
+ } else {
+ generateExpression(constantExpr, isPredicate)
+ }
case col @ ColumnOrField(nameParts) =>
val ref = FieldReference(nameParts)
if (isPredicate && col.dataType.isInstanceOf[BooleanType]) {
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2StrategySuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2StrategySuite.scala
index 4af5a3251534..a0ca66183fdf 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2StrategySuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2StrategySuite.scala
@@ -22,6 +22,7 @@ import
org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute
import org.apache.spark.sql.catalyst.dsl.expressions._
import org.apache.spark.sql.catalyst.expressions._
import org.apache.spark.sql.catalyst.expressions.variant.VariantGet
+import org.apache.spark.sql.catalyst.optimizer.ConstantFolding
import org.apache.spark.sql.catalyst.util.V2ExpressionBuilder
import org.apache.spark.sql.connector.expressions.{Expression => V2Expression,
FieldReference, GeneralScalarExpression, LiteralValue, VariantGet =>
V2VariantGet}
import org.apache.spark.sql.connector.expressions.filter.{AlwaysFalse,
AlwaysTrue, And => V2And, Not => V2Not, Or => V2Or, Predicate}
@@ -1034,6 +1035,25 @@ class DataSourceV2StrategySuite extends
SharedSparkSession {
}
}
+ test("SPARK-58428: translating an expression that failed to evaluate does
not loop forever") {
+ withSQLConf(SQLConf.ANSI_ENABLED.key -> "true") {
+ // `coalesce(c, 1 div 0) = 1`. Constant folding defers the divide by
zero error because the
+ // failing expression sits in a conditional branch, so it is tagged
FAILED_TO_EVALUATE and
+ // left as is. `div` returns BIGINT, so `c` is LONG to keep the
`coalesce` inputs equal.
+ val c = AttributeReference("c", LongType)()
+ val predicate =
+ EqualTo(Coalesce(Seq(c, IntegralDivide(Literal(1), Literal(0)))),
Literal(1L))
+ val folded = ConstantFolding.constantFolding(predicate)
+ assert(
+ folded.exists(_.containsTag(ConstantFolding.FAILED_TO_EVALUATE)),
+ "expected the divide by zero branch to be tagged FAILED_TO_EVALUATE")
+
+ // Translating such an expression used to recurse forever. Note that a
regression hangs
+ // this test instead of failing it, as the recursion is in tail position.
+ assert(new V2ExpressionBuilder(folded, isPredicate =
true).build().isEmpty)
+ }
+ }
+
/**
* Translate the given Catalyst [[Expression]] into data source V2
[[Predicate]]
* then verify against the given [[Predicate]].
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]