Conversation
db5812e to
5ed5d6a
Compare
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
…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.
…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
5ed5d6a to
9b43895
Compare
|
Rebased onto #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:
main's behavior is the correct one, so
What is left specific to this branch is the ANSI-mode handling and the missing Verified on the rebased branch: |
Which issue does this PR close?
No single issue tracks these fixes. They were surfaced by an audit of
next_dayagainst the Spark source and a live Spark 4.2.0. The divergencesthis PR does not fix are filed separately and referenced from the test file:
next_dayrejectsSTRINGandTIMESTAMPstart dates that Spark acceptsin the ANSI default and error class between Spark 3.5 and 4.x
next_dayreturns NULLfor far-future start dates where Spark returns a value
Rationale for this change
Three problems, only one of which is version-related.
The ANSI flag was ignored. Spark's
NextDayhas takenfailOnError: Boolean = SQLConf.get.ansiEnabledsince at least 3.5, raising onan 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
ansiEnabledand theexception type, not the behavior the flag selects.
datafusion-sparkignoredthe flag entirely and always returned NULL, with
TODOcomments in the sourceacknowledging the gap.
A panic reachable from SQL.
next_date_for_day_of_weekusedNaiveDate + Duration.Date32Type::to_naive_date_optsucceeds up tochrono::NaiveDate::MAX(epoch day95026236), and the subsequent additionthen panics on overflow. Every epoch day in
95026230..=95026236panicked, forany weekday:
This is reachable from plain SQL and from any
Date32column containing such avalue.
A whole argument shape was unsupported.
SELECT next_day(<date literal>, <string column>)fell into anexec_err!catch-all and raised a hard error ona 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_weekuseschecked_add_signed, trading the panic forNULL. NULL still diverges from Spark, which computes on the epoch day as
Intwith 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.ColumnarValueshapes is now exhaustive over all fourcombinations, so the
(Scalar, Array)case works and the compiler enforcescoverage. The
_ => exec_err!catch-all is gone.datafusion.execution.enable_ansi_mode, matchinghow
math/abs.rsandmath/negative.rsread it. Spark's per-row nullintolerance 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 bothmodes. On the
(Array, Scalar)path this is expressed asdate_array.null_count() < date_array.len(), which is the closed form ofexactly when the row-wise path raises.
ILLEGAL_DAY_OF_WEEKtemplate rather than Spark 3.5.8's olderIllegalArgumentExceptiontext.spark_next_dayis split intoparse_day_of_weekandnext_date_for_day_of_week. The old code round-tripped"MO"through"MONDAY"intostr::parse::<Weekday>()with an unreachableErrarm; thereplacement is a single table matching
DateTimeUtils.getDayOfWeekFromStringcase for case.
Are these changes tested?
Yes.
next_day.sltgrows from 90 to 336 lines. Every expected value wasobserved by running the query against a local
pyspark==4.2.0rather thanderived 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
ColumnarValueshapes, eachcrossed with all-NULL, some-NULL and no-NULL inputs, each crossed with both
ANSI modes. The
(Array, Scalar)null-intolerance case in particular passes onthe 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-31including the entirepreviously-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: