Skip to main content

nodedb_columnar/
delete_bitmap.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Per-segment delete bitmap for columnar UPDATE/DELETE.
4//!
5//! Uses roaring bitmaps for space-efficient tracking of deleted row indices.
6//! Each segment has at most one associated DeleteBitmap, stored alongside
7//! the segment as `{segment_id}.del`.
8//!
9//! The bitmap is serialized to bytes for persistence and deserialized on
10//! segment open. Scans consult the bitmap to skip deleted rows.
11
12use roaring::RoaringBitmap;
13
14use crate::error::ColumnarError;
15
16/// Per-segment bitmap of deleted row indices.
17///
18/// Wraps a roaring bitmap for O(1) membership checks and efficient
19/// serialization. Row indices are 0-based within the segment.
20#[derive(Debug, Clone)]
21pub struct DeleteBitmap {
22    inner: RoaringBitmap,
23}
24
25impl DeleteBitmap {
26    /// Create an empty delete bitmap.
27    pub fn new() -> Self {
28        Self {
29            inner: RoaringBitmap::new(),
30        }
31    }
32
33    /// Mark a row as deleted. Returns true if the row was newly deleted.
34    pub fn mark_deleted(&mut self, row_idx: u32) -> bool {
35        self.inner.insert(row_idx)
36    }
37
38    /// Mark multiple rows as deleted.
39    pub fn mark_deleted_batch(&mut self, row_indices: &[u32]) {
40        for &idx in row_indices {
41            self.inner.insert(idx);
42        }
43    }
44
45    /// Check whether a row is deleted.
46    pub fn is_deleted(&self, row_idx: u32) -> bool {
47        self.inner.contains(row_idx)
48    }
49
50    /// Number of deleted rows.
51    pub fn deleted_count(&self) -> u64 {
52        self.inner.len()
53    }
54
55    /// Whether the bitmap is empty (no deletions).
56    pub fn is_empty(&self) -> bool {
57        self.inner.is_empty()
58    }
59
60    /// Delete ratio: deleted_count / total_rows.
61    ///
62    /// Reported as a segment-health statistic on Origin, where segments are
63    /// never rewritten. Embedded deployments act on it via [`Self::should_compact`].
64    pub fn delete_ratio(&self, total_rows: u64) -> f64 {
65        if total_rows == 0 {
66            return 0.0;
67        }
68        self.inner.len() as f64 / total_rows as f64
69    }
70
71    /// Whether this segment should be compacted (delete ratio > threshold).
72    pub fn should_compact(&self, total_rows: u64, threshold: f64) -> bool {
73        self.delete_ratio(total_rows) > threshold
74    }
75
76    /// Check whether an entire block is fully deleted.
77    ///
78    /// `block_start` is the first row index of the block, `block_len` is
79    /// the number of rows. Returns true if every row in the range is deleted.
80    pub fn is_block_fully_deleted(&self, block_start: u32, block_len: u32) -> bool {
81        if block_len == 0 {
82            return true;
83        }
84        // Count deleted rows in the range [block_start, block_start + block_len).
85        let count = self
86            .inner
87            .range(block_start..block_start + block_len)
88            .count() as u32;
89        count == block_len
90    }
91
92    /// Apply the delete bitmap to a validity vector: set deleted rows to false.
93    ///
94    /// `global_offset` is the row index of the first element in `valid`
95    /// (used when processing a block that starts at a non-zero row).
96    pub fn apply_to_validity(&self, valid: &mut [bool], global_offset: u32) {
97        for (i, v) in valid.iter_mut().enumerate() {
98            if self.inner.contains(global_offset + i as u32) {
99                *v = false;
100            }
101        }
102    }
103
104    /// Serialize the bitmap to bytes for persistence.
105    pub fn to_bytes(&self) -> Result<Vec<u8>, ColumnarError> {
106        let mut buf = Vec::with_capacity(self.inner.serialized_size());
107        self.inner
108            .serialize_into(&mut buf)
109            .map_err(|e| ColumnarError::Serialization(format!("delete bitmap: {e}")))?;
110        Ok(buf)
111    }
112
113    /// Deserialize a bitmap from bytes.
114    pub fn from_bytes(data: &[u8]) -> Result<Self, ColumnarError> {
115        let inner = RoaringBitmap::deserialize_from(data)
116            .map_err(|e| ColumnarError::Serialization(format!("delete bitmap: {e}")))?;
117        Ok(Self { inner })
118    }
119
120    /// Iterate over all deleted row indices.
121    pub fn iter(&self) -> impl Iterator<Item = u32> + '_ {
122        self.inner.iter()
123    }
124
125    /// Remove a row from the deleted set (undo a tombstone).
126    ///
127    /// Used exclusively by transaction rollback to clear tombstones that
128    /// were set by a preceding insert's upsert path. Returns true if the
129    /// row was previously deleted, false if it was already live.
130    pub fn unmark_deleted(&mut self, row_idx: u32) -> bool {
131        self.inner.remove(row_idx)
132    }
133
134    /// Remove every row index at or above `start` from the deleted set.
135    ///
136    /// Used when the rows from `start` on are cut from a memtable: an index
137    /// that later holds a new row must not inherit a tombstone.
138    pub fn unmark_from(&mut self, start: u32) {
139        self.inner.remove_range(start..);
140    }
141
142    /// Merge another bitmap into this one (union).
143    ///
144    /// Used when two views of the same segment's tombstones must be combined
145    /// (for example a restored bitmap and one accumulated since the restore).
146    pub fn merge(&mut self, other: &DeleteBitmap) {
147        self.inner |= &other.inner;
148    }
149}
150
151impl Default for DeleteBitmap {
152    fn default() -> Self {
153        Self::new()
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn mark_and_check() {
163        let mut bm = DeleteBitmap::new();
164        assert!(!bm.is_deleted(5));
165        assert!(bm.mark_deleted(5));
166        assert!(bm.is_deleted(5));
167        assert!(!bm.mark_deleted(5)); // Already deleted.
168        assert_eq!(bm.deleted_count(), 1);
169    }
170
171    #[test]
172    fn unmark_from_clears_the_tail_only() {
173        let mut bm = DeleteBitmap::new();
174        bm.mark_deleted_batch(&[1, 4, 5, 9]);
175        bm.unmark_from(4);
176        assert!(bm.is_deleted(1));
177        assert!(!bm.is_deleted(4));
178        assert!(!bm.is_deleted(5));
179        assert!(!bm.is_deleted(9));
180        assert_eq!(bm.deleted_count(), 1);
181    }
182
183    #[test]
184    fn batch_delete() {
185        let mut bm = DeleteBitmap::new();
186        bm.mark_deleted_batch(&[1, 3, 5, 7, 9]);
187        assert_eq!(bm.deleted_count(), 5);
188        assert!(bm.is_deleted(3));
189        assert!(!bm.is_deleted(4));
190    }
191
192    #[test]
193    fn serialization_roundtrip() {
194        let mut bm = DeleteBitmap::new();
195        bm.mark_deleted_batch(&[0, 10, 100, 1000, 10000]);
196
197        let bytes = bm.to_bytes().unwrap();
198        let restored = DeleteBitmap::from_bytes(&bytes).expect("deserialize");
199
200        assert_eq!(restored.deleted_count(), 5);
201        assert!(restored.is_deleted(0));
202        assert!(restored.is_deleted(10));
203        assert!(restored.is_deleted(10000));
204        assert!(!restored.is_deleted(50));
205    }
206
207    #[test]
208    fn delete_ratio_reports_fraction_deleted() {
209        let mut bm = DeleteBitmap::new();
210        for i in 0..30 {
211            bm.mark_deleted(i);
212        }
213        // 30 deleted out of 100 total = 0.3
214        assert!((bm.delete_ratio(100) - 0.3).abs() < 0.001);
215        // A zero-row segment must not divide by zero.
216        assert_eq!(DeleteBitmap::new().delete_ratio(0), 0.0);
217        // Default threshold 0.2 → should compact.
218        assert!(bm.should_compact(100, 0.2));
219        // Threshold 0.5 → should not compact.
220        assert!(!bm.should_compact(100, 0.5));
221    }
222
223    #[test]
224    fn block_fully_deleted() {
225        let mut bm = DeleteBitmap::new();
226        // Delete rows 10..20.
227        for i in 10..20 {
228            bm.mark_deleted(i);
229        }
230        assert!(bm.is_block_fully_deleted(10, 10));
231        assert!(!bm.is_block_fully_deleted(10, 11)); // Row 20 is not deleted.
232        assert!(!bm.is_block_fully_deleted(5, 10)); // Rows 5-9 are not deleted.
233    }
234
235    #[test]
236    fn apply_to_validity() {
237        let mut bm = DeleteBitmap::new();
238        bm.mark_deleted_batch(&[2, 4]);
239
240        let mut valid = vec![true, true, true, true, true];
241        bm.apply_to_validity(&mut valid, 0);
242
243        assert!(valid[0]);
244        assert!(valid[1]);
245        assert!(!valid[2]); // Deleted.
246        assert!(valid[3]);
247        assert!(!valid[4]); // Deleted.
248    }
249
250    #[test]
251    fn apply_to_validity_with_offset() {
252        let mut bm = DeleteBitmap::new();
253        bm.mark_deleted(1025); // Row in second block.
254
255        let mut valid = vec![true; 1024];
256        bm.apply_to_validity(&mut valid, 1024); // Block starts at row 1024.
257
258        assert!(valid[0]); // Global row 1024: not deleted.
259        assert!(!valid[1]); // Global row 1025: deleted.
260        assert!(valid[2]); // Global row 1026: not deleted.
261    }
262
263    #[test]
264    fn merge_bitmaps() {
265        let mut bm1 = DeleteBitmap::new();
266        bm1.mark_deleted_batch(&[1, 2, 3]);
267
268        let mut bm2 = DeleteBitmap::new();
269        bm2.mark_deleted_batch(&[3, 4, 5]);
270
271        bm1.merge(&bm2);
272        assert_eq!(bm1.deleted_count(), 5); // Union: {1,2,3,4,5}.
273    }
274
275    #[test]
276    fn empty_bitmap() {
277        let bm = DeleteBitmap::new();
278        assert!(bm.is_empty());
279        assert_eq!(bm.deleted_count(), 0);
280        assert!(!bm.is_deleted(0));
281
282        // Serialization roundtrip of empty bitmap.
283        let bytes = bm.to_bytes().unwrap();
284        let restored = DeleteBitmap::from_bytes(&bytes).expect("deserialize");
285        assert!(restored.is_empty());
286    }
287}