[ 
https://issues.apache.org/jira/browse/SPARK-60315?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Max Gekk updated SPARK-60315:
-----------------------------
    Description: 
The same query raises an error with whole-stage codegen off and returns rows 
with it on. It happens when a filter's condition is a conjunction, an earlier 
conjunct is NULL on a row, and a later conjunct raises on that row.

h3. Reproducer

Default (ANSI) mode, a Parquet table, local mode:

{code:sql}
CREATE TABLE t USING parquet AS
  SELECT * FROM VALUES (NULL, 1, 2147483647), (0, 1, 0) AS v(a, b, y);

SET spark.sql.codegen.wholeStage=true;
SELECT a FROM t WHERE NOT (a > 0 AND b > 0) AND y + 1 > 0;
-- 0

SET spark.sql.codegen.wholeStage=false;
SELECT a FROM t WHERE NOT (a > 0 AND b > 0) AND y + 1 > 0;
-- [ARITHMETIC_OVERFLOW] overflow. Use 'try_add' to tolerate overflow and 
return NULL instead. ... SQLSTATE: 22003
{code}

The same happens in legacy mode with year-month interval arithmetic, which 
raises on overflow in every mode:

{code:sql}
SET spark.sql.ansi.enabled=false;
CREATE TABLE t2 USING parquet AS
  SELECT * FROM VALUES (NULL, 1, 178956970), (0, 1, 0) AS v(a, b, y);
SELECT a FROM t2 WHERE NOT (a > 0 AND b > 0) AND make_ym_interval(y, 0) + 
INTERVAL '1' YEAR > INTERVAL '0' YEAR;
-- codegen on: 0
-- codegen off: [INTERVAL_ARITHMETIC_OVERFLOW.WITH_SUGGESTION] Integer overflow 
while operating with intervals. ... SQLSTATE: 22015
{code}

On the first row, {{NOT (a > 0 AND b > 0)}} is {{NOT (NULL AND TRUE)}}, which 
is NULL, and the second conjunct overflows. The second row keeps the Parquet 
row group from being skipped by its statistics; it passes both conjuncts on 
both paths. {{NOT (a > 0 AND b > 0)}} is written so that the optimizer infers 
no {{isnotnull}} filter that would drop the first row earlier on both paths.

Checked on master at 5d17442dec4 (8 October 2026) with {{bin/spark-sql --master 
local[1]}}.

h3. Cause

The two paths evaluate the conjunction differently:

* *Whole-stage codegen* ({{FilterExec.generatePredicateCode}}) generates each 
top-level conjunct separately and skips the row as soon as one is false *or 
null* ({{if (isNull || !value) continue;}}). A filter keeps only rows whose 
condition is true, so the remaining conjuncts are never evaluated on that row.
* *Interpreted* ({{FilterExec.doExecute}} without codegen, via 
{{Predicate.create}} over the whole condition) evaluates the {{And}} tree with 
{{And.eval}}, which short-circuits only on {{false}}. A null left side still 
evaluates the right side, because {{NULL AND FALSE}} is false and {{NULL AND 
TRUE}} is null, and that evaluation raises.

Both are valid evaluations of {{AND}} in isolation. Inside a filter, though, 
false and null both drop the row, so the distinction the interpreted path 
computes the right side for is never used.

h3. Expected

The query should behave the same with whole-stage codegen on and off. The SQL 
standard's text points to the interpreted path's result, the error:

* *WHERE* (ISO/IEC 9075-2, 7.11 <where clause>, General Rule 2): "The <search 
condition> is effectively evaluated for each row of T. The result of the <where 
clause> is a table of those rows of T for which the result of the <search 
condition> is True."
* *AND* (9075-2, 6.35 <boolean value expression>, Table 11): Unknown AND True 
is Unknown, and Unknown AND False is False. With the first conjunct Unknown, 
the value of the search condition depends on the second conjunct.
* *Overflow* (9075-2, 6.27 <numeric value expression>, General Rule 5a): an 
exact numeric result that is not exactly representable raises "data exception — 
numeric value out of range" (SQLSTATE 22003); interval arithmetic raises "data 
exception — interval field overflow" (22015, 6.33).
* *Which parts may be skipped* (9075-1, 6.3.3.3 "Rule evaluation order"; SQL-92 
3.3.4.4): "If the result of an expression or search condition can be determined 
without completely evaluating all parts of the expression or search condition, 
then the parts of the expression or search condition whose evaluation is not 
necessary are called the inessential parts. [...] If evaluation of the 
inessential parts would cause an exception condition to be raised, then it is 
implementation-dependent whether or not that exception condition is raised." 
The same subclause makes the order of evaluation implementation-dependent, 
"particularly when operands or operators might cause conditions to be raised".

The second conjunct is not an inessential part of the search condition: without 
it the result could be False or Unknown. So the standard has it evaluated, and 
its exception raised. Skipping it, as whole-stage codegen does, relies on the 
fact that a WHERE clause drops both False and Unknown rows, but the standard 
defines inessential parts by the result of the search condition, not by the 
effect of the clause.

Read this way, the codegen path should keep evaluating the remaining conjuncts 
after a NULL one when they can raise, and stop early only on FALSE. The 
alternative, the interpreted path stopping at the first NULL conjunct as 
codegen does, keeps today's default behaviour but departs from the standard's 
text. Either way the two paths should agree.

(Sources: 9075-2 as the 2013 committee draft of the 2016 edition; the 9075-1 
rule as worded in SQL-92, 3.3.4.4, since Part 1's current text was not to hand.)

Found by differential testing of random queries against Spark's own row engine.


  was:
The same query raises an error with whole-stage codegen off and returns rows 
with it on. It happens when a filter's condition is a conjunction, an earlier 
conjunct is NULL on a row, and a later conjunct raises on that row.

h3. Reproducer

Default (ANSI) mode, a Parquet table, local mode:

{code:sql}
CREATE TABLE t USING parquet AS
  SELECT * FROM VALUES (NULL, 1, 2147483647), (0, 1, 0) AS v(a, b, y);

SET spark.sql.codegen.wholeStage=true;
SELECT a FROM t WHERE NOT (a > 0 AND b > 0) AND y + 1 > 0;
-- 0

SET spark.sql.codegen.wholeStage=false;
SELECT a FROM t WHERE NOT (a > 0 AND b > 0) AND y + 1 > 0;
-- [ARITHMETIC_OVERFLOW] overflow. Use 'try_add' to tolerate overflow and 
return NULL instead. ... SQLSTATE: 22003
{code}

The same happens in legacy mode with year-month interval arithmetic, which 
raises on overflow in every mode:

{code:sql}
SET spark.sql.ansi.enabled=false;
CREATE TABLE t2 USING parquet AS
  SELECT * FROM VALUES (NULL, 1, 178956970), (0, 1, 0) AS v(a, b, y);
SELECT a FROM t2 WHERE NOT (a > 0 AND b > 0) AND make_ym_interval(y, 0) + 
INTERVAL '1' YEAR > INTERVAL '0' YEAR;
-- codegen on: 0
-- codegen off: [INTERVAL_ARITHMETIC_OVERFLOW.WITH_SUGGESTION] Integer overflow 
while operating with intervals. ... SQLSTATE: 22015
{code}

On the first row, {{NOT (a > 0 AND b > 0)}} is {{NOT (NULL AND TRUE)}}, which 
is NULL, and the second conjunct overflows. The second row keeps the Parquet 
row group from being skipped by its statistics; it passes both conjuncts on 
both paths. {{NOT (a > 0 AND b > 0)}} is written so that the optimizer infers 
no {{isnotnull}} filter that would drop the first row earlier on both paths.

Checked on master at 5d17442dec4 (8 October 2026) with {{bin/spark-sql --master 
local[1]}}.

h3. Cause

The two paths evaluate the conjunction differently:

* *Whole-stage codegen* ({{FilterExec.generatePredicateCode}}) generates each 
top-level conjunct separately and skips the row as soon as one is false *or 
null* ({{if (isNull || !value) continue;}}). A filter keeps only rows whose 
condition is true, so the remaining conjuncts are never evaluated on that row.
* *Interpreted* ({{FilterExec.doExecute}} without codegen, via 
{{Predicate.create}} over the whole condition) evaluates the {{And}} tree with 
{{And.eval}}, which short-circuits only on {{false}}. A null left side still 
evaluates the right side, because {{NULL AND FALSE}} is false and {{NULL AND 
TRUE}} is null, and that evaluation raises.

Both are valid evaluations of {{AND}} in isolation. Inside a filter, though, 
false and null both drop the row, so the distinction the interpreted path 
computes the right side for is never used.

h3. Expected

The query should behave the same with whole-stage codegen on and off. 
Whole-stage codegen is the default, and its behaviour (no error) is the one 
users see, so the interpreted filter should match it: evaluate the top-level 
conjuncts in the same order and stop at the first one that is false or null. 
The same applies to other interpreted evaluations of a filter condition that 
codegen splits into conjuncts.

Found by differential testing of random queries against Spark's own row engine.



> Filter raises an error with whole-stage codegen off but not on when an 
> earlier conjunct is null
> -----------------------------------------------------------------------------------------------
>
>                 Key: SPARK-60315
>                 URL: https://issues.apache.org/jira/browse/SPARK-60315
>             Project: Spark
>          Issue Type: Bug
>          Components: SQL
>    Affects Versions: 3.5.8, 4.2.0, 5.0.0, 4.1.3, 4.0.4
>            Reporter: Max Gekk
>            Priority: Major
>
> The same query raises an error with whole-stage codegen off and returns rows 
> with it on. It happens when a filter's condition is a conjunction, an earlier 
> conjunct is NULL on a row, and a later conjunct raises on that row.
> h3. Reproducer
> Default (ANSI) mode, a Parquet table, local mode:
> {code:sql}
> CREATE TABLE t USING parquet AS
>   SELECT * FROM VALUES (NULL, 1, 2147483647), (0, 1, 0) AS v(a, b, y);
> SET spark.sql.codegen.wholeStage=true;
> SELECT a FROM t WHERE NOT (a > 0 AND b > 0) AND y + 1 > 0;
> -- 0
> SET spark.sql.codegen.wholeStage=false;
> SELECT a FROM t WHERE NOT (a > 0 AND b > 0) AND y + 1 > 0;
> -- [ARITHMETIC_OVERFLOW] overflow. Use 'try_add' to tolerate overflow and 
> return NULL instead. ... SQLSTATE: 22003
> {code}
> The same happens in legacy mode with year-month interval arithmetic, which 
> raises on overflow in every mode:
> {code:sql}
> SET spark.sql.ansi.enabled=false;
> CREATE TABLE t2 USING parquet AS
>   SELECT * FROM VALUES (NULL, 1, 178956970), (0, 1, 0) AS v(a, b, y);
> SELECT a FROM t2 WHERE NOT (a > 0 AND b > 0) AND make_ym_interval(y, 0) + 
> INTERVAL '1' YEAR > INTERVAL '0' YEAR;
> -- codegen on: 0
> -- codegen off: [INTERVAL_ARITHMETIC_OVERFLOW.WITH_SUGGESTION] Integer 
> overflow while operating with intervals. ... SQLSTATE: 22015
> {code}
> On the first row, {{NOT (a > 0 AND b > 0)}} is {{NOT (NULL AND TRUE)}}, which 
> is NULL, and the second conjunct overflows. The second row keeps the Parquet 
> row group from being skipped by its statistics; it passes both conjuncts on 
> both paths. {{NOT (a > 0 AND b > 0)}} is written so that the optimizer infers 
> no {{isnotnull}} filter that would drop the first row earlier on both paths.
> Checked on master at 5d17442dec4 (8 October 2026) with {{bin/spark-sql 
> --master local[1]}}.
> h3. Cause
> The two paths evaluate the conjunction differently:
> * *Whole-stage codegen* ({{FilterExec.generatePredicateCode}}) generates each 
> top-level conjunct separately and skips the row as soon as one is false *or 
> null* ({{if (isNull || !value) continue;}}). A filter keeps only rows whose 
> condition is true, so the remaining conjuncts are never evaluated on that row.
> * *Interpreted* ({{FilterExec.doExecute}} without codegen, via 
> {{Predicate.create}} over the whole condition) evaluates the {{And}} tree 
> with {{And.eval}}, which short-circuits only on {{false}}. A null left side 
> still evaluates the right side, because {{NULL AND FALSE}} is false and 
> {{NULL AND TRUE}} is null, and that evaluation raises.
> Both are valid evaluations of {{AND}} in isolation. Inside a filter, though, 
> false and null both drop the row, so the distinction the interpreted path 
> computes the right side for is never used.
> h3. Expected
> The query should behave the same with whole-stage codegen on and off. The SQL 
> standard's text points to the interpreted path's result, the error:
> * *WHERE* (ISO/IEC 9075-2, 7.11 <where clause>, General Rule 2): "The <search 
> condition> is effectively evaluated for each row of T. The result of the 
> <where clause> is a table of those rows of T for which the result of the 
> <search condition> is True."
> * *AND* (9075-2, 6.35 <boolean value expression>, Table 11): Unknown AND True 
> is Unknown, and Unknown AND False is False. With the first conjunct Unknown, 
> the value of the search condition depends on the second conjunct.
> * *Overflow* (9075-2, 6.27 <numeric value expression>, General Rule 5a): an 
> exact numeric result that is not exactly representable raises "data exception 
> — numeric value out of range" (SQLSTATE 22003); interval arithmetic raises 
> "data exception — interval field overflow" (22015, 6.33).
> * *Which parts may be skipped* (9075-1, 6.3.3.3 "Rule evaluation order"; 
> SQL-92 3.3.4.4): "If the result of an expression or search condition can be 
> determined without completely evaluating all parts of the expression or 
> search condition, then the parts of the expression or search condition whose 
> evaluation is not necessary are called the inessential parts. [...] If 
> evaluation of the inessential parts would cause an exception condition to be 
> raised, then it is implementation-dependent whether or not that exception 
> condition is raised." The same subclause makes the order of evaluation 
> implementation-dependent, "particularly when operands or operators might 
> cause conditions to be raised".
> The second conjunct is not an inessential part of the search condition: 
> without it the result could be False or Unknown. So the standard has it 
> evaluated, and its exception raised. Skipping it, as whole-stage codegen 
> does, relies on the fact that a WHERE clause drops both False and Unknown 
> rows, but the standard defines inessential parts by the result of the search 
> condition, not by the effect of the clause.
> Read this way, the codegen path should keep evaluating the remaining 
> conjuncts after a NULL one when they can raise, and stop early only on FALSE. 
> The alternative, the interpreted path stopping at the first NULL conjunct as 
> codegen does, keeps today's default behaviour but departs from the standard's 
> text. Either way the two paths should agree.
> (Sources: 9075-2 as the 2013 committee draft of the 2016 edition; the 9075-1 
> rule as worded in SQL-92, 3.3.4.4, since Part 1's current text was not to 
> hand.)
> Found by differential testing of random queries against Spark's own row 
> engine.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

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

Reply via email to