Skip to content

Order -0.0 below 0.0 in data file metrics bounds - #4331

Merged
Fokko merged 2 commits into
apache:mainfrom
AP24110011695:fix/stats-aggregator-negative-zero-bounds
Nov 12, 2026
Merged

Fokko merged 2 commits into
apache:mainfrom
AP24110011695:fix/stats-aggregator-negative-zero-bounds

Conversation

@AP24110011695

Copy link
Copy Markdown
Contributor

What's changed

StatsAggregator.update_min/update_max aggregate parquet row-group statistics into DataFile.lower_bounds/upper_bounds. On a tie between +0.0 and -0.0, min(val, current) and max(val, current) keep the first argument — so whichever zero arrives last wins:

  • a late -0.0 lowers an existing +0.0 upper bound (e.g. row group 1 contains +0.0, a later row group's max is -0.0),
  • a late +0.0 raises an existing -0.0 lower bound.

The serialized bounds then order above a value that is present in the file, violating the two's-comparable bound ordering.

#4324 fixed the same class in the partition-field summary path (PartitionFieldStats.update in manifest.py); this applies the equivalent guard to the data-file metrics path. The argument order here is flipped (min(val, current) vs min(self._min, value)), so the guarded directions are mirrored, and the guard is split across the two update methods. Verified against #4324's pinned bar: for both arrival orders the bounds must serialize as lower = to_bytes(-0.0), upper = to_bytes(+0.0).

NaN needs no handling here: pyarrow row-group statistics exclude NaN (has_min_max is False for all-NaN chunks), so the aggregator never sees it on this path.

Test

test_stats_aggregator_orders_negative_zero_below_zero — parametrized over both arrival orders and FloatType/DoubleType, asserting the serialized bounds match the #4324 canonical pair. Verified failing-first: 4/4 fail on main (533a617) and pass with the guard; the existing StatsAggregator tests (18) still pass.

AI disclosure: code and tests drafted with AI assistance, verified locally by running the full failing-first cycle.

StatsAggregator.update_min/update_max feed DataFile lower/upper bounds,
but min(val, current) and max(val, current) keep the first argument on a
tie, so whichever zero arrives last wins: a late -0.0 lowers an existing
+0.0 upper bound, and a late +0.0 raises an existing -0.0 lower bound.
The serialized bounds then order above a value present in the file.

Mirror the fix that apache#4324 applied to partition field summaries: restore
the stored bound when a zero arrives that would invert the -0.0 < 0.0
ordering. The argument order here (val first) flips which direction
needs the guard compared to manifest.py.

Verified failing-first: the new tests fail on main for both arrival
orders and both float types, and pass with the guard.

Signed-off-by: Ayush Kumar Saha <225407166+AP24110011695@users.noreply.github.com>

@Fokko Fokko left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good one @AP24110011695, I think we can simplify a bit. Let me know what you think

Comment thread pyiceberg/io/pyarrow.py Resolved
Comment thread pyiceberg/io/pyarrow.py Resolved
Flip the min/max argument order so a tie keeps the existing value (the
same shape as PartitionFieldStats.update in manifest.py), which lets the
guard only handle an incoming zero of the wrong sign and drops the
previous-value capture. Behavior verified unchanged: the regression
tests and all zero-arrival interleavings still produce the canonical
-0.0-below-0.0 bounds.

Signed-off-by: Ayush Kumar Saha <225407166+AP24110011695@users.noreply.github.com>
@AP24110011695

Copy link
Copy Markdown
Contributor Author

Applied both suggestions as-is (2e7bd68) — flipping the argument order is clearly better: the tie now keeps the existing value exactly like PartitionFieldStats.update, the guard reads as "an incoming zero of the wrong sign corrects the bound", and previous is gone. You were also right about the comment — the old one was accurate for the code I should have written.

Verified unchanged behavior: all 22 StatsAggregator tests pass, and I re-probed the zero interleavings (both two-value orders plus (0.0, -0.0, 0.0) and (-0.0, 0.0, -0.0)) — every one produces the canonical lower = to_bytes(-0.0), upper = to_bytes(+0.0) pair.

@Fokko Fokko left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @AP24110011695 for the quick follow up, LGTM 🚀

@Fokko
Fokko added this pull request to the merge queue Nov 12, 2026
Merged via the queue into apache:main with commit aeaddb1 Nov 12, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants