feat(scan): apply Iceberg v3 deletion vectors on read - #77
Conversation
Decode deletion-vector-v1 Puffin blobs (roaring bitmap) and feed the
decoded positions into the same RowSelection/ParquetAccessPlan union point
the positional-delete path uses, so DVs compose with projection/filter/
LIMIT/pruning identically. Databricks-UniForm v3 tables now return correct
post-delete rows instead of silently pre-delete rows.
- New deletion-vector-v1 decoder (BE length/magic/CRC, portable roaring,
cardinality validation, fail-loud redacted errors).
- Normalized interned per-shard wire {deleteFiles, dataFiles}: each physical
delete file/container interned once, df-indexed refs carry optional
offset/length for blob-addressed DVs. Retires FileEntryWire/DeleteFileRef/
DeleteFileContentType. JoinSpec.files reuses the same shape.
- Manifest walk sources DV coordinates (plan_files drops them); classify
now accepts Puffin position deletes, still rejects equality/ORC/Avro.
- Per-data-file dispatch on delete type; Puffin container opened once per
shard and reused across its data files; referenced-data-file cross-checked.
- Exclude DV Puffin containers from positional refs: iceberg-rust 0.10 also
surfaces the DV file in FileScanTask.deletes, so the manifest walk stays
authoritative and the container is applied once (as a DV), not twice.
- Fail loud when a DV references a data file matching no snapshot file.
- Spark fixtures: positive mor_dv, mixed positional+DV mor_mixed, ORC
unsupported; new deletion-vector E2E matrix; retarget positional fail-loud
test to ORC.
Bumps lakehouse-engine to 0.25.0.
Closes #12
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge the 7 feature deltas into the permanent spec library and archive the plan to specs/_recorded/2026-07-07-add-deletion-vector-application. - NEW: datafusion-scan/scan-execution-deletion-vectors, packaging/deletion-vector-fixtures, packaging/e2e-harness-deletion-vectors - CHANGED: vs-adapter/pushdown-file-pruning, datafusion-scan/scan-execution-spec-reconstitution, datafusion-scan/scan-execution-positional-deletes, packaging/e2e-harness-positional-deletes - Promoted ADR-092..096 (DV decoder, shared union point, manifest-sourced DV refs incl. the FileScanTask.deletes E2E correction, fail-loud validation, normalized interned wire). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review synthesis — deletion-vector planning & format detectionFocused review of how the planning phase acquires positional deletes + DVs and how format is How it works today
Findings (ranked)
|
Deep dive: the double manifest read (finding #4) — problem, root cause, directionsRecording the exploration so the analysis isn't lost. No decision made yet — this lays out the ProblemPlanning does two metadata passes:
Two S3 metadata passes per query, in tension with the "resolve metadata once per query" rule. It's Root cause (iceberg-rust 0.10, tag
|
| Source of the fields | Planning | Scan |
|---|---|---|
| Manifest walk (today) | double read | can be footer-free (not currently exploited) |
file_format only, upstream |
single pass | must read footer |
all four, upstream on FileScanTaskDeleteFile |
single pass | footer-free direct read |
This is the argument for the four-field ask over file_format-only: the coordinates aren't
needed for planning correctness, they're what makes the scan footer-free.
Upstream status (don't wait on it)
- EPIC #2411 (v3 support) lists "DV read
support" and "Scanner integration" as open. Umbrella: #630. - PR #2681 "apply V3 deletion vectors on
read" — its commit "carry file_format + referenced_data_file" onscan/task.rsis exactly this
fix, and it reads the blob viacontent_offset/content_size_in_bytes(direct access). But it
is stacked on #2678 →
#2203 (RowDelta write side), all OPEN,
CONFLICTING, 0 reviews. PR #2414 (DV blob
decoder, ≈ ourdeletion_vectors.rs) stalled since June. Realistically not landing in a pinnable
release soon. - Clean future exit: when #2681 (or just its field-carrying slice) lands, we can delete our
manifest walk anddeletion_vectors.rs/puffin.rsand letplan_filesdo it in one pass.
Worth// TODO(iceberg-rust#2681)markers now so the removal is signposted.
On the table (not decided)
A) the delete-manifest-only gate removes the double read for the common case now; (B) the
direct-access scan removes the footer read; (C) a narrow four-field upstream PR would collapse both
into a genuine single pass. (A) pairs naturally with the module extraction in review finding #2.
Summary
Applies Apache Iceberg v3 deletion vectors (
deletion-vector-v1roaring-bitmap Puffin blobs) on read, closing the deletion-vector half of #11 (Closes #12). Databricks-UniForm v3 tables and v2→v3-migrated tables now return correct post-delete rows instead of silently pre-delete rows.A decoded DV feeds the same
RowSelection/ParquetAccessPlanunion point the positional-delete path uses, so DVs compose with projection/filter/LIMIT/pruning for free, and a shard may mix positional-delete-backed and DV-backed data files.Follow-up to #72 (positional deletes); reconciled onto
mainafter #71 (broadcast join) merged.What changed
deletion-vector-v1decoder (scan/deletion_vectors.rs): BE length +D1 D3 39 64magic + portable roaring vector + BE CRC-32, validated against the declaredcardinality. Fails loud (credential-redacted) on any magic/CRC/cardinality/truncation mismatch. Uses the existingroaringdep +crc32fast(verified as the correct CRC-32/ISO-HDLC variant).{deleteFiles, dataFiles}(scan/spec.rs): each physical delete file/container interned once per shard;df-indexed refs carry optionaloffset/lengthfor blob-addressed DVs. RetiresFileEntryWire/DeleteFileRef/DeleteFileContentType.JoinSpec.filesmigrated to the same shape (reconciliation with JOIN pushdown Phase 1: broadcast inner equi-join (BL-001) #71's broadcast-join block).adapter/pushdown.rs): manifest walk sources DV coordinates (plan_filesdrops them);classify_manifest_fileaccepts Puffin position deletes, still rejects equality/ORC/Avro; fails loud when a DV references a data file matching no snapshot file.scan/puffin.rs,positional_deletes.rs): per-data-file dispatch on delete type; Puffin container opened once per shard and reused; blob's referenced-data-file cross-checked against the applied data file.scripts/spark-fixtures/,tests/): positivemor_dv, mixed positional+DVmor_mixed, ORC-unsupported fixture; new deletion-vector E2E matrix; positional fail-loud test retargeted to ORC.lakehouse-engine0.24.0 → 0.25.0.A note on the interesting bug
Host tests passed but E2E against a real Spark v3 fixture caught a genuine integration bug: iceberg-rust 0.10 does surface the Puffin DV file in
FileScanTask.deletes(the plan assumed it didn't), so the container was emitted as both a mis-typedPOS_DEL(Parquet) ref and aDVref — the POS_DEL ref then tried to parse the Puffin footer as Parquet ("Corrupt footer"). Fixed by excluding manifest-collected DV container paths from the positional refs; the manifest walk stays authoritative for DVs. (FileScanTaskDeleteFileexposes no file-format field, so path-exclusion is the robust fix.)Verification
make cross-musl-udf-build→ exit 0 (release.so, v0.25.0)cargo test→ 457 lib + all integration, 0 failurescargo clippy --all-targets+cargo fmt --check→ cleanmake test-e2e(live Exasol + MinIO + Iceberg REST + Spark) → 83 passed, 0 failed (capability 8, count_distinct 6, deletion_vectors 9, join 6, positional_deletes 11, scan 43)Spec deltas recorded into the permanent library (
spec(record)commit); ADR-092..096 promoted.🤖 Generated with Claude Code