Skip to content

feat(iceberg): serialize DV-s to the deletion-vector-v1 blob layout - #3232

Open
laskoviymishka wants to merge 1 commit into
apache:mainfrom
laskoviymishka:feat/dv-v1-serializer
Open

laskoviymishka wants to merge 1 commit into
apache:mainfrom
laskoviymishka:feat/dv-v1-serializer

Conversation

@laskoviymishka

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Add DeleteVector::serialize, which produces a deletion-vector-v1 blob that DeleteVector::deserialize reads back (#2580). It frames the roaring treemap as [length][magic][vector][crc], matching what deserialize reads: the roaring portable 64-bit directory (a little-endian bitmap count followed by ascending u32-keyed bitmaps), a big-endian length prefix covering the magic and vector, and a big-endian CRC-32 over the same bytes. The vector is written as-is with sparse-key encoding rather than run-length-encoded, so for the same positions the bytes need not match Iceberg-Java's BitmapPositionDeleteIndex, which run-optimizes and writes dense keys; both encodings are spec-conformant and mutually decodable. This emits the raw blob only; wrapping it in a Puffin Blob with the snapshot and sequence-number properties is left to a later change.

Fold the framing into a single implementation shared by serialize and the tests, so the encode paths no longer duplicate the layout: the test-only frame_dv_blob and the encode_dv_blob helpers now delegate to serialize, and the length prefix uses a checked u32 conversion instead of a truncating cast. Round-trip tests cover an empty vector, a single position, a dense single-container run, positions spanning multiple 2^32 containers, and a run-optimized dense range, plus golden assertions on the magic offset, the length prefix, and CRC validation, and a byte-stability test that re-serializes a decoded blob to guard against container-type normalization on the round trip.

…lob layout

Add DeleteVector::serialize, which produces a deletion-vector-v1 blob that
DeleteVector::deserialize reads back (apache#2580). It frames the roaring treemap as
[length][magic][vector][crc], matching what deserialize reads: the roaring
portable 64-bit directory (a little-endian bitmap count followed by ascending
u32-keyed bitmaps), a big-endian length prefix covering the magic and vector,
and a big-endian CRC-32 over the same bytes. The vector is written as-is with
sparse-key encoding rather than run-length-encoded, so for the same positions
the bytes need not match Iceberg-Java's BitmapPositionDeleteIndex, which
run-optimizes and writes dense keys; both encodings are spec-conformant and
mutually decodable. This emits the raw blob only; wrapping it in a Puffin Blob
with the snapshot and sequence-number properties is left to a later change.

Fold the framing into a single implementation shared by serialize and the
tests, so the encode paths no longer duplicate the layout: the test-only
frame_dv_blob and the encode_dv_blob helpers now delegate to serialize, and the
length prefix uses a checked u32 conversion instead of a truncating cast.
Round-trip tests cover an empty vector, a single position, a dense
single-container run, positions spanning multiple 2^32 containers, and a
run-optimized dense range, plus golden assertions on the magic offset, the
length prefix, and CRC validation, and a byte-stability test that re-serializes
a decoded blob to guard against container-type normalization on the round trip.
@laskoviymishka
laskoviymishka marked this pull request as ready for review September 15, 2026 20:03
assert_eq!(&blob[4..8], &[0xD1, 0xD3, 0x39, 0x64]);
let declared = u32::from_be_bytes(blob[..4].try_into().unwrap());
// Everything but the length prefix and the trailing CRC is covered by `declared`.
let framing_overhead = DV_LENGTH_PREFIX_BYTES + DV_CRC_BYTES;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Just out of curiosity, why didn't you hard code 8 here instead of using the constants? Wouldn't you want to make this test as independent as possible?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to make this a bit more self-explanatory, just having 8 means not much, but this expression has meaning, alternative would be just a comment i guess.

let mut blob = Vec::with_capacity(DV_LENGTH_PREFIX_BYTES + body_len + DV_CRC_BYTES);
blob.extend_from_slice(&(body_len as u32).to_be_bytes());
let body_len =
u32::try_from(body_len).expect("deletion-vector-v1 body length exceeds u32::MAX");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a good catch. But should we do a panic here? why not return an error? I agree it is highly unlikely to get to this though, but it would be good to avoid panics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants