Two strategies, chosen by the table property write.delete.mode:
copy-on-write (the default, as in Java, and the only thing PyIceberg
can do) — every data file with a matching row is rewritten without those
rows and the original is marked DELETED. The result needs no delete files
at all, so every reader can read it, at the cost of rewriting whole files
to remove one row.
merge-on-read — the rows stay where they are and a delete file records
their positions. Under v3 that is a deletion vector: one Puffin
deletion-vector-v1 blob per data file, referenced by a manifest entry with
content=1, file_format=puffin, referenced_data_file and the
content_offset/content_size_in_bytes that must match the blob's footer
entry exactly. Under v2 it is a position delete file: a Parquet file
of (file_path, pos) pairs — the two reserved field ids 2147483546 and
2147483545 — sorted by path then position, one per partition.
Whichever strategy is in force, a data file whose rows are all deleted is
removed rather than emptied. That is the metadata-delete fast path: when the
scan's residual for a file is true — the partition predicate alone already
proves every row matches — the file is not read at all, and the delete costs
one manifest rewrite.
Merging. A deletion vector must contain every position deleted from its
data file, not just the new ones: the spec makes a new vector absorb the ones
it replaces, and a reader that finds a vector ignores every position delete
file for that data file. So a second delete against a file that already has a
vector reads the old vector, unions the new positions in, writes a fresh blob,
and marks the old vector's manifest entry DELETED. Position delete files
are left alone even when their positions are absorbed: one of them can cover
several data files, and removing it would resurrect rows in the others.
Overwrite is a delete and an append in one snapshot, always copy-on-write,
with operation=overwrite. dynamic_partition_overwrite derives the filter
from the data instead of taking one: it replaces exactly the partitions the
new rows land in, which needs no row-level work at all — a partition is
replaced by removing every data file in it.
Aliases
comptime PROP_DELETE_MODE = String("write.delete.mode")
comptime MODE_COPY_ON_WRITE = String("copy-on-write")
comptime MODE_MERGE_ON_READ = String("merge-on-read")
comptime POS_DELETE_SCHEMA_JSON = String("{\22schema-id\22:0,\22type\22:\22struct\22,\22fields\22:[{\22id\22:2147483546,\22name\22:\22file_path\22,\22required\22:true,\22type\22:\22string\22},{\22id\22:2147483545,\22name\22:\22pos\22,\22required\22:true,\22type\22:\22long\22}]}")