Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #23898 +/- ##
========================================
Coverage 81.93% 81.93%
========================================
Files 1136 1136
Lines 429152 429299 +147
Branches 429152 429299 +147
========================================
+ Hits 351633 351757 +124
- Misses 56475 56488 +13
- Partials 21044 21054 +10 ☔ 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#23987) ## Which issue does this PR close? - Closes apache#23894 ## Rationale for this change `datafusion-spark`'s `mod` routes through the shared `try_rem` helper in `datafusion/spark/src/function/math/modulus.rs`, which has two gaps in its zero-divisor handling (both verified against Spark 3.5.8 – 4.2.0 in the issue): 1. **ANSI mode, floating-point divisor.** `try_rem` delegates to Arrow's `rem` kernel in ANSI mode, but Arrow only reports division by zero for integer and decimal types. Floating-point divisors follow IEEE 754 and quietly produce `NaN`, while Spark raises `REMAINDER_BY_ZERO` for a zero divisor of any numeric type: ```sql set datafusion.execution.enable_ansi_mode = true; SELECT mod(10.5::float8, 0.0::float8); -- NaN, Spark raises ``` 2. **`-0.0` divisor, both modes.** The legacy path nulls out zero divisors via `eq(right, 0)`, but Arrow's floating-point comparisons use a total order in which `-0.0` is distinct from `0.0`, so a `-0.0` divisor goes unrecognised. Spark's `isZero` is a numeric comparison and treats `-0.0` as zero: ```sql SELECT mod(10.5::float8, -0.0::float8); -- NaN, Spark returns NULL (legacy) ``` ## What changes are included in this PR? `try_rem` now detects zero divisors itself, mirroring the shape apache#23898 established for `pmod`: - A new `is_zero` helper counts `-0.0` as zero for the floating-point types (via a `negative_zero` companion, same as apache#23898). - In ANSI mode, any row with a zero divisor raises `ArrowError::DivideByZero` — the same error Arrow's `rem` already produces for integers today, so the message stays uniform across types. The check is masked by the validity of the dividend because Spark's remainder expressions are null intolerant: a NULL dividend short-circuits to NULL before the divisor is validated, so `mod(NULL, 0)` must return NULL rather than raise. - Both modes substitute NULL for zero divisors before calling Arrow's `rem`, so the kernel never sees a zero divisor: legacy mode gets NULLs, and ANSI mode has already raised on the rows that required it. Note on overlap with apache#23898: that PR rewrites `pmod` to no longer use `try_rem` and adds identical `is_zero`/`negative_zero` helpers. This PR is independent of it — `mod` is fixed either way — but whichever lands second should dedupe the helpers in a rebase. Until apache#23898 lands, `pmod` also picks up the `-0.0` and ANSI floating-point zero-divisor fixes through the shared helper. Out of scope: apache#23897 (reproducing Spark's exact ANSI error text) is a repository-wide error-message policy question, as noted in that issue. ## Are these changes tested? Yes: - New unit tests in `modulus.rs`: ANSI floating-point zero divisor raises; `-0.0` divisor returns NULL in legacy mode and raises in ANSI mode; a NULL dividend with a zero divisor returns NULL in ANSI mode (integer and float). - New sqllogictest cases in `spark/math/mod.slt` covering the same behavior at SQL level. - Verified `cargo test -p datafusion-spark`, the `spark/math/mod.slt` and `spark/math/pmod.slt` sqllogictests, `./dev/rust_lint.sh`, and the extended workspace suite (ci profile with `avro,json,backtrace,extended_tests,recursive_protection,parquet_encryption`) — all green. ## Are there any user-facing changes? Only the bug fixes, and only for the Spark `mod`/`pmod` functions: in ANSI mode a floating-point zero divisor now raises instead of returning NaN, and a `-0.0` divisor is treated as zero in both modes (NULL in legacy mode, an error in ANSI mode), matching Spark. No API changes.
…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#23987) ## Which issue does this PR close? - Closes apache#23894 ## Rationale for this change `datafusion-spark`'s `mod` routes through the shared `try_rem` helper in `datafusion/spark/src/function/math/modulus.rs`, which has two gaps in its zero-divisor handling (both verified against Spark 3.5.8 – 4.2.0 in the issue): 1. **ANSI mode, floating-point divisor.** `try_rem` delegates to Arrow's `rem` kernel in ANSI mode, but Arrow only reports division by zero for integer and decimal types. Floating-point divisors follow IEEE 754 and quietly produce `NaN`, while Spark raises `REMAINDER_BY_ZERO` for a zero divisor of any numeric type: ```sql set datafusion.execution.enable_ansi_mode = true; SELECT mod(10.5::float8, 0.0::float8); -- NaN, Spark raises ``` 2. **`-0.0` divisor, both modes.** The legacy path nulls out zero divisors via `eq(right, 0)`, but Arrow's floating-point comparisons use a total order in which `-0.0` is distinct from `0.0`, so a `-0.0` divisor goes unrecognised. Spark's `isZero` is a numeric comparison and treats `-0.0` as zero: ```sql SELECT mod(10.5::float8, -0.0::float8); -- NaN, Spark returns NULL (legacy) ``` ## What changes are included in this PR? `try_rem` now detects zero divisors itself, mirroring the shape apache#23898 established for `pmod`: - A new `is_zero` helper counts `-0.0` as zero for the floating-point types (via a `negative_zero` companion, same as apache#23898). - In ANSI mode, any row with a zero divisor raises `ArrowError::DivideByZero` — the same error Arrow's `rem` already produces for integers today, so the message stays uniform across types. The check is masked by the validity of the dividend because Spark's remainder expressions are null intolerant: a NULL dividend short-circuits to NULL before the divisor is validated, so `mod(NULL, 0)` must return NULL rather than raise. - Both modes substitute NULL for zero divisors before calling Arrow's `rem`, so the kernel never sees a zero divisor: legacy mode gets NULLs, and ANSI mode has already raised on the rows that required it. Note on overlap with apache#23898: that PR rewrites `pmod` to no longer use `try_rem` and adds identical `is_zero`/`negative_zero` helpers. This PR is independent of it — `mod` is fixed either way — but whichever lands second should dedupe the helpers in a rebase. Until apache#23898 lands, `pmod` also picks up the `-0.0` and ANSI floating-point zero-divisor fixes through the shared helper. Out of scope: apache#23897 (reproducing Spark's exact ANSI error text) is a repository-wide error-message policy question, as noted in that issue. ## Are these changes tested? Yes: - New unit tests in `modulus.rs`: ANSI floating-point zero divisor raises; `-0.0` divisor returns NULL in legacy mode and raises in ANSI mode; a NULL dividend with a zero divisor returns NULL in ANSI mode (integer and float). - New sqllogictest cases in `spark/math/mod.slt` covering the same behavior at SQL level. - Verified `cargo test -p datafusion-spark`, the `spark/math/mod.slt` and `spark/math/pmod.slt` sqllogictests, `./dev/rust_lint.sh`, and the extended workspace suite (ci profile with `avro,json,backtrace,extended_tests,recursive_protection,parquet_encryption`) — all green. ## Are there any user-facing changes? Only the bug fixes, and only for the Spark `mod`/`pmod` functions: in ANSI mode a floating-point zero divisor now raises instead of returning NaN, and a `-0.0` divisor is treated as zero in both modes (NULL in legacy mode, an error in ANSI mode), matching Spark. No API changes.
…o handling Audit `pmod` against Apache Spark 4.2.0 and fix three divergences. Spark evaluates `(r + n) % n` with Java arithmetic, which wraps around for `int` and `long` and promotes `byte` and `short` to `int`. Arrow's checked `add` reported an overflow instead, so `pmod(-1, -2147483648)` errored where Spark returns `2147483647`. Use `add_wrapping`, and widen the narrow integer types before the addition so Java's operand promotion is reproduced. Arrow's `rem` only reports division by zero for integer and decimal types, so in ANSI mode a floating point zero divisor quietly produced `NaN` where Spark raises. Check the divisor directly, masked by the validity of the dividend so the null intolerant short circuit is preserved and `pmod(NULL, 0)` stays NULL. Arrow's comparison kernels order floating point values totally, so `-0.0` was neither recognised as a zero divisor nor kept out of the negative branch. Treat `-0.0` as zero when testing the divisor, and select the adjusted value only where the remainder is negative so a `-0.0` result keeps its sign. `pmod` no longer shares `try_rem` with `mod`, whose behavior is unchanged. Rebase note: apache#23987 landed the same ANSI-zero-divisor and `-0.0` fixes for `mod` while this branch was open, by rewriting the shared `try_rem` and adding `negative_zero` and `is_zero`. This rebase keeps main's `try_rem` and its helpers rather than the versions developed here, so `mod` is untouched; the `is_zero` helper is identical either way. What remains specific to this branch is the Java overflow and operand promotion handling, the `-0.0` result sign, and the `pmod` test coverage. The two test suites are kept in full.
9903028 to
b2a7d2c
Compare
|
Rebased onto #23987 fixed the ANSI zero divisor and
What remains specific to this branch is the Java overflow and operand promotion handling ( One correction while rebasing: the doc comment on Both test suites are kept in full — 29 Verified on the rebased branch: |
|
Heads up @andygrove — this now conflicts with main, and #24409 landing is the cause, so apologies for the extra work. I test-merged it against current main (76 commits behind). Conflicts in The two changes look complementary rather than overlapping. #24409 added One thing worth watching in the resolution. This PR uses The other two conflicts are mechanical: the import block, and Happy to push the rebase myself if that's useful, since my PR caused it — just say the word and I'll open it against your branch. Otherwise this is just a flag. Disclosure: written with AI assistance, reviewed by me before posting. |
Resolve conflicts with apache#24409 by keeping its decimal result type and null passthrough while retaining the wrapping add and Int8/Int16 widening for the (r + n) % n step.
|
Thanks @amitvijapur, merged |
Which issue does this PR close?
No issue tracked these three bugs; they were surfaced by an audit of
pmodagainst the Spark source and a live Spark 4.2.0. The divergences this PR does
not fix were filed by that audit and are referenced from the test file:
modhas the same zerodivisor and
-0.0bugs fixed here forpmodpmodreturns a widerdecimal type than Spark
pmodrejects stringarguments Spark implicitly casts
message is reported instead of Spark's
REMAINDER_BY_ZERORationale for this change
Spark computes
pmodas((r + n) % n)whenr < 0, wherer = a % n, usingJava arithmetic. Three divergences follow from not reproducing that exactly.
Integer overflow where Spark wraps. The shift used a checked add, so
pmod(-1, -2147483648)raised a hard error. Java wraps, and Spark returns2147483647.The width rule is subtle. Java promotes
byteandshortoperands toint,so those widths cannot overflow, while
intandlongare evaluated at theirown width and wrap. Both halves are observable:
The
TINYINTcase is what distinguishes the two rules: wrapping atInt8would give
126.A zero floating point divisor did not raise in ANSI mode. Arrow's
remkernel only errors for integer and decimal types, so the float path returned
NaNwhere Spark raises.-0.0divisors and results were mishandled. Arrow's comparison kernelsorder floats by total order, so
-0.0compared as less than0.0rather thanequal to it. A
-0.0divisor was therefore not recognised as a zero divisor,and an unconditional add flattened
-0.0results to0.0.What changes are included in this PR?
Scoped to
pmod.modand the sharedtry_remhelper are untouched.((r + n) % n)rather than(r + n). The secondmodulo is a no-op for a positive divisor but decides the negative case:
pmod(-7, -3)is-1, not-4.Int8andInt16are widened toInt32for the shift, reproducing Java'snumeric promotion; wider types use a wrapping add so overflow matches Spark.
In ANSI mode a zero divisor raises; otherwise it yields NULL. The check is
masked by the validity of the dividend, because Spark's
pmodis nullintolerant: a NULL dividend evaluates to NULL and never raises, even where
the divisor on that row is zero.
r < 0, rather than adding zero elsewhere,so
-0.0results survive.pmodno longer routes throughtry_rem.Are these changes tested?
Yes.
pmod.sltgrows by 238 lines and the unit tests inmodulus.rsby asimilar amount. Every expected value was observed by running the query against
a local
pyspark==4.2.0, not derived from reading the Spark source.Coverage crosses the argument shapes with NULL handling and with both ANSI
modes, rather than testing each axis alone. Added specifically:
Int8,Int16,Int32andInt64,including
i32::MIN % -1and thepmod(-1, TYPE_MIN)family-0.0divisors across integer, float and decimal types, in bothANSI modes
Fail-before evidence: stashing only
modulus.rsand re-running produces 11errors naming exactly the added queries — four "expected to fail but succeeded"
for the ANSI and
-0.0cases, and seven overflow errors.Verified with:
All 60 files under
test_files/spark/math/pass, not onlypmod.slt.