Skip to content

Do not treat truncated, NaN-free or zero bounds as exact column extremes - #140

Open
Vedin wants to merge 5 commits into
fix/cleanup-failed-writesfrom
rustice/inexact-string-bounds
Open

Vedin wants to merge 5 commits into
fix/cleanup-failed-writesfrom
rustice/inexact-string-bounds

Conversation

@Vedin

@Vedin Vedin commented Oct 28, 2026

Copy link
Copy Markdown

Stacked on #138 (base fix/cleanup-failed-writes); retarget to main once #138 merges.

Manifest bounds were turned into column statistics and pruning arrays as if they were exact extremes of the column. Four cases broke that assumption, and two of them give wrong results.

  1. String and binary bounds are truncated. Under the default truncate(16) metrics mode, the lower bound is a prefix of the smallest value and the upper bound an incremented prefix. As exact statistics, DataFusion answered MIN/MAX with the truncated prefixes. They are now Inexact.

  2. Float upper bounds exclude NaN (Iceberg spec), while Snowflake orders NaN above every number. Over {1.0, NaN, 1.0} Snowflake gives COUNT_IF(k > 2) = 1, MAX = NaN and order 1, 1, NaN. Trusting the upper bound broke all three:

    • k > 2 pruned the file;
    • MAX came from statistics as 1.0;
    • min = max turned the column into the constant 1.0.

    A float column's upper bound is now used only when the file's nan_value_counts entry is recorded as 0, in both the statistics and planning-time data-file pruning. A float partition's upper bound is used only when contains_nan is Some(false). Lower bounds stay usable.

  3. Value::into_any boxed the OrderedFloat wrapper. The bound arrays downcast to f32/f64, so every float bound was null and float columns never pruned files or manifests at planning time. It now boxes the plain float, which enables that pruning. Item 2 keeps it NaN-safe.

  4. Zero float bounds are not exact extremes. Parquet writers record a zero minimum as -0.0 and a zero maximum as +0.0 whatever the sign of the stored zeros. MIN over {0.0, 1.0} came back as -0 (Snowflake: 0). Such bounds are now Inexact.

Since float bounds now reach the planning-time predicates, manifest and data-file pruning keeps every container when the predicate fails to evaluate on the bounds (as Parquet row-group pruning does), instead of failing the query.

Cost:

  • Files written by this fork record no NaN counts, so their float columns lose upper-bound pruning and MAX from statistics.
  • String MIN/MAX now scan.

Not changed: a NaN with the sign bit set (-'NaN', inf - inf) sorts below every number in arrow's total order, unlike Snowflake. That is a separate, pre-existing execution difference.

Tests:

  • New unit tests for the bound statistics, both pruning paths and into_any. The NaN pruning tests fail with the guards removed.
  • cargo test -p datafusion_iceberg -p iceberg-rust passes, except integration_trino_rest, which fails starting its Trino container in this environment.
  • The Rustice end-to-end tests that pin the Snowflake results land with the pin bump.

🤖 Generated with Claude Code

Vedin and others added 5 commits October 27, 2026 19:44
Writers truncate string and binary bounds (the default metrics mode is
truncate(16)): the lower bound is a prefix of the minimum and the upper
bound an incremented prefix of the maximum. The table statistics marked
them exact, so DataFusion answered MIN/MAX over such a column from the
bounds without scanning and returned the truncated prefixes. They are now
inexact: still usable for pruning and estimates, never for answers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bound arrays downcast float bounds to f32 / f64, but into_any boxed the
OrderedFloat wrapper, so every float bound became null and float columns
never pruned files or manifests at planning time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Iceberg bounds exclude NaN, while NaN sorts above every number. A file
holding 1.0 and NaN has upper bound 1.0, so trusting it pruned the file
for k > 2, answered MAX(k) with 1.0 and read the column as the constant
1.0. Use a float column's upper bound only when the file's NaN count is
recorded as zero, and a float partition's only when contains_nan is false.
Lower bounds stay usable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Parquet row-group pruning keeps a row group whose bounds the predicate
cannot evaluate; Iceberg manifest and data-file pruning failed the query
instead. Float bounds now reach these predicates, so a cast that cannot
take a file's bound must not fail a query whose rows evaluate fine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Parquet writers record a zero minimum as -0.0 and a zero maximum as +0.0
whatever the stored zeros' sign, so MIN over {0.0, 1.0} came back as -0
from statistics (Snowflake: 0). Keep such bounds for pruning only. Test
that a NaN-free file keeps its float maximum and that into_any boxes
plain floats.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant