Skip to main content

iceberg/
delete_vector.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Deletion vectors: the deleted row positions of one data file, as a Puffin
19//! `deletion-vector-v1` blob decodes to. Public so an engine that reads
20//! Parquet with a reader of its own can still decode the vectors Iceberg
21//! writes.
22
23use std::ops::BitOrAssign;
24
25use bytes::Buf;
26use roaring::bitmap::Iter;
27use roaring::treemap::BitmapIter;
28use roaring::{RoaringBitmap, RoaringTreemap};
29
30use crate::error::invalid_data;
31use crate::{Error, ErrorKind, Result};
32
33/// Magic bytes prefixing a serialized `deletion-vector-v1` bitmap, per the Iceberg Puffin spec.
34/// Iceberg-Java stores these as the little-endian int 1681511377 (0x6439D3D1).
35const DV_MAGIC: [u8; 4] = [0xD1, 0xD3, 0x39, 0x64];
36const DV_LENGTH_PREFIX_BYTES: usize = 4;
37const DV_MAGIC_BYTES: usize = 4;
38const DV_CRC_BYTES: usize = 4;
39const DV_MIN_BLOB_BYTES: usize = DV_LENGTH_PREFIX_BYTES + DV_MAGIC_BYTES + DV_CRC_BYTES;
40
41/// The deleted row positions of one data file: a set of 64-bit positions,
42/// kept as a [`RoaringTreemap`].
43#[derive(Debug, Default)]
44pub struct DeleteVector {
45    inner: RoaringTreemap,
46}
47
48impl DeleteVector {
49    /// Wraps `roaring_treemap`, whose members are the deleted positions.
50    pub fn new(roaring_treemap: RoaringTreemap) -> DeleteVector {
51        DeleteVector {
52            inner: roaring_treemap,
53        }
54    }
55
56    /// Iterates the deleted positions in ascending order.
57    pub fn iter(&self) -> DeleteVectorIterator<'_> {
58        let outer = self.inner.bitmaps();
59        DeleteVectorIterator { outer, inner: None }
60    }
61
62    /// Marks `pos` as deleted, returning whether it was not already.
63    pub fn insert(&mut self, pos: u64) -> bool {
64        self.inner.insert(pos)
65    }
66
67    /// Marks the given `positions` as deleted and returns the number of elements appended.
68    ///
69    /// The input slice must be strictly ordered in ascending order, and every value must be greater than all existing values already in the set.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error if the precondition is not met.
74    pub fn insert_positions(&mut self, positions: &[u64]) -> Result<usize> {
75        if let Err(err) = self.inner.append(positions.iter().copied()) {
76            return Err(Error::new(
77                ErrorKind::PreconditionFailed,
78                "failed to marks rows as deleted".to_string(),
79            )
80            .with_source(err));
81        }
82        Ok(positions.len())
83    }
84
85    /// How many positions are deleted.
86    pub fn len(&self) -> u64 {
87        self.inner.len()
88    }
89
90    /// Whether no position is deleted.
91    pub fn is_empty(&self) -> bool {
92        self.inner.is_empty()
93    }
94
95    /// Returns the deleted positions as ascending, non-overlapping half-open ranges.
96    ///
97    /// Adjacent ranges may not be merged (a run is split at each 2^32 boundary), so callers
98    /// shouldn't assume a range is maximal. A range holding only `u64::MAX` is dropped, because a
99    /// half-open `u64` range can't hold it and no Parquet row can be there.
100    pub(crate) fn ranges(&self) -> DeleteVectorRanges<'_> {
101        DeleteVectorRanges {
102            bitmaps: self.inner.bitmaps(),
103            high_bits: 0,
104            bitmap_iter: None,
105            buffered: None,
106        }
107    }
108
109    /// Parses a `deletion-vector-v1` Puffin blob into a `DeleteVector`.
110    ///
111    /// The layout, defined by the Iceberg Puffin spec and matching Iceberg-Java's
112    /// `BitmapPositionDeleteIndex`, is:
113    ///
114    /// ```text
115    /// [length: u32 big-endian][magic: D1 D3 39 64][vector][crc: u32 big-endian]
116    /// ```
117    ///
118    /// `length` counts the magic and vector bytes (not itself or the CRC). The CRC-32 is
119    /// computed over the magic and vector. `vector` is a roaring bitmap in the portable 64-bit
120    /// format: a directory of 32-bit key / 32-bit roaring bitmap pairs, ordered by unsigned
121    /// comparison of the keys, one bitmap per key.
122    ///
123    /// # Errors
124    ///
125    /// Returns [`ErrorKind::DataInvalid`] if the blob is shorter than the minimum, the length
126    /// prefix or CRC does not match, the magic is wrong, the roaring bitmap count exceeds the
127    /// portable format's maximum, the roaring directory's keys are not ordered by unsigned
128    /// comparison, or the roaring payload fails to decode.
129    pub fn deserialize(blob: &[u8]) -> Result<Self> {
130        if blob.len() < DV_MIN_BLOB_BYTES {
131            return Err(invalid_data!(
132                "deletion-vector-v1 blob is {} bytes, shorter than the {DV_MIN_BLOB_BYTES}-byte minimum",
133                blob.len()
134            ));
135        }
136
137        // The magic and vector, i.e. the bytes covered by both the length prefix and the CRC.
138        let body = &blob[DV_LENGTH_PREFIX_BYTES..blob.len() - DV_CRC_BYTES];
139        verify_length_prefix(&blob[..DV_LENGTH_PREFIX_BYTES], body)?;
140
141        // Verify the CRC before interpreting any bytes so a corrupt blob yields a single clear
142        // error rather than an opaque roaring decode failure.
143        verify_crc(body, &blob[blob.len() - DV_CRC_BYTES..])?;
144
145        let (magic, vector) = body.split_at(DV_MAGIC_BYTES);
146        verify_magic(magic)?;
147
148        let inner = decode_roaring_directory(vector)?;
149
150        Ok(DeleteVector { inner })
151    }
152}
153
154fn verify_length_prefix(mut prefix: &[u8], body: &[u8]) -> Result<()> {
155    let declared_len = prefix.try_get_u32().map_err(|e| {
156        Error::new(
157            ErrorKind::Unexpected,
158            "failed to read the deletion-vector-v1 length prefix",
159        )
160        .with_source(e)
161    })? as usize;
162    if declared_len != body.len() {
163        return Err(invalid_data!(
164            "deletion-vector-v1 length prefix is {declared_len}, expected {}",
165            body.len()
166        ));
167    }
168    Ok(())
169}
170
171fn verify_crc(body: &[u8], mut crc_bytes: &[u8]) -> Result<()> {
172    let stored_crc = crc_bytes.try_get_u32().map_err(|e| {
173        Error::new(
174            ErrorKind::Unexpected,
175            "failed to read the deletion-vector-v1 CRC",
176        )
177        .with_source(e)
178    })?;
179    let computed_crc = crc32fast::hash(body);
180    if computed_crc != stored_crc {
181        return Err(invalid_data!(
182            "deletion-vector-v1 CRC mismatch: computed {computed_crc:#010x}, stored {stored_crc:#010x}"
183        ));
184    }
185    Ok(())
186}
187
188fn verify_magic(magic: &[u8]) -> Result<()> {
189    if magic != DV_MAGIC {
190        return Err(invalid_data!(
191            "deletion-vector-v1 magic mismatch: {magic:02x?}, expected {DV_MAGIC:02x?}"
192        ));
193    }
194    Ok(())
195}
196
197// The Puffin spec defines the roaring directory as the bitmaps "ordered by unsigned comparison
198// of the 32-bit keys", with one bitmap per key. Walk it ourselves (rather than
199// `RoaringTreemap::deserialize_from`, which stores keys in a `BTreeMap` via a plain insert and
200// would silently accept a stream with duplicate or out-of-order keys, discarding the earlier
201// bitmap on a duplicate) so a non-conformant blob is rejected instead of decoded into a value
202// that doesn't match what was actually written.
203fn decode_roaring_directory(mut reader: &[u8]) -> Result<RoaringTreemap> {
204    let bitmap_count = reader.try_get_u64_le().map_err(|e| {
205        invalid_data!("failed to decode deletion-vector-v1 roaring payload").with_source(e)
206    })?;
207    // The roaring portable format restricts the bitmap count to [0, 2^32 - 1] (it is stored as a
208    // u64 with the upper 32 bits reserved as zero padding).
209    if bitmap_count > u32::MAX as u64 {
210        return Err(invalid_data!(
211            "deletion-vector-v1 roaring bitmap count {bitmap_count} exceeds the {}-key maximum",
212            u32::MAX
213        ));
214    }
215
216    let mut bitmaps = Vec::new();
217    let mut last_key: Option<u32> = None;
218    for _ in 0..bitmap_count {
219        let key = reader.try_get_u32_le().map_err(|e| {
220            invalid_data!("failed to decode deletion-vector-v1 roaring payload").with_source(e)
221        })?;
222        if let Some(last) = last_key
223            && key <= last
224        {
225            return Err(invalid_data!(
226                "deletion-vector-v1 roaring keys must be ordered by unsigned comparison, got key {key} after {last}"
227            ));
228        }
229        last_key = Some(key);
230
231        let bitmap = RoaringBitmap::deserialize_from(&mut reader).map_err(|e| {
232            invalid_data!("failed to decode deletion-vector-v1 roaring payload").with_source(e)
233        })?;
234        bitmaps.push((key, bitmap));
235    }
236
237    // `bitmaps` is already sorted by key, but `roaring` has no constructor that accepts
238    // pre-sorted pairs without re-sorting them; revisit if that changes upstream.
239    Ok(RoaringTreemap::from_bitmaps(bitmaps))
240}
241
242/// Iterates the deleted positions of a [`DeleteVector`] in ascending order.
243pub struct DeleteVectorIterator<'a> {
244    outer: BitmapIter<'a>,
245    inner: Option<DeleteVectorIteratorInner<'a>>,
246}
247
248struct DeleteVectorIteratorInner<'a> {
249    high_bits: u32,
250    bitmap_iter: Iter<'a>,
251}
252
253impl Iterator for DeleteVectorIterator<'_> {
254    type Item = u64;
255
256    fn next(&mut self) -> Option<Self::Item> {
257        if let Some(inner) = &mut self.inner
258            && let Some(inner_next) = inner.bitmap_iter.next()
259        {
260            return Some(u64::from(inner.high_bits) << 32 | u64::from(inner_next));
261        }
262
263        if let Some((high_bits, next_bitmap)) = self.outer.next() {
264            self.inner = Some(DeleteVectorIteratorInner {
265                high_bits,
266                bitmap_iter: next_bitmap.iter(),
267            })
268        } else {
269            return None;
270        }
271
272        self.next()
273    }
274}
275
276impl DeleteVectorIterator<'_> {
277    /// Skips the positions below `pos`, so the next one yielded is the first
278    /// at or after it. A `pos` below the next position does nothing.
279    pub fn advance_to(&mut self, pos: u64) {
280        let hi = (pos >> 32) as u32;
281        let lo = pos as u32;
282
283        if let Some(inner) = &mut self.inner {
284            if inner.high_bits > hi {
285                return;
286            }
287            if inner.high_bits == hi {
288                inner.bitmap_iter.advance_to(lo);
289                return;
290            }
291        }
292
293        // `pos` is in a later bitmap, or nothing has been yielded yet.
294        self.inner = None;
295        for (high_bits, bitmap) in self.outer.by_ref() {
296            if high_bits < hi {
297                continue;
298            }
299            let mut bitmap_iter = bitmap.iter();
300            if high_bits == hi {
301                bitmap_iter.advance_to(lo);
302            }
303            self.inner = Some(DeleteVectorIteratorInner {
304                high_bits,
305                bitmap_iter,
306            });
307            return;
308        }
309    }
310}
311
312/// Number of consecutive positions [`DeleteVectorRanges`] walks one by one before it asks the
313/// bitmap for the rest of the run.
314///
315/// `roaring`'s `next_range` binary-searches the rest of an array container on every call, which
316/// costs more than `next` when most runs are a single position, so it only pays off for long
317/// runs. Tuned with `roaring` 0.11.4 by reading a 20M-row file with deletes in runs of 20, 100
318/// and longer, and as scattered single rows and pairs: 4 was best on runs, compared with 8, 16
319/// and 32, and no slower on scattered deletes.
320const RUN_PROBE_LEN: u32 = 4;
321
322/// What [`DeleteVectorRanges`] has taken from its bitmap iterator but not returned yet.
323enum Buffered {
324    /// Start of the next range.
325    Pos(u32),
326    /// A complete range, inclusive end.
327    Range(u32, u32),
328}
329
330/// Iterator over a [`DeleteVector`]'s deleted positions as half-open ranges, see
331/// [`DeleteVector::ranges`].
332pub(crate) struct DeleteVectorRanges<'a> {
333    bitmaps: BitmapIter<'a>,
334    /// High 32 bits of the positions in `bitmap_iter` and `buffered`.
335    high_bits: u32,
336    bitmap_iter: Option<Iter<'a>>,
337    buffered: Option<Buffered>,
338}
339
340impl DeleteVectorRanges<'_> {
341    /// Returns `None` only for a range holding nothing but `u64::MAX`, see [`DeleteVector::ranges`].
342    fn full_range(&self, start: u32, end_inclusive: u32) -> Option<(u64, u64)> {
343        let base = u64::from(self.high_bits) << 32;
344        let start = base | u64::from(start);
345        let end = (base | u64::from(end_inclusive)).saturating_add(1);
346        (start < end).then_some((start, end))
347    }
348
349    /// Skips every deleted position below `pos`. A range still buffered here that starts below
350    /// `pos` and ends after it is cut to start at `pos`; a range already returned by `next` is
351    /// the caller's to cut.
352    pub(crate) fn skip_to(&mut self, pos: u64) {
353        let hi = (pos >> 32) as u32;
354        let lo = pos as u32;
355
356        if self.bitmap_iter.is_some() && self.high_bits > hi {
357            return;
358        }
359        if let Some(iter) = self.bitmap_iter.as_mut()
360            && self.high_bits == hi
361        {
362            self.buffered = match self.buffered.take() {
363                Some(Buffered::Pos(start)) if start < lo => None,
364                Some(Buffered::Range(_, end)) if end < lo => None,
365                Some(Buffered::Range(start, end)) if start < lo => Some(Buffered::Range(lo, end)),
366                buffered => buffered,
367            };
368            iter.advance_to(lo);
369            return;
370        }
371
372        // `pos` is in a later bitmap: drop everything buffered from the current one.
373        self.bitmap_iter = None;
374        self.buffered = None;
375        for (high_bits, bitmap) in self.bitmaps.by_ref() {
376            if high_bits < hi {
377                continue;
378            }
379            let mut iter = bitmap.iter();
380            if high_bits == hi {
381                iter.advance_to(lo);
382            }
383            self.high_bits = high_bits;
384            self.bitmap_iter = Some(iter);
385            return;
386        }
387    }
388}
389
390impl Iterator for DeleteVectorRanges<'_> {
391    type Item = (u64, u64);
392
393    fn next(&mut self) -> Option<Self::Item> {
394        loop {
395            let pending = match self.buffered.take() {
396                Some(Buffered::Range(start, end)) => return self.full_range(start, end),
397                Some(Buffered::Pos(start)) => Some(start),
398                None => None,
399            };
400
401            if let Some(iter) = self.bitmap_iter.as_mut()
402                && let Some(start) = pending.or_else(|| iter.next())
403            {
404                // Walk short runs with `next`, and ask for the rest of a long run at once.
405                let mut end = start;
406                let mut walked = 0;
407                loop {
408                    if walked == RUN_PROBE_LEN {
409                        match iter.next_range() {
410                            Some(run) if Some(*run.start()) == end.checked_add(1) => {
411                                end = *run.end();
412                            }
413                            Some(run) => {
414                                self.buffered = Some(Buffered::Range(*run.start(), *run.end()))
415                            }
416                            None => {}
417                        }
418                        break;
419                    }
420                    match iter.next() {
421                        Some(next) if Some(next) == end.checked_add(1) => {
422                            end = next;
423                            walked += 1;
424                        }
425                        next => {
426                            self.buffered = next.map(Buffered::Pos);
427                            break;
428                        }
429                    }
430                }
431                return self.full_range(start, end);
432            }
433
434            let (high_bits, bitmap) = self.bitmaps.next()?;
435            self.high_bits = high_bits;
436            self.bitmap_iter = Some(bitmap.iter());
437        }
438    }
439}
440
441impl BitOrAssign for DeleteVector {
442    fn bitor_assign(&mut self, other: Self) {
443        self.inner.bitor_assign(&other.inner);
444    }
445}
446
447// Reproduces Iceberg-Java's `deletion-vector-v1` framing so tests can round-trip through
448// `deserialize` without a Java writer, and so other test modules can build blob fixtures.
449// Cross-implementation golden fixtures produced by Iceberg-Java are tracked separately; this
450// only checks that our decode matches our encode.
451#[cfg(test)]
452pub(crate) fn frame_dv_blob(vector: &[u8]) -> Vec<u8> {
453    let body_len = DV_MAGIC_BYTES + vector.len();
454    let mut blob = Vec::with_capacity(DV_LENGTH_PREFIX_BYTES + body_len + DV_CRC_BYTES);
455    blob.extend_from_slice(&(body_len as u32).to_be_bytes());
456    blob.extend_from_slice(&DV_MAGIC);
457    blob.extend_from_slice(vector);
458    let crc = crc32fast::hash(&blob[DV_LENGTH_PREFIX_BYTES..]);
459    blob.extend_from_slice(&crc.to_be_bytes());
460    blob
461}
462
463/// Deterministic pseudo-random sets mixing single positions, short and long runs, runs across
464/// 2^16 and 2^32 boundaries, and empty bitmaps between used ones.
465#[cfg(test)]
466pub(crate) fn mixed_delete_vectors() -> Vec<DeleteVector> {
467    use rand::rngs::StdRng;
468    use rand::{Rng, SeedableRng};
469
470    let mut rng = StdRng::seed_from_u64(7);
471    let mut vectors = vec![DeleteVector::default()];
472    for case in 0..40u64 {
473        let mut dv = DeleteVector::default();
474        let base = if case % 4 == 0 { (1u64 << 32) - 40 } else { 0 };
475        let mut pos = base + rng.random_range(0..50);
476        for _ in 0..rng.random_range(0..60) {
477            let run = match rng.random_range(0..5) {
478                0 => 1,
479                1 => 2,
480                // one past the probe length: the rest comes from `next_range`
481                2 => u64::from(RUN_PROBE_LEN) + 1,
482                3 => rng.random_range(1..=40),
483                _ => rng.random_range(1..=70_000),
484            };
485            dv.inner.insert_range(pos..pos + run);
486            pos += run + rng.random_range(1..=30);
487        }
488        vectors.push(dv);
489    }
490    let mut boundaries = DeleteVector::default();
491    boundaries.inner.insert_range((1 << 16) - 3..(1 << 16) + 3);
492    boundaries
493        .inner
494        .insert_range((1 << 32) - 20..(1 << 32) + 20);
495    boundaries.inner.insert((2 << 32) + 7);
496    vectors.push(boundaries);
497    // High bits 0, 3 and 7, so skipping walks past empty bitmaps.
498    let mut gaps = DeleteVector::default();
499    gaps.inner.insert_range(2..12);
500    gaps.inner.insert_range((3 << 32) + 5..(3 << 32) + 70_000);
501    gaps.inner.insert((7 << 32) + 1);
502    vectors.push(gaps);
503    vectors
504}
505
506#[cfg(test)]
507mod tests {
508    use rand::rngs::StdRng;
509    use rand::{Rng, SeedableRng};
510
511    use super::*;
512
513    #[test]
514    fn test_ranges_cover_exactly_the_deleted_positions() {
515        for dv in mixed_delete_vectors() {
516            let positions: Vec<u64> = dv.iter().collect();
517            let mut expanded = Vec::new();
518            let mut last_end = None;
519            for (start, end) in dv.ranges() {
520                assert!(start < end, "empty range {start}..{end}");
521                assert!(
522                    last_end.is_none_or(|last| last <= start),
523                    "ranges out of order"
524                );
525                last_end = Some(end);
526                expanded.extend(start..end);
527            }
528            assert_eq!(expanded, positions);
529        }
530    }
531
532    #[test]
533    fn test_ranges_skip_to_matches_model() {
534        let mut rng = StdRng::seed_from_u64(7);
535
536        for dv in mixed_delete_vectors() {
537            let deleted: Vec<u64> = dv.iter().collect();
538            let max = deleted.last().copied().unwrap_or(0) + 10;
539            // `deleted[next..]` has been neither returned nor skipped, and every position below
540            // `done` has.
541            let mut next = 0;
542            let mut done = 0u64;
543            let mut ranges = dv.ranges();
544            loop {
545                match rng.random_range(0..6) {
546                    0 | 1 => {
547                        let target = done.max(deleted.get(next).copied().unwrap_or(max))
548                            + rng.random_range(0..200);
549                        ranges.skip_to(target);
550                        next = deleted.partition_point(|&p| p < target);
551                        done = target;
552                        continue;
553                    }
554                    // A target at or below `done`, near it or anywhere below: does nothing.
555                    2 => {
556                        let back = if rng.random_bool(0.5) {
557                            done.saturating_sub(rng.random_range(0..200))
558                        } else {
559                            rng.random_range(0..=done)
560                        };
561                        ranges.skip_to(back);
562                        continue;
563                    }
564                    _ => {}
565                }
566                match ranges.next() {
567                    Some((start, end)) => {
568                        assert!(start < end, "empty range {start}..{end}");
569                        let len = (end - start) as usize;
570                        assert_eq!(
571                            deleted
572                                .get(next..next + len)
573                                .map(|run| (run[0], run[len - 1])),
574                            Some((start, end - 1)),
575                            "returned {start}..{end}"
576                        );
577                        next += len;
578                        done = end;
579                    }
580                    None => {
581                        assert_eq!(next, deleted.len(), "missed {:?}", &deleted[next..]);
582                        break;
583                    }
584                }
585            }
586        }
587    }
588
589    #[test]
590    fn test_advance_to_matches_model() {
591        let mut rng = StdRng::seed_from_u64(11);
592
593        for dv in mixed_delete_vectors() {
594            let deleted: Vec<u64> = dv.iter().collect();
595            let max = deleted.last().copied().unwrap_or(0) + 10;
596            let mut next = 0;
597            let mut done = 0u64;
598            let mut iter = dv.iter();
599            loop {
600                match rng.random_range(0..6) {
601                    0 => {
602                        let target = done.max(deleted.get(next).copied().unwrap_or(max))
603                            + rng.random_range(0..200);
604                        iter.advance_to(target);
605                        next = deleted.partition_point(|&p| p < target);
606                        done = target;
607                        continue;
608                    }
609                    1 => {
610                        let back = if rng.random_bool(0.5) {
611                            done.saturating_sub(rng.random_range(0..200))
612                        } else {
613                            rng.random_range(0..=done)
614                        };
615                        iter.advance_to(back);
616                        continue;
617                    }
618                    _ => {}
619                }
620                let pos = iter.next();
621                assert_eq!(pos, deleted.get(next).copied());
622                match pos {
623                    Some(pos) => {
624                        next += 1;
625                        done = pos + 1;
626                    }
627                    None => break,
628                }
629            }
630        }
631    }
632
633    #[test]
634    fn test_ranges_skip_to_inside_buffered_run() {
635        // After a run of exactly RUN_PROBE_LEN + 1, the following run is already buffered.
636        let probe = u64::from(RUN_PROBE_LEN);
637        let mut dv = DeleteVector::default();
638        dv.inner.insert_range(0..probe + 1);
639        dv.inner.insert_range(probe + 3..probe + 13);
640        let mut ranges = dv.ranges();
641        assert_eq!(ranges.next(), Some((0, probe + 1)));
642        ranges.skip_to(probe + 8);
643        assert_eq!(ranges.next(), Some((probe + 8, probe + 13)));
644        assert_eq!(ranges.next(), None);
645    }
646
647    #[test]
648    fn test_ranges_join_runs_across_2_16() {
649        // `roaring`'s `next_range` joins a run across containers, so these come back whole.
650        // Starting near the boundary also covers the positions walked before `next_range`.
651        for start in [(1 << 16) - 100, (1 << 16) - 2] {
652            let mut dv = DeleteVector::default();
653            dv.inner.insert_range(start..(3 << 16) + 5);
654            assert_eq!(dv.ranges().collect::<Vec<_>>(), vec![(
655                start,
656                (3 << 16) + 5
657            )]);
658        }
659    }
660
661    #[test]
662    fn test_ranges_end_at_u64_max() {
663        let dv = DeleteVector::new(RoaringTreemap::from_iter([5, u64::MAX - 2, u64::MAX - 1]));
664        assert_eq!(dv.ranges().collect::<Vec<_>>(), vec![
665            (5, 6),
666            (u64::MAX - 2, u64::MAX)
667        ]);
668
669        // `u64::MAX` itself can't end a half-open range, and no row can be there.
670        let dv = DeleteVector::new(RoaringTreemap::from_iter([5, u64::MAX - 1, u64::MAX]));
671        assert_eq!(dv.ranges().collect::<Vec<_>>(), vec![
672            (5, 6),
673            (u64::MAX - 1, u64::MAX)
674        ]);
675        let dv = DeleteVector::new(RoaringTreemap::from_iter([u64::MAX]));
676        assert_eq!(dv.ranges().next(), None);
677    }
678
679    // The cases from https://github.com/apache/iceberg-rust/issues/3400, for `skip_to` and for
680    // `advance_to`.
681    #[test]
682    fn test_ranges_skip_to_missing_later_bitmap() {
683        let dv = DeleteVector::new(RoaringTreemap::from_iter([1u64, 5, 9]));
684        let mut ranges = dv.ranges();
685        assert_eq!(ranges.next(), Some((1, 2)));
686        ranges.skip_to((1 << 32) + 5);
687        assert_eq!(ranges.next(), None);
688    }
689
690    #[test]
691    fn test_ranges_skip_to_below_current_bitmap() {
692        let high = 1u64 << 32;
693        let dv = DeleteVector::new(RoaringTreemap::from_iter([high + 1, high + 2, high + 9]));
694        let mut ranges = dv.ranges();
695        assert_eq!(ranges.next(), Some((high + 1, high + 3)));
696        ranges.skip_to(10);
697        assert_eq!(ranges.next(), Some((high + 9, high + 10)));
698        assert_eq!(ranges.next(), None);
699    }
700
701    #[test]
702    fn test_advance_to_missing_later_bitmap() {
703        let dv = DeleteVector::new(RoaringTreemap::from_iter([5u64, 7]));
704        let mut iter = dv.iter();
705        assert_eq!(iter.next(), Some(5));
706        iter.advance_to((1 << 32) + 10);
707        assert_eq!(iter.next(), None);
708    }
709
710    #[test]
711    fn test_advance_to_below_current_bitmap() {
712        let high = 1u64 << 32;
713        let dv = DeleteVector::new(RoaringTreemap::from_iter([high + 10, high + 20]));
714        let mut iter = dv.iter();
715        assert_eq!(iter.next(), Some(high + 10));
716        iter.advance_to(50);
717        assert_eq!(iter.collect::<Vec<_>>(), vec![high + 20]);
718    }
719
720    #[test]
721    fn test_advance_to_before_first_next() {
722        let dv = DeleteVector::new(RoaringTreemap::from_iter([5u64, 200]));
723        let mut iter = dv.iter();
724        iter.advance_to(100);
725        assert_eq!(iter.collect::<Vec<_>>(), vec![200]);
726    }
727
728    #[test]
729    fn test_insertion_and_iteration() {
730        let mut dv = DeleteVector::default();
731        assert!(dv.insert(42));
732        assert!(dv.insert(100));
733        assert!(!dv.insert(42));
734
735        let mut items: Vec<u64> = dv.iter().collect();
736        items.sort();
737        assert_eq!(items, vec![42, 100]);
738        assert_eq!(dv.len(), 2);
739    }
740
741    #[test]
742    fn test_successful_insert_positions() {
743        let mut dv = DeleteVector::default();
744        let positions = vec![1, 2, 3, 1000, 1 << 33];
745        assert_eq!(dv.insert_positions(&positions).unwrap(), 5);
746
747        let mut collected: Vec<u64> = dv.iter().collect();
748        collected.sort();
749        assert_eq!(collected, positions);
750    }
751
752    /// Testing scenario: bulk insertion fails because input positions are not strictly increasing.
753    #[test]
754    fn test_failed_insertion_unsorted_elements() {
755        let mut dv = DeleteVector::default();
756        let positions = vec![1, 3, 5, 4];
757        let res = dv.insert_positions(&positions);
758        assert!(res.is_err());
759    }
760
761    /// Testing scenario: bulk insertion fails because input positions have intersection with existing ones.
762    #[test]
763    fn test_failed_insertion_with_intersection() {
764        let mut dv = DeleteVector::default();
765        let positions = vec![1, 3, 5];
766        assert_eq!(dv.insert_positions(&positions).unwrap(), 3);
767
768        let res = dv.insert_positions(&[2, 4]);
769        assert!(res.is_err());
770    }
771
772    /// Testing scenario: bulk insertion fails because input positions have duplicates.
773    #[test]
774    fn test_failed_insertion_duplicate_elements() {
775        let mut dv = DeleteVector::default();
776        let positions = vec![1, 3, 5, 5];
777        let res = dv.insert_positions(&positions);
778        assert!(res.is_err());
779    }
780
781    fn encode_dv_blob(dv: &DeleteVector) -> Vec<u8> {
782        let mut vector = Vec::with_capacity(dv.inner.serialized_size());
783        dv.inner.serialize_into(&mut vector).unwrap();
784        frame_dv_blob(&vector)
785    }
786
787    fn dv_of(positions: impl IntoIterator<Item = u64>) -> DeleteVector {
788        let mut dv = DeleteVector::default();
789        for pos in positions {
790            dv.insert(pos);
791        }
792        dv
793    }
794
795    fn sorted(dv: &DeleteVector) -> Vec<u64> {
796        let mut positions: Vec<u64> = dv.iter().collect();
797        positions.sort_unstable();
798        positions
799    }
800
801    #[test]
802    fn test_deserialize_roundtrip_empty() {
803        let blob = encode_dv_blob(&DeleteVector::default());
804        assert_eq!(DeleteVector::deserialize(&blob).unwrap().len(), 0);
805    }
806
807    #[test]
808    fn test_deserialize_roundtrip_small() {
809        let positions = [0u64, 5, 100, 1000];
810        let dv = DeleteVector::deserialize(&encode_dv_blob(&dv_of(positions))).unwrap();
811        assert_eq!(sorted(&dv), positions);
812    }
813
814    #[test]
815    fn test_deserialize_roundtrip_spanning_64bit_keys() {
816        let positions = [1u64, 1 << 33, (1 << 33) + 5, 1 << 34];
817        let dv = DeleteVector::deserialize(&encode_dv_blob(&dv_of(positions))).unwrap();
818        assert_eq!(sorted(&dv), positions);
819    }
820
821    // Java run-optimizes every deletion vector before writing, so real blobs carry RUN
822    // containers, which use the SERIAL_COOKIE roaring layout. Force that layout so decode
823    // exercises the run-container path rather than only array and bitmap containers.
824    #[test]
825    fn test_deserialize_roundtrip_run_optimized() {
826        let mut dv = dv_of(0..10_000);
827        assert!(
828            dv.inner.optimize(),
829            "expected a dense range to run-length encode"
830        );
831        let decoded = DeleteVector::deserialize(&encode_dv_blob(&dv)).unwrap();
832        assert_eq!(decoded.len(), 10_000);
833        let positions = sorted(&decoded);
834        assert_eq!(positions.first(), Some(&0));
835        assert_eq!(positions.last(), Some(&9_999));
836    }
837
838    #[test]
839    fn test_deserialize_rejects_short_blob() {
840        let err = DeleteVector::deserialize(&[0u8; DV_MIN_BLOB_BYTES - 1]).unwrap_err();
841        assert_eq!(err.kind(), ErrorKind::DataInvalid);
842    }
843
844    #[test]
845    fn test_deserialize_rejects_bad_magic() {
846        let mut blob = encode_dv_blob(&dv_of([1]));
847        blob[DV_LENGTH_PREFIX_BYTES] ^= 0xFF;
848        // Recompute the CRC so the magic check, not the CRC check, is what fails.
849        let end = blob.len() - DV_CRC_BYTES;
850        let crc = crc32fast::hash(&blob[DV_LENGTH_PREFIX_BYTES..end]);
851        blob[end..].copy_from_slice(&crc.to_be_bytes());
852        let err = DeleteVector::deserialize(&blob).unwrap_err();
853        assert!(err.message().contains("magic mismatch"), "got: {err}");
854    }
855
856    #[test]
857    fn test_deserialize_rejects_bad_crc() {
858        let mut blob = encode_dv_blob(&dv_of([1, 2, 3]));
859        let end = blob.len() - DV_CRC_BYTES;
860        blob[end] ^= 0xFF;
861        let err = DeleteVector::deserialize(&blob).unwrap_err();
862        assert!(err.message().contains("CRC mismatch"), "got: {err}");
863    }
864
865    #[test]
866    fn test_deserialize_rejects_length_prefix_mismatch() {
867        let mut blob = encode_dv_blob(&dv_of([1]));
868        let declared = u32::from_be_bytes(blob[..DV_LENGTH_PREFIX_BYTES].try_into().unwrap());
869        blob[..DV_LENGTH_PREFIX_BYTES].copy_from_slice(&(declared + 1).to_be_bytes());
870        let err = DeleteVector::deserialize(&blob).unwrap_err();
871        assert!(err.message().contains("length prefix"), "got: {err}");
872    }
873
874    // Crafts a raw roaring treemap directory (bitmap count header + key/bitmap entries) so tests
875    // can exercise key-ordering violations that `dv_of`/`encode_dv_blob` can never produce, since
876    // `DeleteVector::insert` always keeps keys unique and ascending.
877    fn raw_roaring_vector(entries: &[(u32, &[u32])]) -> Vec<u8> {
878        let mut vector = Vec::new();
879        vector.extend_from_slice(&(entries.len() as u64).to_le_bytes());
880        for (key, positions) in entries {
881            let mut bitmap = RoaringBitmap::new();
882            for &pos in *positions {
883                bitmap.insert(pos);
884            }
885            vector.extend_from_slice(&key.to_le_bytes());
886            bitmap.serialize_into(&mut vector).unwrap();
887        }
888        vector
889    }
890
891    // `RoaringTreemap::deserialize_from` stores keys in a `BTreeMap` via a plain insert, so
892    // without our own ordering check, decoding this would silently keep only the second bitmap
893    // for key 5 (position 2) and drop the first (position 1).
894    #[test]
895    fn test_deserialize_rejects_duplicate_keys() {
896        let vector = raw_roaring_vector(&[(5, &[1]), (5, &[2])]);
897        let err = DeleteVector::deserialize(&frame_dv_blob(&vector)).unwrap_err();
898        assert!(err.message().contains("unsigned comparison"), "got: {err}");
899    }
900
901    // The Puffin spec requires the roaring directory's keys to be "ordered by unsigned
902    // comparison"; a stream with unique but out-of-order keys is not a conformant blob even
903    // though `BTreeMap` would happily reorder it into a correct-looking result.
904    #[test]
905    fn test_deserialize_rejects_out_of_order_keys() {
906        let vector = raw_roaring_vector(&[(5, &[1]), (3, &[2])]);
907        let err = DeleteVector::deserialize(&frame_dv_blob(&vector)).unwrap_err();
908        assert!(err.message().contains("unsigned comparison"), "got: {err}");
909    }
910
911    // The roaring portable format stores the bitmap count as a u64 with the upper 32 bits
912    // reserved as zero padding, restricting it to [0, 2^32 - 1]; a value above that is not a
913    // conformant blob, regardless of whether any key/bitmap entries follow.
914    #[test]
915    fn test_deserialize_rejects_bitmap_count_overflow() {
916        let vector = (u32::MAX as u64 + 1).to_le_bytes().to_vec();
917        let err = DeleteVector::deserialize(&frame_dv_blob(&vector)).unwrap_err();
918        assert!(err.message().contains("exceeds the"), "got: {err}");
919    }
920}