This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new d8072a13 [core] Build deletion-vector row ranges from roaring runs 
(#1148)
d8072a13 is described below

commit d8072a1302d70ab3a0c654f4c01ef5d4c268caff
Author: JeonDaehong <[email protected]>
AuthorDate: Sun Oct 18 17:00:30 2026 +0900

    [core] Build deletion-vector row ranges from roaring runs (#1148)
---
 crates/paimon/src/deletion_vector/core.rs        | 104 +++++++++++++++++++++++
 crates/paimon/src/table/data_evolution_reader.rs |  12 +--
 crates/paimon/src/table/data_file_reader.rs      |  36 ++++++--
 3 files changed, 140 insertions(+), 12 deletions(-)

diff --git a/crates/paimon/src/deletion_vector/core.rs 
b/crates/paimon/src/deletion_vector/core.rs
index 99f700f2..e274e301 100644
--- a/crates/paimon/src/deletion_vector/core.rs
+++ b/crates/paimon/src/deletion_vector/core.rs
@@ -36,6 +36,9 @@ const MAGIC_NUMBER: u32 = 1581511376;
 /// unlike the v1 magic, so the two are told apart by byte order as well as 
value.
 const MAGIC_NUMBER_64: u32 = 1681511377;
 const MAGIC_NUMBER_SIZE_BYTES: u64 = 4;
+/// Adjacent deleted positions walked one by one before switching to
+/// `next_range` in [`DeletionVector::deleted_ranges`].
+const RUN_PROBE_LEN: u32 = 4;
 /// Size of the `bitmapLength` prefix and of the trailing CRC. Mirrors Java
 /// `Bitmap64DeletionVector.LENGTH_SIZE_BYTES` / `CRC_SIZE_BYTES`; a v2 entry
 /// counts both in `DeletionFile.length()` while a v1 entry counts neither.
@@ -93,6 +96,50 @@ impl DeletionVector {
         self.bitmap.is_empty()
     }
 
+    /// Deleted positions as ascending inclusive `(start, end)` runs.
+    ///
+    /// Short runs are walked with `next`; once a run is longer than
+    /// [`RUN_PROBE_LEN`], the rest of it is taken with `next_range`, one 
roaring
+    /// container at a time. `next_range` alone is slower than `next` on
+    /// scattered deletes.
+    pub(crate) fn deleted_ranges(&self) -> impl Iterator<Item = (u32, u32)> + 
'_ {
+        let mut iter = self.bitmap.iter();
+        let mut pending: Option<(u32, u32)> = None;
+        std::iter::from_fn(move || {
+            let (start, mut end) = match pending.take() {
+                Some(run) => run,
+                None => {
+                    let pos = iter.next()?;
+                    (pos, pos)
+                }
+            };
+            let mut walked = end - start;
+            while walked < RUN_PROBE_LEN {
+                match iter.next() {
+                    Some(pos) if end.checked_add(1) == Some(pos) => {
+                        end = pos;
+                        walked += 1;
+                    }
+                    Some(pos) => {
+                        pending = Some((pos, pos));
+                        return Some((start, end));
+                    }
+                    None => return Some((start, end)),
+                }
+            }
+            // A run split at a container boundary comes back in pieces; join 
them.
+            while let Some(run) = iter.next_range() {
+                if end.checked_add(1) == Some(*run.start()) {
+                    end = *run.end();
+                } else {
+                    pending = Some((*run.start(), *run.end()));
+                    break;
+                }
+            }
+            Some((start, end))
+        })
+    }
+
     /// Serialize using Java `BitmapDeletionVector` format:
     /// `i32 bitmapLength | i32 magic | roaring bitmap bytes | i32 crc`.
     pub(crate) fn serialize_to_bytes(&self) -> crate::Result<Vec<u8>> {
@@ -588,4 +635,61 @@ mod tests {
         // Positions above u32::MAX cannot exist in a roaring32 bitmap -> not 
deleted.
         assert!(!dv.is_deleted(u64::from(u32::MAX) + 1));
     }
+
+    /// Runs of `bitmap.iter()`, the obvious way.
+    fn model_ranges(bitmap: &RoaringBitmap) -> Vec<(u32, u32)> {
+        let mut ranges: Vec<(u32, u32)> = Vec::new();
+        for pos in bitmap {
+            match ranges.last_mut() {
+                Some((_, end)) if end.checked_add(1) == Some(pos) => *end = 
pos,
+                _ => ranges.push((pos, pos)),
+            }
+        }
+        ranges
+    }
+
+    fn assert_ranges(bitmap: RoaringBitmap) {
+        let expected = model_ranges(&bitmap);
+        let dv = DeletionVector::from_bitmap(bitmap);
+        assert_eq!(dv.deleted_ranges().collect::<Vec<_>>(), expected);
+    }
+
+    #[test]
+    fn test_deleted_ranges_edges() {
+        assert_ranges(RoaringBitmap::new());
+        assert_ranges(RoaringBitmap::from([7]));
+        // Runs right at the probe length, and one past it.
+        assert_ranges((10..14).chain(20..25).chain(30..31).collect());
+        // One run across several roaring containers, then a short one.
+        assert_ranges((65_530..200_000).chain([200_002]).collect());
+        // A long run followed by an adjacent-container run that does not 
touch it.
+        assert_ranges((0..65_535).chain(65_537..70_000).collect());
+        // Runs ending at u32::MAX.
+        assert_ranges([u32::MAX].into_iter().collect());
+        assert_ranges((u32::MAX - 70_000..=u32::MAX).collect());
+    }
+
+    #[test]
+    fn test_deleted_ranges_match_model_on_random_bitmaps() {
+        let mut state = 0x2545_f491_4f6c_dd1du64;
+        let mut next = move || {
+            state ^= state << 13;
+            state ^= state >> 7;
+            state ^= state << 17;
+            state
+        };
+        for round in 0..128 {
+            let max_gap = [2u64, 30, 3_000, 100_000][round % 4];
+            let max_len = [3u64, 50, 3_000, 70_000][(round / 4) % 4];
+            let mut bitmap = RoaringBitmap::new();
+            let mut pos = (next() % 200_000) as u32;
+            for _ in 0..40 {
+                let len = 1 + (next() % max_len) as u32;
+                let end = pos.saturating_add(len);
+                bitmap.insert_range(pos..end);
+                pos = end.saturating_add(1 + (next() % max_gap) as u32);
+            }
+            assert_ranges(bitmap);
+        }
+    }
 }
diff --git a/crates/paimon/src/table/data_evolution_reader.rs 
b/crates/paimon/src/table/data_evolution_reader.rs
index 15c0cdc9..d90f99c0 100644
--- a/crates/paimon/src/table/data_evolution_reader.rs
+++ b/crates/paimon/src/table/data_evolution_reader.rs
@@ -2149,15 +2149,15 @@ fn prefix_row_ranges(
 fn non_deleted_local_ranges(row_count: i64, deletion_vector: &DeletionVector) 
-> Vec<RowRange> {
     let mut ranges = Vec::new();
     let mut cursor = 0i64;
-    for deleted in deletion_vector.iter() {
-        let deleted = deleted as i64;
-        if deleted >= row_count {
+    for (start, end) in deletion_vector.deleted_ranges() {
+        let (start, end) = (i64::from(start), i64::from(end));
+        if start >= row_count {
             break;
         }
-        if deleted > cursor {
-            ranges.push(RowRange::new(cursor, deleted - 1));
+        if start > cursor {
+            ranges.push(RowRange::new(cursor, start - 1));
         }
-        cursor = deleted + 1;
+        cursor = end + 1;
     }
     if cursor < row_count {
         ranges.push(RowRange::new(cursor, row_count - 1));
diff --git a/crates/paimon/src/table/data_file_reader.rs 
b/crates/paimon/src/table/data_file_reader.rs
index 742771b3..c3edf965 100644
--- a/crates/paimon/src/table/data_file_reader.rs
+++ b/crates/paimon/src/table/data_file_reader.rs
@@ -1458,15 +1458,15 @@ fn ranges_cover_all_rows(ranges: &[RowRange], 
row_count: i64) -> bool {
 fn dv_to_non_deleted_ranges(dv: &DeletionVector, row_count: i64) -> 
Vec<RowRange> {
     let mut result = Vec::new();
     let mut cursor: i64 = 0;
-    for deleted in dv.iter() {
-        let del = deleted as i64;
-        if del >= row_count {
+    for (start, end) in dv.deleted_ranges() {
+        let (start, end) = (i64::from(start), i64::from(end));
+        if start >= row_count {
             break;
         }
-        if del > cursor {
-            result.push(RowRange::new(cursor, del - 1));
+        if start > cursor {
+            result.push(RowRange::new(cursor, start - 1));
         }
-        cursor = del + 1;
+        cursor = end + 1;
     }
     if cursor < row_count {
         result.push(RowRange::new(cursor, row_count - 1));
@@ -2546,6 +2546,30 @@ mod tests {
         );
     }
 
+    #[test]
+    fn dv_to_non_deleted_ranges_handles_long_runs() {
+        let dv = DeletionVector::from_bitmap;
+        // A run crossing roaring containers.
+        assert_eq!(
+            dv_to_non_deleted_ranges(&dv((70_000..140_000).collect()), 
200_000),
+            vec![RowRange::new(0, 69_999), RowRange::new(140_000, 199_999)]
+        );
+        // A run that goes past the end of the file.
+        assert_eq!(
+            dv_to_non_deleted_ranges(&dv((3..100_000).collect()), 50_000),
+            vec![RowRange::new(0, 2)]
+        );
+        // Positions at or past row_count are ignored.
+        assert_eq!(
+            
dv_to_non_deleted_ranges(&dv((0..10).chain(60_000..60_010).collect()), 50_000),
+            vec![RowRange::new(10, 49_999)]
+        );
+        assert_eq!(
+            dv_to_non_deleted_ranges(&dv((0..50_000).collect()), 50_000),
+            vec![]
+        );
+    }
+
     #[test]
     fn 
file_index_selection_coalesces_positions_and_rejects_out_of_range_values() {
         let selection = [0_u32, 1, 3].into_iter().collect();

Reply via email to