Metadata-only delete when a DELETE removes every live row of a data file#1178
Draft
paultmathew wants to merge 5 commits into
Draft
Metadata-only delete when a DELETE removes every live row of a data file#1178paultmathew wants to merge 5 commits into
paultmathew wants to merge 5 commits into
Conversation
A DELETE that removes all remaining rows of a data file now drops the file with a metadata-only manifest edit (marking its manifest entry DELETED) instead of writing a positional delete file / deletion vector. Detection is exact: at flush time the union of pre-existing and newly-deleted row positions is compared to the file's record_count. Delete files provably scoped to only the dropped data file (v3 deletion vectors and DuckDB's file-scoped positional deletes) are invalidated alongside it; multi-file deletes from other engines are left for their maintenance to reclaim. Reuses the existing IcebergManifestDeletes -> RewriteManifestFile commit path (also used by iceberg_rewrite_data_files); adds an invalidate-only AddSnapshot path for the case where a delete only drops whole files.
added 4 commits
July 14, 2026 16:25
When an UPDATE/MERGE's delete portion becomes a metadata-only whole-file delete, it produces no delete-file entries, so AddUpdateSnapshot was building an empty DELETE manifest and asserting in manifest_file::WriteToFile. Guard both the delete and data manifest so a manifest file is only added when it has entries, mirroring AddSnapshot.
A metadata-only whole-file delete marks the data file's manifest entry DELETED only at commit, so a read within the same transaction (e.g. UPDATE then SELECT before COMMIT) still saw the old file and double-counted. Collect the files invalidated by the transaction's alters and skip them when enumerating data files for transaction-local scans. Also add an UPDATE/MERGE metadata-only regression test (including the in-transaction case) and update two existing tests whose expectations assumed merge-on-read for whole-partition/whole-file deletes.
The test deleted a pre-seeded row whose data file layout varies by catalog: on some catalogs id=1 was a whole file (now a metadata-only delete -> no delete file -> COLUMN_DATA_SCAN), on Lakekeeper it was a partial delete (delete file -> ICEBERG_SCAN). Insert a known two-row file and delete one row so the delete is always a positional (merge-on-read) delete, restoring the catalog-independent "delete file present -> ICEBERG_SCAN" assertion.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What / why
I hit this building a delete + insert incremental event-stream pipeline (repeatedly deleting a partition and re-inserting it). Every
DELETEwrites merge-on-read delete files/DVs even when the predicate removes every live row of a data file, so the dead files pile up and every laterscan/MERGE/DELETEstill has to read them plus their delete files. When a delete fully empties a file, I'd rather drop it via metadata.This is Phase 1 and I'd like a sanity check on direction before going further.
Phase 1 (this PR) — drop fully-deleted files at flush time
We already scan every matching row, so at flush we know the exact deleted positions per file. When
existing + new deleted positions == record_count(positions are 0-based ordinals, so this holds exactly when no live row survives), invalidate the data file instead of writing a delete file; otherwise fall back to the current path.IcebergManifestDeletes->RewriteManifestFilecommit path (the oneiceberg_rewrite_data_filesuses).AddSnapshotpath for when a delete only drops whole files (no new manifest entries).referenced_data_file). Multi-file positional delete files written by other engines still apply to surviving files, so they're left for that engine's maintenance to reclaim once dangling.Phase 2 (follow-up) — skip the scan via strict metrics
On top of Phase 1, add a strict metrics evaluator (the strict dual of the inclusive
IcebergPredicate::MatchBounds) to check per-file stats against the predicate before scanning, so provably-fully-covered files skip the scan entirely; partial files keep the MOR path. Best-effort with a safe fallback.Tests
Fixture tests under
.../catalog_agnostic/delete/:test_metadata_only_full_file_delete.test— partitioned v2 (positional) + v3 (DV): whole-partition delete writes no delete file; partial delete does.test_metadata_only_unpartitioned.test— multiple/single data files, whole vs partial, and a file emptied across two deletes (existing deletes + new == record_count) which also drops the dangling file-scoped delete file.test_metadata_only_with_equality_deletes.test(gated onEQUALITY_DELETE_WRITES_ENABLED) — safety: with equality deletes present, deleting the remaining live rows is not mis-detected as a whole-file drop; it falls back to MOR and reads stay correct.Known limitations (deliberate for this phase)
deleted-data-files/deleted-records) are not decremented for the metadata drop (pre-existing accounting gap); reads/cardinality are correct.Questions
DELETEplan (Phase 1 at flush, Phase 2 at plan time), or opt-in?