Skip to content

fix(spark): correct pmod overflow, ANSI zero divisor and negative zero handling - #23898

Open
andygrove wants to merge 2 commits into
apache:mainfrom
andygrove:audit-pmod
Open

andygrove wants to merge 2 commits into
apache:mainfrom
andygrove:audit-pmod

Conversation

@andygrove

Copy link
Copy Markdown
Member

Which issue does this PR close?

  • Closes #.

No issue tracked these three bugs; they were surfaced by an audit of pmod
against 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:

Rationale for this change

Spark computes pmod as ((r + n) % n) when r < 0, where r = a % n, using
Java 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 returns
2147483647.

The width rule is subtle. Java promotes byte and short operands to int,
so those widths cannot overflow, while int and long are evaluated at their
own width and wrap. Both halves are observable:

spark-sql> SELECT pmod(CAST(-1 AS INT), CAST(-2147483648 AS INT));
2147483647
spark-sql> SELECT pmod(CAST(-2 AS TINYINT), CAST(-128 AS TINYINT));
-2

The TINYINT case is what distinguishes the two rules: wrapping at Int8
would give 126.

A zero floating point divisor did not raise in ANSI mode. Arrow's rem
kernel only errors for integer and decimal types, so the float path returned
NaN where Spark raises.

-0.0 divisors and results were mishandled. Arrow's comparison kernels
order floats by total order, so -0.0 compared as less than 0.0 rather than
equal to it. A -0.0 divisor was therefore not recognised as a zero divisor,
and an unconditional add flattened -0.0 results to 0.0.

spark-sql> SELECT pmod(CAST(10.5 AS DOUBLE), CAST(-0.0 AS DOUBLE));
-- ANSI on:  [REMAINDER_BY_ZERO] Remainder by zero.
-- ANSI off: NULL

What changes are included in this PR?

Scoped to pmod. mod and the shared try_rem helper are untouched.

  • The shift is evaluated as ((r + n) % n) rather than (r + n). The second
    modulo is a no-op for a positive divisor but decides the negative case:
    pmod(-7, -3) is -1, not -4.
  • Int8 and Int16 are widened to Int32 for the shift, reproducing Java's
    numeric promotion; wider types use a wrapping add so overflow matches Spark.
  • The zero divisor check is applied to floats as well as integers and decimals.
    In ANSI mode a zero divisor raises; otherwise it yields NULL. The check is
    masked by the validity of the dividend, because Spark's pmod is null
    intolerant: a NULL dividend evaluates to NULL and never raises, even where
    the divisor on that row is zero.
  • The shift is selected only where r < 0, rather than adding zero elsewhere,
    so -0.0 results survive.
  • pmod no longer routes through try_rem.

Are these changes tested?

Yes. pmod.slt grows by 238 lines and the unit tests in modulus.rs by a
similar 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:

  • the full overflow boundary for Int8, Int16, Int32 and Int64,
    including i32::MIN % -1 and the pmod(-1, TYPE_MIN) family
  • zero and -0.0 divisors across integer, float and decimal types, in both
    ANSI modes
  • NULL dividend paired with a zero divisor, which must not raise under ANSI
  • negative divisors, which exercise the second modulo

Fail-before evidence: stashing only modulus.rs and re-running produces 11
errors naming exactly the added queries — four "expected to fail but succeeded"
for the ANSI and -0.0 cases, and seven overflow errors.

Verified with:

cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --test sqllogictests -- spark/math/pmod
cargo test -p datafusion-spark --lib modulus

All 60 files under test_files/spark/math/ pass, not only pmod.slt.

@codecov-commenter

codecov-commenter commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.27451% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.93%. Comparing base (140c7c5) to head (96c88e1).

Files with missing lines Patch % Lines
datafusion/spark/src/function/math/modulus.rs 86.27% 7 Missing and 14 partials ⚠️
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.
📢 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.
haohuaijin pushed a commit to haohuaijin/arrow-datafusion that referenced this pull request Aug 16, 2026
…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.
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.
imtherealnaska pushed a commit to imtherealnaska/datafusion that referenced this pull request Aug 16, 2026
…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.
@andygrove

Copy link
Copy Markdown
Member Author

Rebased onto main. The conflict overlapped with #23987, so noting the resolution here.

#23987 fixed the ANSI zero divisor and -0.0 handling for mod while this branch was open, by rewriting the shared try_rem and adding negative_zero and is_zero. This branch had developed its own versions of the same three things for pmod. The overlap is resolved in favor of main:

  • try_rem — main's version is kept. It handles the zero divisor of any numeric type, treats -0.0 as zero, and masks the ANSI check by the validity of the dividend. The version on this branch was the older one that delegated straight to Arrow's rem under ANSI. mod is therefore untouched by this PR.
  • is_zero — byte-identical in both; main's is kept.
  • negative_zero — same implementation. Kept main's, with the doc comment reworded to cover is_negative too.

What remains specific to this branch is the Java overflow and operand promotion handling (add_wrapping plus widening Int8/Int16), preserving the sign of a -0.0 result by selecting the adjusted value only where the remainder is negative, and the pmod test coverage.

One correction while rebasing: the doc comment on spark_pmod said it does not share try_rem because it needs to treat -0.0 as a zero divisor and raise on float zero divisors under ANSI. Since #23987 that is no longer a difference. The reason that still holds is that pmod needs the zero-divisor-masked divisor again for the (r + n) % n step, which try_rem does not return, so the comment now says that instead.

Both test suites are kept in full — 29 modulus unit tests, including main's *_negative_zero_divisor_*, *_ansi_float and *_ansi_null_dividend cases alongside this branch's test_pmod_integer_boundaries, test_pmod_negative_zero_result and test_pmod_zero_divisor_by_type. In pmod.slt, main's legacy-mode -0.0 case was not covered here and is kept.

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

@amitvijapur

Copy link
Copy Markdown
Contributor

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 datafusion/spark/src/function/math/modulus.rs and datafusion/sqllogictest/test_files/spark/math/pmod.slt.

The two changes look complementary rather than overlapping. #24409 added pmod_decimal_result_type (Spark's Remainder rule for the decimal result type) plus null passthrough for the uncoerced DataType::Null that Coercible lets through, per #19458. This PR adds the ANSI zero-divisor raise masked by dividend validity, is_negative with -0.0 handling, and the Java integer-promotion arithmetic. Nothing in one subsumes the other.

One thing worth watching in the resolution. This PR uses add_wrapping deliberately, so pmod(-1, -2147483648) yields 2147483647 the way Spark does. Main now imports plain arrow::compute::kernels::numeric::add and calls it at modulus.rs:172. A resolution that takes main's import block wholesale drops the wrapping behaviour, and I don't think any existing test catches that, so it would go silently. The Int8/Int16 widening to Int32 has the same property.

The other two conflicts are mechanical: the import block, and is_negative versus pmod_decimal_result_type landing at the same insertion point.

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.
@andygrove

Copy link
Copy Markdown
Member Author

Thanks @amitvijapur, merged main in. The resolution keeps #24409's pmod_decimal_result_type, the DataType::Null passthrough and the decimal narrowing cast, and keeps add_wrapping plus the Int8/Int16 widening for the (r + n) % n step, so main's plain add import is dropped. The overflow cases are covered by test_pmod_integer_boundaries and the pmod(-1, -2147483648) family in pmod.slt, so losing the wrapping behavior would now fail tests. The branch's unit tests were updated for the new result_type parameter. All datafusion-spark unit tests and all 244 spark/ slt files pass.

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.

3 participants