t3hw opened a new pull request, #2342:
URL: https://github.com/apache/iceberg-rust/pull/2342

   
   
   ## Which issue does this PR close?
   
   No tracking issue filed yet. The symptom ("equality-delete scans scale
   quadratically in deletes × data rows") has been observed on our own
   uncompacted MOR workloads. Happy to open a tracking issue first if a
   committer prefers.
   
   ## What changes are included in this PR?
   
   This PR contains **two commits** that are tightly coupled. The squash
   merge collapses them; the split is purely to help reviewers read the
   perf change separately from the correctness fix it enables.
   
   ### Commit 1 — `perf(reader): replace O(N*M) equality-delete predicate tree 
with O(N+M) HashSet-based filter`
   
   The existing implementation builds a `BoundPredicate` AST with one
   leaf per delete record and evaluates it against every data row via
   parquet-rs `RowFilter`. For `N` data rows and `M` delete records this
   is **O(N · M)**: data-row count multiplies by delete-record count at
   evaluation time.
   
   This commit replaces the predicate tree with a
   `HashSet<EqDeleteKey>` of delete-key tuples — the same approach Java
   (`StructLikeSet`) takes. Delete records are collected into the hash set
   at parse time (O(M)); each data row is then checked with an O(1)
   lookup (O(N + M) total).
   
   Key pieces:
   
   - **`caching_delete_file_loader.rs`** —
     `parse_equality_deletes_record_batch_stream` returns an `EqDeleteSet`
     (`HashSet<EqDeleteKey>` + field metadata) instead of a `Predicate`.
     The balanced-binary-tree construction and `rewrite_not()` passes are
     eliminated. `Datum` already derives `Hash + Eq` (via `OrderedFloat`
     for floats), so no new trait impls are needed.
   - **`delete_filter.rs`** — `EqDelState::Loaded` holds
     `Arc<EqDeleteSet>` instead of `Predicate`. The new
     `build_equality_delete_sets()` groups delete files by their
     `equality_ids` field layout before unioning, preventing incorrect
     merges when different delete files use different equality column
     sets. Single-file groups return the cached `Arc` directly with no
     deep clone.
   - **`reader.rs`** — equality-delete filtering is decoupled from the
     scan predicate `RowFilter`. The scan predicate remains in the Parquet
     `RowFilter` pipeline (so page/row-group pruning is preserved).
     Equality deletes are applied as a lazy post-read `.map()` step on
     the record-batch stream via `apply_eq_delete_filter`, which reuses a
     single `EqDeleteKey` allocation across all rows to avoid per-row
     `Vec` allocations.
   
   ### Commit 2 — `fix(reader): auto-include equality delete key columns in 
projection`
   
   Consequence of commit 1: because the new `apply_eq_delete_filter`
   operates on arrow batches **after** the Parquet read (rather than
   through parquet-rs's `RowFilter`, which transparently widens its own
   `ProjectionMask`), the post-read filter needs the delete-key columns to
   be materialized. When a caller scans with
   `.select(["col_a", "col_b"])` against a table whose equality deletes
   are keyed on a column outside the projection (e.g. `id`), the filter
   raises:
   
   ```text
   Equality delete key column 'id' (field_id=1) not found in batch
   ```
   
   This commit widens the Parquet projection mask and
   `RecordBatchTransformer` to include any equality-delete key field IDs
   missing from the user's projection, then **strips those extra columns
   from the output batches** after deletes are applied — so the user sees
   only the columns they requested. This matches the behavior of Spark,
   Flink, and Trino, which all transparently widen the internal
   projection for delete evaluation.
   
   Note: the previous `RowFilter`-based path did not need this fix because
   parquet-rs's `ArrowPredicateFn` carries its own
   `ProjectionMask::leaves(parquet_schema, column_indices)` that reads the
   filter's columns independently of the user projection. The need for
   explicit auto-include is a direct consequence of moving deletes to a
   post-read filter.
   
   ### Measured impact
   
   The primary motivation is asymptotic: the old algorithm is O(N · M),
   so delete-heavy MOR tables (large `M`) pay a runtime penalty that
   scales with the data row count. The new algorithm is O(N + M).
   
   We verified that **the change is not a regression on low-delete-ratio
   workloads**. On a 36-hour pipeline-level benchmark (Iceberg table,
   ~71 M rows scanned, `M = 120 k` equality-delete records — a 0.17 %
   delete ratio, pipeline including downstream Rayon-parallel aggregation
   and DataFusion transforms), wall-clock changed from 59.15 s to 59.62 s
   (+0.8 %, within run-to-run noise). At this delete ratio the HashSet
   setup cost (~7.5 CPU-seconds of extra userland work) shows up as
   higher CPU utilization (166 % → 178 % average cores active) rather
   than added wall-clock.
   
   In the regime the change is actually designed for — large `M` — the
   setup cost is amortized by O(1) per-row lookups replacing
   O(M)-per-row evaluation of the predicate tree. An empirical
   measurement on a delete-heavy table would put hard numbers on that
   claim; we haven't run one yet.
   
   ### Memory trade-off
   
   `HashSet` peak-RSS grows with `M` (the number of delete records, not
   data rows). On the low-`M` matrix workload we observed ~0.3 GB RSS
   growth; a delete-heavy table with 10 M delete records would grow
   proportionally. Callers on memory-constrained systems with very
   delete-heavy MOR tables should size their heap accordingly.
   
   ## Are these changes tested?
   
   Yes.
   
   - **Unit tests for the parser and filter** live in
     `caching_delete_file_loader.rs` (group-by-equality-ids, single-file
     fast path, mixed layouts) and `delete_filter.rs`.
   - **Integration-style tests** in `reader.rs` cover:
     - Equality deletes where the user projection **does** include the
       delete-key column.
     - Equality deletes where the user projection **does not** include
       the delete-key column (the auto-include fix).
     - Mixed projections with other non-key columns to confirm the
       extra key column is stripped from output batches.
   - The `arrow::reader` lib test suite is green (43/43), and the full
     `cargo test -p iceberg --lib` passes (1229/1229).
   - `cargo clippy -p iceberg --all-features --lib --tests -- -D warnings`
     and nightly `cargo fmt --check` are clean.
   
   ---
   
   ## Notes for reviewers
   
   - **Diff size (~920 lines).** Above the 300–500 line guideline — I am
     happy to land the two commits as separate PRs, but the fix in commit
     2 only fires once commit 1 is in (otherwise the bug cannot trigger),
     so reviewing them together is easier. If a committer prefers them
     split, happy to open a follow-up PR for the fix.
   - **Semantics.** The `EqDeleteKey` tuple uses `Datum` equality (via
     `OrderedFloat` for floats), matching Iceberg's spec for equality
     deletes. NaN handling is left as the Rust-stdlib/OrderedFloat
     default, same as existing equality-predicate paths.
   - **`equality_ids` grouping.** The pre-union grouping by
     `equality_ids` is load-bearing: it prevents merging delete files
     that target different columns. A regression test specifically
     covers this.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to