MaxGekk opened a new pull request, #59984:
URL: https://github.com/apache/spark/pull/59984

   ### What changes were proposed in this pull request?
   
   With whole-stage codegen disabled, `FilterExec` evaluates its condition 
through `Predicate.create`, which under the default factory mode builds a 
`GeneratePredicate` with subexpression elimination. `GeneratePredicate` 
evaluated every common subexpression of the condition before the condition 
itself (`subexprFunctionsCode`), so a subexpression shared only by conjuncts 
after a FALSE conjunct was evaluated on a row that `And.eval` drops at that 
conjunct, and an error it raised failed the query only with whole-stage codegen 
off.
   
   This PR makes `GeneratePredicate` evaluate a common subexpression just 
before the first top-level conjunct that references it, as the CSE branch of 
`FilterExec.doConsume` does with whole-stage codegen on (SPARK-56032):
   
   - `CodegenContext.subexpressionElimination` now returns each common 
subexpression with the call of the function that evaluates it, in dependency 
order. `generateExpressions` still adds the calls to `subexprFunctionsCode`, so 
every other caller is unchanged.
   - New `CodegenContext.generateExpressionsWithSubexprCodes` generates a 
sequence of expressions with subexpression elimination and returns, per 
expression, the calls of the subexpressions it is the first to reference, to be 
put right before its code. A subexpression is still evaluated before the 
expressions that reference it.
   - `GeneratePredicate.create` splits the predicate into its top-level 
conjuncts. When subexpression elimination is on, there are several conjuncts, 
and they have a subexpression in common, it generates each conjunct with the 
subexpressions it is the first to reference before it, and evaluates them like 
`And.eval`: a FALSE conjunct returns false, a NULL one sets a flag and the 
evaluation goes on, and the predicate is true only if all conjuncts are. 
Otherwise the generated code is unchanged.
   
   The subexpression elimination itself does not change: a subexpression common 
to several conjuncts, or repeated within one, is still evaluated once. Only its 
place in the generated code moves, from before the condition to before the 
first conjunct that references it.
   
   The alternative, the internal config 
`spark.sql.subexpressionElimination.skipForShortcutExpr` (SPARK-42815), avoids 
the error by excluding everything after the first operand of an `AND`/`OR` 
chain from subexpression elimination, so a filter condition gets no elimination 
past its first conjunct. Emitting the subexpression at the first use site 
inside the generated `And` code was rejected too: a subexpression common to all 
branches of a conditional expression is used in several places, and only the 
first would carry the code.
   
   ### Why are the changes needed?
   
   The same query must not fail with whole-stage codegen off and return rows 
with it on (SPARK-60332). Reproducer (ANSI mode):
   
   ```sql
   CREATE TABLE t USING parquet AS
     SELECT * FROM VALUES (NULL, 1, 2147483647, NULL), (0, 1, 0, 1) AS v(a, b, 
y, x);
   SET spark.sql.parquet.filterPushdown=false;
   
   SET spark.sql.codegen.wholeStage=false;
   SELECT a FROM t WHERE x IS NOT NULL AND NOT (a > 0 AND b > 0) AND y + 1 > 0 
AND y + 1 > -1;
   -- before: [ARITHMETIC_OVERFLOW] overflow. ... SQLSTATE: 22003
   -- after:  0
   
   SET spark.sql.codegen.wholeStage=true;
   SELECT a FROM t WHERE x IS NOT NULL AND NOT (a > 0 AND b > 0) AND y + 1 > 0 
AND y + 1 > -1;
   -- before and after: 0
   ```
   
   On the first row `isnotnull(x)` is FALSE, so `And.eval` never reaches `y + 
1`, but the common `y + 1` was evaluated up front and overflowed. The same 
happens without a NULL conjunct, e.g. with `b < 0` as the first conjunct.
   
   This is independent of SPARK-60315 (a NULL conjunct stopping the evaluation 
with codegen on) and of SPARK-60329 (an `IS NOT NULL` check generated later 
than its position with codegen on): the inconsistency here is in the opposite 
direction, codegen off raised where codegen on drops the row. The common 
subexpressions of the update expressions of an aggregate function with a 
`FILTER` clause are a separate problem, SPARK-60393.
   
   ### Does this PR introduce _any_ user-facing change?
   
   Yes, a bug fix. A predicate evaluated through `Predicate.create` outside of 
whole-stage codegen, that is a filter condition with whole-stage codegen 
disabled, or a join condition on the non-codegen paths of the hash, sort-merge, 
broadcast nested loop and cartesian product joins, no longer raises an error 
from a subexpression shared by conjuncts after a FALSE one on a row that the 
FALSE conjunct drops, as `And.eval` does not. For example, `SELECT a FROM t 
WHERE b < 0 AND y + 1 > 0 AND y + 1 > -1` no longer fails with 
`ARITHMETIC_OVERFLOW` on a row where `b` is `1` and `y` is `2147483647`. 
Results for rows that pass the earlier conjuncts do not change. A subexpression 
whose first occurrence is in a conditional branch of a conjunct, such as a 
`CASE WHEN` branch, is still evaluated before that conjunct, as the CSE branch 
of `FilterExec` does with whole-stage codegen enabled. The migration guide has 
an entry for this, and the SPARK-60315 entry no longer says that codegen off 
evaluates 
 a shared subexpression before the condition.
   
   ### How was this patch tested?
   
   - A new test in `CodegenSubexpressionEliminationSuite`: a 
`GeneratePredicate` over `And(And(first, Not(leaf)), Not(leaf))`, with and 
without subexpression elimination, checks that `leaf` is not evaluated when 
`first` is FALSE, and is evaluated, with the result of `And.eval`, when `first` 
is TRUE or NULL. `ExprWithEvaluatedState` takes its value and nullability as 
parameters for that.
   - A new test in `WholeStageCodegenSuite`, next to the SPARK-60315 tests: the 
reproducer above, the `b < 0` variant and a NULL-conjunct variant, with 
whole-stage codegen on and off and the legacy config on and off. The reproducer 
and the `b < 0` variant drop the row in every combination; the NULL-conjunct 
variant raises, except with the legacy config and codegen on, which skips the 
row.
   
   ```
   build/sbt 'catalyst/testOnly *SubexpressionEliminationSuite 
*CodeGenerationSuite *PredicateSuite *CodegenSubexpressionEliminationSuite 
*CodegenExpressionCachingSuite *CodeCompilerSuite'
   build/sbt 'sql/testOnly *WholeStageCodegenSuite *SQLQuerySuite'
   ```
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   Generated-by: Claude Code (Claude Fable 5.1)
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to