Uh oh!
There was an error while loading. Please reload this page.
Add PuffinWriter for writing deletion vectors - #3474
Conversation
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| return {path: _bitmaps_to_chunked_array(bitmaps) for path, bitmaps in self._deletion_vectors.items()} | ||
| class PuffinWriter: |
There was a problem hiding this comment.
This name looks too generic. We could consider renaming it to DeletionVectorWriter or a similar name. I've opened #3491 to extract DV-specific logic from puffin.py.
There was a problem hiding this comment.
Sounds good to me. I'll wait for #3491 to land and then rebase this on top of it, renaming the writer to DeletionVectorWriter as part of that. Thanks both!
There was a problem hiding this comment.
sungwy
left a comment
There was a problem hiding this comment.
Thanks for working on this @moomindani - I added some initial review comments
Uh oh!
There was an error while loading. Please reload this page.
| return {path: _bitmaps_to_chunked_array(bitmaps) for path, bitmaps in self._deletion_vectors.items()} | ||
| class PuffinWriter: |
moomindani
commented
Jun 25, 2026
Reworked onto #3491Now that #3491 has merged, I've rebased this onto it (via a merge of
Notable changes in context
How prior review feedback is addressed
Wiring this into the merge-on-read branch of |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| puffin_bytes = out.getvalue() | ||
| with self._output_file.create(overwrite=True) as output_stream: |
There was a problem hiding this comment.
Why is overwrite=True hardcoded here? For idempotency? If a caller accidentally passes an existing file path, this silently replaces it.
There was a problem hiding this comment.
Yes — it's for idempotency. This matches the other write-once writers in PyIceberg: the data file writer (pyarrow.py) and the manifest writer (avro/file.py) both hardcode overwrite=True. Puffin DV files are written to fresh, UUID-based locations (like data/manifest files), so a collision with a different file isn't expected; the realistic case where the path already exists is a retry writing the same file, and overwrite=True keeps that idempotent instead of failing. Only ToOutputFile.table_metadata parameterizes overwrite, since metadata writes can legitimately target an existing path.
That said, if you can think of a case where silently overwriting would be a problem, I'm happy to make it a constructor argument.
ebyhr
commented
Jun 30, 2026
Could you rebase on main to resolve conflicts? |
…inWriter Following the design agreed in apache#3491, split the write path into: - DeletionVector serialization (domain): from_positions(), _serialize_bitmap() and to_blob() as the write-side counterparts of _deserialize_bitmap() and to_vector(), reusing MAX_JAVA_SIGNED / PROPERTY_REFERENCED_DATA_FILE. - A generic, blob-agnostic PuffinWriter (format) that assembles a Puffin file from PuffinBlob payloads, mirroring PuffinFile on the read side and scaling to future blob types (e.g. apache-datasketches-theta-v1). PuffinWriter is a context manager (consistent with ManifestWriter); the file size is available via len(output_file) after exit. The created-by default uses pyiceberg.__version__. Co-authored-by: Isaac
… error - DeletionVector._serialize_bitmap now run-optimizes each bitmap before serializing, matching Java's BitmapPositionDeleteIndex (a contiguous 100k-position DV drops from ~16 KB to ~25 bytes; still round-trips). - from_positions rejects positions above MAX_POSITION, mirroring Java's RoaringPositionBitmap.MAX_POSITION, to avoid an OOM in the gap-fill. - PuffinWriter.__exit__ short-circuits when the with-body raised, so no half-populated file is written. Co-authored-by: Isaac
Restructure test_puffin_writer_does_not_write_on_exception to use try/except instead of nesting an unconditional raise inside pytest.raises, which mypy's warn-unreachable flagged as unreachable. Co-authored-by: Isaac
b26e7ed to
5fdd51fComparemoomindani
commented
Jun 30, 2026
Done — rebased onto the latest |
moomindani
commented
Jul 8, 2026
rambleraptor
commented
Jul 8, 2026
Can you change the PR description? We're not using the Spark test anymore (waiting until a later PR), so we shouldn't mention that the test exists for future PR readers. |
| with io.BytesIO() as out: | ||
| out.write(len(non_empty).to_bytes(8, "little")) | ||
| for key, bitmap in non_empty: | ||
| if key > MAX_JAVA_SIGNED: |
There was a problem hiding this comment.
I think we've got an off-by-one error here.
Here's Java
Java fails if key > (2^31 - 1) - 1
We fail if key > (2^31 ) - 1
This is a weird edge case and definitely an issue in Java, but probably not worth fixing over there.
There was a problem hiding this comment.
You're right, thanks — Java's readKey rejects key > Integer.MAX_VALUE - 1 while this check allowed one more key, so we could write a DV that Java cannot read. Fixed the bound to match Java. Since the key is the list index in _serialize_bitmap, the boundary isn't practically constructible in a test (it would need a 2^31-element list); the existing MAX_POSITION validation in from_positions keeps user input below the bound, so this check stays as defense in depth.
rambleraptor
left a comment
There was a problem hiding this comment.
Broadly this looks great. I saw a quick off-by-one error, but that's fine as-is. I do want to verify that we meant to get rid of the Spark test. I know that was a blocker in my original PR and I want to make sure that we get that test in at some point.
a4b4c67 to
5fdd51fCompareThanks for the review! Updated the description to remove the Spark interop test references. |
Java's RoaringPositionBitmap.readKey rejects keys above Integer.MAX_VALUE - 1, while the serializer allowed one more key, so PyIceberg could write a deletion vector Java cannot read.
moomindani
commented
Aug 7, 2026
Not stale — still active and waiting for review. Status: CI green (17/17), mergeable, and all review comments so far are answered. The three open threads are all replies from me awaiting confirmation:
@sungwy you reviewed the earlier rounds — would you mind taking another look? @ebyhr@rambleraptor if the resolutions above look right to you, marking those threads resolved would help move this along. |
Part of #2261. Continues #2822.
Rationale for this change
This adds a
PuffinWriterfor writing Puffin files containingdeletion-vector-v1blobs — the first building block for deletion-vector write support in PyIceberg (tracking issue #2261).It revives #2822 by @rambleraptor, which was auto-closed by the stale bot rather than on merit. The original work — including all review feedback already addressed there (@ebyhr, @geruh) — is preserved commit-for-commit.
On top of that, this PR adds unit tests for two agreed review items that were not yet asserted by any test:
fieldsvalue[2147483645](JavaMetadataColumns.ROW_POSITION, INT_MAX - 2), required for Java/Spark interoperability; andPuffinFilereader skips, so the round-trip tests did not previously exercise it.As in the original PR, this is intentionally scoped to the writer + tests so we can agree on the write semantics before wiring it into the delete/manifest writers and the merge-on-read path. Per the original review discussion, the writer expects the caller to provide one merged deletion vector per data file.
Are these changes tested?
Yes: unit tests for round-trip write/read, the single-blob (1:1) behavior, the DV field id, byte-level blob framing, and empty files (
tests/table/test_puffin.py).Are there any user-facing changes?
No.
PuffinWriteris a new internal building block and is not yet wired into any public write path.