Skip to content

fix: [spark] honor ANSI mode in next_day, and fix a panic and a missing argument shape - #23892

Open
andygrove wants to merge 2 commits into
apache:mainfrom
andygrove:next-day-spark-4-2-conformance
Open

andygrove wants to merge 2 commits into
apache:mainfrom
andygrove:next-day-spark-4-2-conformance

Conversation

@andygrove

@andygrove andygrove commented Jul 25, 2026

Copy link
Copy Markdown
Member

Which issue does this PR close?

  • Closes #.

No single issue tracks these fixes. They were surfaced by an audit of
next_day against the Spark source and a live Spark 4.2.0. The divergences
this PR does not fix are filed separately and referenced from the test file:

Rationale for this change

Three problems, only one of which is version-related.

The ANSI flag was ignored. Spark's NextDay has taken
failOnError: Boolean = SQLConf.get.ansiEnabled since at least 3.5, raising on
an unparseable day-of-week name and returning NULL otherwise. This is not new
in Spark 4: what changed in 4.0 is the default value of ansiEnabled and the
exception type, not the behavior the flag selects. datafusion-spark ignored
the flag entirely and always returned NULL, with TODO comments in the source
acknowledging the gap.

A panic reachable from SQL. next_date_for_day_of_week used
NaiveDate + Duration. Date32Type::to_naive_date_opt succeeds up to
chrono::NaiveDate::MAX (epoch day 95026236), and the subsequent addition
then panics on overflow. Every epoch day in 95026230..=95026236 panicked, for
any weekday:

SELECT next_day(arrow_cast(95026236, 'Date32'), 'Mon'::string);
-- thread panicked: `NaiveDate + TimeDelta` overflowed

This is reachable from plain SQL and from any Date32 column containing such a
value.

A whole argument shape was unsupported. SELECT next_day(<date literal>, <string column>) fell into an exec_err! catch-all and raised a hard error on
a query Spark evaluates normally.

The last two are version-independent bugs. The cross-version divergence proper,
the ANSI default flip and the error class change between 3.5 and 4.x, is not
fixed here and is tracked by
#23890.

What changes are included in this PR?

  • next_date_for_day_of_week uses checked_add_signed, trading the panic for
    NULL. NULL still diverges from Spark, which computes on the epoch day as
    Int with no calendar limit, so the residual gap is filed as [Bug] next_day returns NULL for far-future start dates where Spark returns a value #23891.
  • The match over ColumnarValue shapes is now exhaustive over all four
    combinations, so the (Scalar, Array) case works and the compiler enforces
    coverage. The _ => exec_err! catch-all is gone.
  • ANSI mode is honored via datafusion.execution.enable_ansi_mode, matching
    how math/abs.rs and math/negative.rs read it. Spark's per-row null
    intolerance is preserved: a NULL start date short-circuits to NULL before the
    day-of-week name is validated, so next_day(NULL, 'xx') is NULL in both
    modes. On the (Array, Scalar) path this is expressed as
    date_array.null_count() < date_array.len(), which is the closed form of
    exactly when the row-wise path raises.
  • The error message gains Spark 4.2.0's trailing period, matching the
    ILLEGAL_DAY_OF_WEEK template rather than Spark 3.5.8's older
    IllegalArgumentException text.
  • spark_next_day is split into parse_day_of_week and
    next_date_for_day_of_week. The old code round-tripped "MO" through
    "MONDAY" into str::parse::<Weekday>() with an unreachable Err arm; the
    replacement is a single table matching DateTimeUtils.getDayOfWeekFromString
    case for case.

Are these changes tested?

Yes. next_day.slt grows from 90 to 336 lines. Every expected value was
observed by running the query against a local pyspark==4.2.0 rather than
derived from reading the Spark source.

Coverage is organized as a product rather than a checklist, because the two
bugs above hid in the gaps between axes: all four ColumnarValue shapes, each
crossed with all-NULL, some-NULL and no-NULL inputs, each crossed with both
ANSI modes. The (Array, Scalar) null-intolerance case in particular passes on
the row-wise path and previously failed on the vectorized one, so testing only
one shape would have missed it.

Also covered: boundary dates through +262142-12-31 including the entire
previously-panicking range, day-name parsing (all three lengths, case
insensitivity, whitespace padding, non-ASCII), and the argument type and arity
errors.

Divergences that are not fixed here are checked in as commented-out queries
carrying the Spark 4.2.0 result and a link to the tracking issue. Uncommenting
one is the contract for verifying a future fix.

Verified with:

cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --test sqllogictests -- spark/datetime/next_day
cargo test -p datafusion-spark --lib next_day

@andygrove andygrove changed the title fix: make next_day match Spark 4.2.0 semantics fix: honor ANSI mode in next_day, and fix a panic and a missing argument shape Jul 25, 2026
@andygrove
andygrove force-pushed the next-day-spark-4-2-conformance branch 2 times, most recently from db5812e to 5ed5d6a Compare July 25, 2026 17:52
@github-actions github-actions Bot added sqllogictest SQL Logic Tests (.slt) spark labels Jul 25, 2026
@andygrove andygrove changed the title fix: honor ANSI mode in next_day, and fix a panic and a missing argument shape fix: [spark] honor ANSI mode in next_day, and fix a panic and a missing argument shape Jul 25, 2026
@codecov-commenter

codecov-commenter commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.83051% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.94%. Comparing base (140c7c5) to head (9f94893).

Files with missing lines Patch % Lines
datafusion/spark/src/function/datetime/next_day.rs 89.83% 9 Missing and 3 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #23892   +/-   ##
=======================================
  Coverage   81.93%   81.94%           
=======================================
  Files        1136     1136           
  Lines      429152   429199   +47     
  Branches   429152   429199   +47     
=======================================
+ Hits       351633   351694   +61     
+ Misses      56475    56461   -14     
  Partials    21044    21044           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

pull Bot pushed a commit to TCeason/arrow-datafusion that referenced this pull request Aug 15, 2026
…pache#23893)

## Which issue does this PR close?

- Closes #.

No issue tracks this. It is tooling, proposed for discussion. Related:
apache#15914 (the
`datafusion-spark`
epic) and apache#23887
(version-specific
SLT expectations, which this skill's cross-version findings link to).

## Rationale for this change

The `datafusion-spark` crate reimplements Spark's built-in expressions.
Correctness against Spark is the entire point of the crate, but there is
no
routine process for checking an implementation against the Spark source,
and no
record of which functions have been checked.

The testing situation makes this harder than it looks. Comet, the
sibling
project, tests by differential execution: it runs each query in both
engines
and compares, so expected values never need to be written down.
DataFusion's
`.slt` files hard-code every expected result, with no Spark in the loop.
Two
consequences follow:

1. Someone has to determine what Spark actually returns before writing
the
expectation. Reading `nullSafeEval` and predicting the output is how
wrong
   golden values get written, and once written they look like verified
   behavior.
2. A hard-coded expectation encodes exactly one Spark version, so every
file
   has to be written with one version in mind.

Many files under `test_files/spark/` were machine-generated from Sail's
gold
data and their values were never checked against a running Spark. An
existing
passing expectation is a claim, not evidence.

## What changes are included in this PR?

One file: `.ai/skills/audit-datafusion-spark-expression/SKILL.md`,
following
the `.ai/skills/` convention already described in the repository
`CLAUDE.md`.
No production code changes.

The skill audits one function per invocation, in eight steps: read the
Spark
source across 3.5.8, 4.0.4, 4.1.3 and 4.2.0 to detect cross-version
divergence;
harvest Spark's own unit tests and `sql-tests/results/*.sql.out` golden
files;
review the Rust implementation's signature and coercion, return
nullability,
null propagation, overflow, type dispatch and ANSI handling; cross-check
Comet
and Sail; review existing SLT coverage; build a gap matrix; establish
ground
truth; then apply findings.

Three decisions are worth calling out for review:

**Spark 4.2.0 is the declared baseline.** Older versions are read only
to
detect that behavior changed, never to set an expectation.

**Expected values come from a real Spark.** The skill sets up a
`pyspark==4.2.0`
venv and runs every candidate query through it under both ANSI modes,
including
expectations that already exist in the file. If PySpark cannot be
installed the
audit continues from source-derived values, but every such expectation
is
marked unverified rather than presented as observed.

**`--complete` is forbidden for files under `test_files/spark/`.** That
mode
fills expectations with whatever DataFusion currently returns, which on
an
audit would silently cement the bug being audited as the golden answer.

The skill also establishes a convention for divergences it cannot fix: a
commented-out SLT query carrying the correct Spark result and a link to
the
tracking issue, where uncommenting it is the contract for verifying a
fix.
That convention is new to this tree and is the piece most worth arguing
about.

## Are these changes tested?

The skill was run end to end against `next_day`, which is how it was
debugged.
That run produced apache#23892 and
issues
apache#23889, apache#23890 and apache#23891.

Running it surfaced eleven defects in the skill itself, which are fixed
here.
Two are worth citing as evidence that the exercise was not circular:

- The instruction to `unset SPARK_HOME` sat in a different code block
from the
command that needed it, so it never took effect and the run reproduced
the
exact `TypeError: 'JavaPackage' object is not callable` failure the text
was
  written to prevent.
- The ground-truth harness reported a Python `ValueError` as though it
were a
Spark error, which would have written a fabricated divergence into the
`.slt`
  as observed fact.

Review of the resulting `next_day` work then found a genuine
SQL-reachable
panic in the audited function that the audit had missed, and the gap
that hid
it, which is now a rule in the skill: cross the argument shapes with the
NULL
cases and with both ANSI modes rather than checking each axis alone.

The skill carries no automated test. Its verification is that following
it
produces a correct audit, which is what apache#23892 demonstrates.

It has since been run a second time, against `pmod`, which found three
real
bugs now fixed in apache#23898 and
four more
divergences filed as apache#23894, apache#23895, apache#23896 and apache#23897. That run
surfaced four
further skill gaps, folded in here. The most valuable is a section on
Arrow
kernel semantics not matching Java's: total-ordered float comparison
hiding
`-0.0`, checked rather than wrapping arithmetic, and Java's
`byte`/`short`
promotion to `int` each caused one of the `pmod` bugs, and the skill had
said
nothing about any of them.

The `datafusion-spark` README already points contributors to Comet and
Sail
when implementing a function. This is the same idea applied to checking
one.
imtherealnaska pushed a commit to imtherealnaska/datafusion that referenced this pull request Aug 16, 2026
…pache#23893)

## Which issue does this PR close?

- Closes #.

No issue tracks this. It is tooling, proposed for discussion. Related:
apache#15914 (the
`datafusion-spark`
epic) and apache#23887
(version-specific
SLT expectations, which this skill's cross-version findings link to).

## Rationale for this change

The `datafusion-spark` crate reimplements Spark's built-in expressions.
Correctness against Spark is the entire point of the crate, but there is
no
routine process for checking an implementation against the Spark source,
and no
record of which functions have been checked.

The testing situation makes this harder than it looks. Comet, the
sibling
project, tests by differential execution: it runs each query in both
engines
and compares, so expected values never need to be written down.
DataFusion's
`.slt` files hard-code every expected result, with no Spark in the loop.
Two
consequences follow:

1. Someone has to determine what Spark actually returns before writing
the
expectation. Reading `nullSafeEval` and predicting the output is how
wrong
   golden values get written, and once written they look like verified
   behavior.
2. A hard-coded expectation encodes exactly one Spark version, so every
file
   has to be written with one version in mind.

Many files under `test_files/spark/` were machine-generated from Sail's
gold
data and their values were never checked against a running Spark. An
existing
passing expectation is a claim, not evidence.

## What changes are included in this PR?

One file: `.ai/skills/audit-datafusion-spark-expression/SKILL.md`,
following
the `.ai/skills/` convention already described in the repository
`CLAUDE.md`.
No production code changes.

The skill audits one function per invocation, in eight steps: read the
Spark
source across 3.5.8, 4.0.4, 4.1.3 and 4.2.0 to detect cross-version
divergence;
harvest Spark's own unit tests and `sql-tests/results/*.sql.out` golden
files;
review the Rust implementation's signature and coercion, return
nullability,
null propagation, overflow, type dispatch and ANSI handling; cross-check
Comet
and Sail; review existing SLT coverage; build a gap matrix; establish
ground
truth; then apply findings.

Three decisions are worth calling out for review:

**Spark 4.2.0 is the declared baseline.** Older versions are read only
to
detect that behavior changed, never to set an expectation.

**Expected values come from a real Spark.** The skill sets up a
`pyspark==4.2.0`
venv and runs every candidate query through it under both ANSI modes,
including
expectations that already exist in the file. If PySpark cannot be
installed the
audit continues from source-derived values, but every such expectation
is
marked unverified rather than presented as observed.

**`--complete` is forbidden for files under `test_files/spark/`.** That
mode
fills expectations with whatever DataFusion currently returns, which on
an
audit would silently cement the bug being audited as the golden answer.

The skill also establishes a convention for divergences it cannot fix: a
commented-out SLT query carrying the correct Spark result and a link to
the
tracking issue, where uncommenting it is the contract for verifying a
fix.
That convention is new to this tree and is the piece most worth arguing
about.

## Are these changes tested?

The skill was run end to end against `next_day`, which is how it was
debugged.
That run produced apache#23892 and
issues
apache#23889, apache#23890 and apache#23891.

Running it surfaced eleven defects in the skill itself, which are fixed
here.
Two are worth citing as evidence that the exercise was not circular:

- The instruction to `unset SPARK_HOME` sat in a different code block
from the
command that needed it, so it never took effect and the run reproduced
the
exact `TypeError: 'JavaPackage' object is not callable` failure the text
was
  written to prevent.
- The ground-truth harness reported a Python `ValueError` as though it
were a
Spark error, which would have written a fabricated divergence into the
`.slt`
  as observed fact.

Review of the resulting `next_day` work then found a genuine
SQL-reachable
panic in the audited function that the audit had missed, and the gap
that hid
it, which is now a rule in the skill: cross the argument shapes with the
NULL
cases and with both ANSI modes rather than checking each axis alone.

The skill carries no automated test. Its verification is that following
it
produces a correct audit, which is what apache#23892 demonstrates.

It has since been run a second time, against `pmod`, which found three
real
bugs now fixed in apache#23898 and
four more
divergences filed as apache#23894, apache#23895, apache#23896 and apache#23897. That run
surfaced four
further skill gaps, folded in here. The most valuable is a section on
Arrow
kernel semantics not matching Java's: total-ordered float comparison
hiding
`-0.0`, checked rather than wrapping arithmetic, and Java's
`byte`/`short`
promotion to `int` each caused one of the `pmod` bugs, and the skill had
said
nothing about any of them.

The `datafusion-spark` README already points contributors to Comet and
Sail
when implementing a function. This is the same idea applied to checking
one.
Spark's NextDay has taken failOnError = SQLConf.get.ansiEnabled since at least
3.5, raising on an unparseable day-of-week name and returning NULL otherwise.
datafusion-spark ignored the flag and always returned NULL, with TODO comments
acknowledging the gap. It now honors datafusion.execution.enable_ansi_mode,
preserving Spark's per-row null intolerance: a NULL start date short circuits
to NULL before the name is validated.

One version-independent bug is fixed alongside it:

- SELECT next_day(<date literal>, <string column>) hit an unreachable
  catch-all and raised a hard error. The match over ColumnarValue shapes is
  now exhaustive.

The day-of-week parsing and the date arithmetic are split into
parse_day_of_week and next_date_for_day_of_week so the ANSI branch has a single
place to decide between raising and appending NULL.

This branch originally also fixed a panic for epoch days in 95026230..=95026236
by switching to checked_add_signed, which avoided the panic but returned NULL.
That panic has since been fixed on main by apache#24194, which computes the result on
the epoch day and returns the value Spark's Int arithmetic produces. This rebase
keeps main's behavior and drops the weaker fix; the tests and slt cases that
asserted NULL past chrono::NaiveDate::MAX now assert the epoch day instead.

The only version-specific change is the error message text, which now matches
Spark 4.2.0's ILLEGAL_DAY_OF_WEEK template rather than 3.5.8's older
IllegalArgumentException string. The remaining cross-version divergence, the
ANSI default flip and error class change, is not addressed here and is tracked
by apache#23890
@andygrove
andygrove force-pushed the next-day-spark-4-2-conformance branch from 5ed5d6a to 9b43895 Compare September 3, 2026 17:19
@andygrove

Copy link
Copy Markdown
Member Author

Rebased onto main. The conflict was substantive rather than textual, so noting the resolution here.

#24194 landed a fix for the same panic while this branch was open, and it closes #23891 — the issue this branch also addressed. The two fixes are not equivalent:

  • This branch used checked_add_signed, which avoids the panic but returns NULL once the next occurrence passes chrono::NaiveDate::MAX.
  • fix: prevent next_day panic on far-future start dates #24194 computes the result on the epoch day (days.checked_add(delta)), which returns the value Spark's Int arithmetic produces.

main's behavior is the correct one, so next_date_for_day_of_week now uses the epoch-day arithmetic and the weaker fix is dropped. Consequences:

What is left specific to this branch is the ANSI-mode handling and the missing (date literal, string column) argument shape.

Verified on the rebased branch: datafusion-spark 281 unit tests pass, and all 244 spark/ slt files pass. cargo fmt and cargo clippy -p datafusion-spark --all-targets -D warnings are clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

spark sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants