Write deletion vectors as valid Puffin files (add file magic + footer)#1095
Conversation
WriteDeletionVectorFile wrote only the deletion-vector-v1 blob to the file and labelled it file_format=puffin, with content_offset=0 and file_size == blob size. That is not a valid Puffin file: the Puffin spec requires the layout `Magic Blob... Footer`, with the 4-byte magic (0x50 0x46 0x41 0x31, "PFA1") at the start and a Footer (Magic, FooterPayload JSON, FooterPayloadSize, Flags, Magic) at the end. The bare blob only works because every reader locates the vector via the manifest's content_offset/content_size_in_bytes and never parses the Puffin container; a spec-compliant Puffin reader (or any tool that enumerates blobs via the footer) cannot read these files. Add IcebergDeletionVectorData::ToPuffinFile, which wraps the blob in a proper container: leading magic + blob (at offset 4) + footer whose FileMetadata describes the single deletion-vector-v1 blob. Per the spec, snapshot-id and sequence-number are -1, and the blob carries the referenced-data-file and cardinality properties; the blob is uncompressed. content_offset is now 4. Reads are unchanged: they remain driven by the manifest's content_offset, so existing DV files written with content_offset=0 keep reading correctly (no migration). Footer framing (magic, little-endian FooterPayloadSize, flags) verified byte-identical to an Iceberg-Java-generated Puffin file; the blob is unchanged. Adds a test asserting a written deletion-vector file begins and ends with the Puffin magic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
You'll need to resurrect this to get the read path working and actually verify Puffin correctness there: 123b14a |
|
My two cents is that they shouldn't use "puffin" for this, they even add The only reason we add the |
|
Yeah, fair point — since reads jump straight to the blob via What pushed me here is that not every reader goes through those offsets. pyiceberg's deletion-vector reader, for example, validates the file as a standalone Puffin and chokes on the bare blob with And good call on the test — checking the first/last 4 bytes wasn't really proving much. I've reworked it to actually parse the file as a Puffin: leading + footer-start + trailing magic, parse the footer Tangentially related: while validating this I noticed delete manifests get written with |
Upgrade the v3-DELETE deletion-vector test from a leading/trailing magic-byte check to a real Puffin parse: verify the leading, footer-start, and trailing PFA1 magic, parse the footer FileMetadata JSON, and recover the deletion-vector-v1 blob via the footer's own offset/length (confirming the 0xD1D33964 blob magic). This exercises the file as a spec-compliant Puffin container rather than only checking the first and last four bytes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Raghvendra Singh <raghav@cashify.in>
d5333a3 to
549e8e3
Compare
|
@Tishj let me know, how we can take this forward. |
|
This is missing verification in the read path, EDIT: |
When scanning a deletion vector, verify the file is a valid Puffin container before reading the blob via content_offset/content_size_in_bytes: - Always: validate the footer trailing magic and that the FooterPayload is uncompressed. The Puffin spec permits an LZ4-compressed footer, but a deletion-vector footer must be plain so a reader walking the footer can parse the FileMetadata without a decompressor. - In DEBUG builds: parse the whole container (leading/footer magic + the FileMetadata footer) and assert the single blob's offset/length agree with the manifest's content_offset/content_size_in_bytes used by the fast read path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Raghvendra Singh <raghav@cashify.in>
2a9533a to
b53641e
Compare
|
Done — pushed a commit resurrecting the footer parse as a verification step in the read path (
It reuses the already-open |
Problem
IcebergDelete::WriteDeletionVectorFilewrites a deletion vector by serializing thedeletion-vector-v1blob and writing only that blob to the file, while labelling itfile_format = puffinwithcontent_offset = 0andfile_size_bytes == blob size.That is not a valid Puffin file. The Puffin spec
requires the layout
Magic Blob₁ … Blobₙ Footer, with the 4-byte magic0x50 0x46 0x41 0x31(PFA1) at the start of the file and a Footer(
Magic,FooterPayloadJSON,FooterPayloadSize,Flags,Magic) at the end.The file produced today has neither the leading magic nor the footer — it is a bare blob.
It works in practice only because every reader (duckdb-iceberg included) locates the
vector via the manifest's
content_offset/content_size_in_bytesand never parses thePuffin container. A spec-compliant Puffin reader — or any tool that enumerates blobs via
the footer, validates the file standalone, or lacks the manifest offsets — cannot read
these files.
Fix
Add
IcebergDeletionVectorData::ToPuffinFile, which wraps the blob in a proper container:The
FooterPayloadis aFileMetadatadescribing the singledeletion-vector-v1blob.Per the spec, for a deletion vector
snapshot-idandsequence-numberare-1, the blobcarries the required
referenced-data-fileandcardinalityproperties, and it isuncompressed (no
compression-codec, flags = 0).content_offsetis now4(the blob begins right after the leading magic). The read pathis unchanged — it is driven by the manifest's
content_offset, so existing deletion-vectorfiles written with
content_offset = 0keep reading correctly. No migration needed.Validation
FooterPayloadSize, flags)is byte-identical to an Iceberg-Java-generated Puffin file
(
empty-puffin-uncompressed.bin):PFA1 | … | <size LE> | 0x00000000 | PFA1. The blobmetadata field order matches Iceberg-Java
(
type, fields, snapshot-id, sequence-number, offset, length, properties).deletion-vector-v1blob is unchanged (ToBlob), so it remains byte-identical tothe Iceberg reference vector.
Testing
test/.../delete/test_deletion_vector_is_valid_puffin.test: after a v3DELETE, resolvesthe deletion-vector file via
iceberg_metadata(content = 'POSITION_DELETES') and assertsthe file begins and ends with
PFA1— i.e. it is a valid Puffin container, not a bareblob. Skipped on Nessie (v3 fixture limitation, like the other v3 delete tests).