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

   ### What changes were proposed in this pull request?
   
   With whole-stage codegen on, `FilterExec` and the `FILTER` clause of an 
aggregate function (`GeneratePredicateHelper.generatePredicateCode`) generate 
an `IsNotNull` check lazily: just before the first conjunct that references its 
column, or after all conjuncts. With codegen off, `And.eval` evaluates the 
condition in its order. So a FALSE check cannot drop a row before a later 
conjunct that raises on it under codegen.
   
   This PR extends `precedingNotNullChecks` (SPARK-60315) with a rule that 
applies with or without the NULL-conjunct flag: before a conjunct that 
`mayRaise`, the pending checks written in the query that precede it in the 
condition and cannot raise themselves (`ExprUtils.canEvaluateUnconditionally`), 
such as a check on a column or on a struct field, are generated before it, in 
both `generatePredicateCode` and the CSE branch of `FilterExec.doConsume`. Such 
a check only drops a row before an error, as `And.eval` does.
   
   Two kinds of checks keep the lazy placement:
   
   - A check that `InferFiltersFromConstraints` inferred. It is implied by the 
conjunct it was inferred from, so the lazy placement, just before that 
conjunct, does not load its column earlier. Generating it at its position would 
decode the column on every row before a conjunct that can raise, which 
`mayRaise` reports for any function, cast or arithmetic: a filter `WHERE 
upper(name) = 'X' AND d > 5` over 20M Parquet rows with a `DECIMAL(38, 10)` 
column `d` went from 314 ms to 520 ms with that placement, and is back to 316 
ms without it. The rule marks the predicates it adds with a `TreeNodeTag`, 
`InferFiltersFromConstraints.INFERRED`, which the codegen reads; the tag 
survives the optimizer's copies of the predicate.
   - A check on an expression that can raise itself, such as `cast(s AS INT) IS 
NOT NULL`, past a conjunct that may reject the row (SPARK-56032).
   
   Both are generated at their position after a conjunct that can be NULL, as 
before (SPARK-60315). A check written after the raising conjunct stays after it.
   
   The synthetic `IsNotNull` check on a column, for a column covered only by a 
check on an expression, is now generated only when no direct check on the 
column exists (`idx == -1`): a direct check that is generated always precedes 
the conjunct in the generated code, so the synthetic one was dead code there. 
This drops a redundant check from the generated code of a condition that 
references a column in two conjuncts, such as `BETWEEN`.
   
   `spark.sql.legacy.filterShortCircuitOnNullConjunct` keeps controlling only 
the NULL conjunct; the placement applies in both settings, since the issue is 
independent of a NULL conjunct.
   
   ### Why are the changes needed?
   
   The query should not fail only with whole-stage codegen on (SPARK-60329). 
ANSI mode, Parquet filter pushdown off:
   
   ```sql
   CREATE TABLE t USING parquet AS
     SELECT * FROM VALUES (NULL, 1, 2147483647, NULL), (0, 1, 0, 1) AS v(a, b, 
y, x);
   SELECT a FROM t WHERE x IS NOT NULL AND y + 1 > 0 AND x > 0;
   -- codegen on, before:  [ARITHMETIC_OVERFLOW] overflow. SQLSTATE: 22003
   -- codegen on, after:   0
   -- codegen off:         0
   ```
   
   With codegen on, `isnotnull(x)` was generated just before `x > 0`, after the 
overflowing conjunct. The inferred check keeps that placement, so `SELECT a 
FROM t WHERE y + 1 > 0 AND x > 0` still raises with codegen on; the migration 
guide lists it.
   
   ### Does this PR introduce _any_ user-facing change?
   
   Yes. With whole-stage codegen on, an `IS NOT NULL` check written in the 
query that cannot raise is evaluated before a later conjunct that can raise, so 
a row the check rejects no longer raises the later conjunct's error; the row 
was dropped with codegen off already. An inferred check, and a check on an 
expression that can raise, keep the previous placement. Results for rows that 
pass the checks do not change. The migration guide has an entry.
   
   ### How was this patch tested?
   
   A new test in `WholeStageCodegenSuite`, with codegen on and off and the 
legacy config on and off: a check written in the query with and without a later 
conjunct on the column, the same through the CSE branch of `FilterExec` (a 
common `b * 2`, checked from the generated code), a check on a struct field, a 
check that can raise (`cast(s AS INT) IS NOT NULL`) that keeps the lazy 
placement, a check written after the raising conjunct that still raises, the 
inferred check that still raises with codegen on and drops the row with codegen 
off, and the aggregate `FILTER` clause. Before the fix, the codegen-on cases 
with a check written in the query raised `ARITHMETIC_OVERFLOW`. The 
SPARK-60315, SPARK-51356 and SPARK-56032 tests pass unchanged, as do 
`InferFiltersFromConstraintsSuite`, `FilterPushdownSuite`, 
`DataFrameAggregateSuite` and `SQLQuerySuite`.
   
   ```
   build/sbt 'catalyst/testOnly *InferFiltersFromConstraintsSuite 
*FilterPushdownSuite'
   build/sbt 'sql/testOnly *WholeStageCodegenSuite *DataFrameAggregateSuite 
*SQLQuerySuite'
   ```
   
   The benchmark above ran a filter over 20M Parquet rows, codegen on, Parquet 
pushdown and AQE off, median of 7 runs after 3 warm-ups, against the parent 
commit: `upper(name) = 'X' AND d > 5` 314 ms vs 316 ms, `y > 98 AND d > 5` 192 
ms vs 195 ms, `upper(name) = 'X' AND y > 5` 204 ms vs 221 ms.
   
   ### 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