Skip to content

fix(reader): widen promoted page-index bounds to the field type - #3521

Draft
rich7420 wants to merge 5 commits into
apache:mainfrom
rich7420:feat/page-index-widen-promoted-bounds
Draft

rich7420 wants to merge 5 commits into
apache:mainfrom
rich7420:feat/page-index-widen-promoted-bounds

Conversation

@rich7420

Copy link
Copy Markdown

Note

Draft, stacked on #3507. This branch includes the #3507 commits and a merge from main. Only the last two commits are new here: fix(reader): widen promoted page-index bounds to the field type and fix(reader): ignore NaN float page-index bounds.

After #3507 merges, I will rebase this branch onto main so that only these two commits remain, rerun the tests below, and mark the PR ready for review. Feedback on the approach is welcome in the meantime; please save detailed review until the PR is ready.

Which issue does this PR close?

What changes are included in this PR?

This implements the widening follow-up agreed in #3507. That PR prevents pruning based on incomparable page-index bounds, such as INT32 bounds read as a long field after an int -> long promotion. This PR widens supported bounds to the field type so inequality and IN predicates can prune these pages.

  • Signed INT32 bounds for a long field and FLOAT bounds for a double field are widened with Datum::promote_to, added in #3483.
  • UINT_32 bounds are stored as INT32 bit patterns ordered as unsigned values. The column descriptor’s sort order identifies unsigned bounds, which are zero-extended to long. If this produces min > max, as can happen with bounds written in signed order, both bounds are discarded.
  • If either FLOAT or DOUBLE bound is NaN, both bounds are discarded. Legacy writers may store NaN bounds for pages that also contain other values, as documented in the Parquet specification. Datum uses total_cmp for floating-point comparisons, so trusting these bounds can prune matching pages. Widening exposes promoted double fields to this risk too. #3502 added a related NaN guard for row-group inequality pruning.

Discarding bounds prevents pruning based on those bounds; null-count checks and other predicates still apply. Bounds that remain incomparable, such as INT32 bounds for a timestamp field, retain the conservative fallback from #3507.

Are these changes tested?

Yes. Evaluator tests in page_index_evaluator.rs use metadata from real Parquet files and assert exact row selections. The NaN cases substitute synthetic column indexes.

  • long field over INT32 pages: <, >=, < with a literal above i32::MAX, > with i32::MAX, and two-value IN.
  • double field over FLOAT pages: >, <=, and two-value IN.
  • long field over UINT_32 pages, including a value above i32::MAX and a page mixing 100 and 3_000_000_000: =, >, <, and IN.
  • FLOAT pages read as native float or promoted double, and DOUBLE pages read as native double: pages with a NaN minimum or maximum are kept for =, <, >, and two-value IN.
  • timestamp field over INT32 pages: the existing fallback test keeps every page for <, >=, =, and IN.

Helper tests also verify that signed-order UINT_32 bounds are discarded, correctly ordered bounds are widened, and both floating-point bounds are discarded if either is NaN while finite bounds are preserved.

Mutation checks confirmed that sign-extending UINT_32 bounds, removing the min > max check, and removing the NaN check each cause at least one test to fail. cargo test -p iceberg --lib expr::visitors and cargo test -p iceberg --lib arrow:: passed, as did cargo fmt --all -- --check and cargo clippy -p iceberg --all-targets -- -D warnings.

AI Disclosure

Drafted with Claude Code and reviewed with Codex. Review focus: whether zero-extension and the signed-order check are correct for UINT_32 columns, and whether discarding both bounds when either is NaN is conservative across predicates.

…e predicate

Page-index bounds are built with the table field type but the file's
physical literal. When the two differ, e.g. a long field read against
INT32 pages written before an int -> long promotion, a double field
against FLOAT pages, or a UINT_32 column mapped to long, the bounds
never compare with the predicate literal. Inequality and IN predicates
treated that as "cannot match" and skipped pages holding matching rows.

Keep a page whenever its bound cannot be compared with the literal: the
inequality check now treats an incomparable bound as a possible match,
and IN uses all(lt) / all(gt) instead of !any(ge) / !any(le).
A page holding both 100 and 3_000_000_000 has a raw signed max below its
min, which pins the unsigned column order that a later bound widening has
to zero-extend rather than sign-extend.
Page bounds written before an int -> long or float -> double promotion
are now widened with Datum::promote_to, so inequality and IN predicates
can prune these pages instead of keeping them all. UINT_32 bounds are
INT32 bits in unsigned order, so they are zero-extended; if the result
has min > max the writer used signed order and the page is kept.
Legacy writers may store NaN page bounds for pages holding other values.
Datum compares floats with total_cmp, so such a bound, now also produced
by widening FLOAT bounds to double, could prune a matching page. Drop
both bounds of a FLOAT or DOUBLE page when either is NaN, so the page
might match.
comphead added a commit to comphead/iceberg-rust that referenced this pull request Oct 26, 2026
A file written before an int -> long, float -> double, or decimal
precision promotion keeps its old physical type, while the predicate is
bound to the table type. The row filter cast the literal down to the
file's Arrow type, so an out-of-range literal became null and dropped
every row, and a double literal was rounded to f32.

Cast a numeric column to the table type before comparing instead, as
RecordBatchTransformer does for the output. Other columns keep the
literal cast. Page-index pruning is left to apache#3507 and apache#3521.

Part of apache#3122.
comphead added a commit to comphead/iceberg-rust that referenced this pull request Oct 27, 2026
Predicates are bound to the table schema, but the row filter evaluated
them on batches in the file's physical types. For a file written before
an int -> long, float -> double, or decimal precision promotion,
try_cast_literal cast the literal down to the file type, so an
out-of-range literal became null and dropped every row, and a double
literal was rounded to f32.

Convert the filter columns to the table schema with a
RecordBatchTransformer, as the reader does for its output, and make
try_cast_literal fail instead of returning null. Page-index pruning is
left to apache#3507 and apache#3521.

Part of apache#3122.

This branch has not been deployed

No deployments
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