Decode manifest bounds after schema promotion - #410
Conversation
|
Consider width-tolerant bound decoding instead of rewrite-time promotion The bug this fixes is real, but there's likely a much smaller approach that also generalizes better. Per the spec's binary single-value serialization, a bound's encoded width reflects the type it was written with (int = 4 bytes, long = 8), and int→long / float→double promotion is allowed without rewriting data. That means the decode side can absorb the mismatch directly: when decoding a long bound that is only 4 bytes, read an int and widen; likewise a double from 4 bytes reads a float. Roughly: This PR already carries the equivalent read-side logic in decode_bound. The key observation is that once the decoder is width-tolerant, the rewrite-time re-encoding is no longer needed: a rewritten manifest can keep the original 4-byte bounds and still be read correctly against the re-embedded (evolved) schema, since bounds are optional pruning statistics whose width the reader can always widen. That would let the PR drop Minimal shape: make the bound decoder width-tolerant (the decode_bound core you already have), and leave the writer path copying bounds as-is. Two smaller notes:
|
|
Thanks for the review. I reworked #410 around width-tolerant read-side decoding and removed the explicit rewrite-time bound promotion, date/timestamp conversion, and the unrelated DataFusion statistics changes. Complex/removed field IDs are now skipped. One detail required more than just decode_bound: a manifest embeds its write-time schema, while a scan can use a newer (or historical) schema. The scan path now passes its selected schema to the reader; partition decoding still uses the embedded schema. The reader builds the field-ID lookup once per manifest, which is why the internal doc-hidden index remains instead of rescanning a potentially wide schema for every bound. The first rewrite of an old manifest preserves its 4-byte numeric bounds. On a second rewrite, the typed entry has LONG/DOUBLE values and normal serialization may emit 8-byte bounds. The values remain valid and width-tolerant decoding handles both, but this is not byte-for-byte preservation through arbitrary rewrites. Do you require that stronger property? It would need a raw-bound sidecar or a raw Avro rewrite path; I left it out of this focused change pending your preference. I added an Avro regression for old-manifest/current-schema reads and the first rewrite, and verified the local spec, core, and DataFusion test suites plus clippy. |
Alternative: width-tolerant decode + promotion at the statistics boundaryThe three requirements this PR's tests pin down are exactly right — (1) bounds must surface as the promoted type so pruning works against the scan schema, (2) historical snapshots must keep their own types, (3) a rewrite must not corrupt bounds for either reader. I'd like to propose a smaller architecture that meets all three without threading a schema into the manifest reader. Core idea: keep decoding bounds against the manifest's embedded schema (unchanged reader), make that decode width-tolerant, and apply promotion at the two places that already hold the scan/snapshot schema — the DataFusion statistics conversion. Values then stay typed at their stored width everywhere in between, so re-serialization round-trips bytes exactly and the rewrite path needs no special mode. Changes (4 files, roughly 45 lines of implementation):
Why the three requirements still hold:
What this would remove from the PR: One corner to be aware of: a manifest that already carries the wide-schema/narrow-bytes mismatch decodes wide ( Suggested test coverage (largely a relocation of what this PR already tests): narrow-under-promoted-schema decode incl. nested-skip and unknown-id skip; unknown-width rejection; stored-width round-trip in both directions; one serialized entry decoded via the old schema → Separately valuable from this PR regardless of direction: the nested-field-id index (top-level |
27d21df to
0e86a57
Compare
|
Thanks for the simpler boundary-based approach. I replaced the PR with that design in 0e86a57: manifests decode against their embedded schema, and DataFusion promotes bounds when consuming statistics/pruning. Independent review also identified mixed partition specs and dropped historical partition source fields, so the patch conservatively disables only incompatible summaries rather than risking false pruning or failed scans. Focused tests and Rust 1.95 Clippy pass; the PR description has the exact results. |
|
Looks great, thanks a lot! |
Summary
This replaces the earlier selected-schema threading and removes the public
ManifestFieldTypeIndex, extra entry constructors, and rewrite-specific reader mode from this PR. The nested-field index and writer error-propagation cleanup can be considered separately.Validation
cargo +1.95.0 test -q -p iceberg-rust-spec(174 active passed; upstream ignored tests unchanged)cargo +1.95.0 test -q -p iceberg-rust --lib(166 active passed)cargo +1.95.0 test -q -p datafusion_iceberg --lib(49 passed)cargo +1.95.0 clippy -p iceberg-rust-spec -p datafusion_iceberg --all-targets -- -D warningsgit diff --checkRust 1.98 Clippy currently reports pre-existing
useless_borrows_in_formattingwarnings in unrelated upstream files; the pinned validation above uses Rust 1.95.