Skip to main content

velesdb_core/column_store/
vacuum.rs

1//! Vacuum and compaction operations for `ColumnStore`.
2//!
3//! Extracted from `mod.rs` for maintainability (04-06 module splitting).
4//! Handles tombstone removal, column compaction, and deletion bitmap operations.
5
6use super::types::{TypedColumn, VacuumConfig, VacuumStats};
7use super::ColumnStore;
8
9use roaring::RoaringBitmap;
10use std::collections::HashMap;
11
12/// Returns whether `idx` is tombstoned in the deletion bitmap.
13///
14/// Indices that don't fit `u32` can never be tombstoned, so they count as live.
15#[inline]
16fn is_deleted(deleted: &RoaringBitmap, idx: usize) -> bool {
17    u32::try_from(idx).is_ok_and(|i| deleted.contains(i))
18}
19
20/// Filters a column vector, removing entries at deleted indices and counting reclaimed bytes.
21fn compact_vec<T: Copy>(data: &[T], deleted: &RoaringBitmap, element_bytes: u64) -> (Vec<T>, u64) {
22    let mut new_data = Vec::with_capacity(data.len().saturating_sub(deleted.len() as usize));
23    let mut bytes_reclaimed = 0u64;
24    for (idx, value) in data.iter().enumerate() {
25        if is_deleted(deleted, idx) {
26            bytes_reclaimed += element_bytes;
27        } else {
28            new_data.push(*value);
29        }
30    }
31    (new_data, bytes_reclaimed)
32}
33
34/// Clone-based variant of `compact_vec` for non-Copy types (e.g., `SmallVec`).
35fn compact_vec_clone<T: Clone>(
36    data: &[T],
37    deleted: &RoaringBitmap,
38    element_bytes: u64,
39) -> (Vec<T>, u64) {
40    let mut new_data = Vec::with_capacity(data.len().saturating_sub(deleted.len() as usize));
41    let mut bytes_reclaimed = 0u64;
42    for (idx, value) in data.iter().enumerate() {
43        if is_deleted(deleted, idx) {
44            bytes_reclaimed += element_bytes;
45        } else {
46            new_data.push(value.clone());
47        }
48    }
49    (new_data, bytes_reclaimed)
50}
51
52impl ColumnStore {
53    /// Runs vacuum to remove tombstones and compact data.
54    ///
55    /// This operation removes deleted rows from the column store, reclaiming
56    /// space and improving query performance. The operation is done in-place
57    /// by building new column vectors without the deleted rows.
58    ///
59    /// # Arguments
60    ///
61    /// * `_config` - Vacuum configuration. **Currently ignored**: the vacuum
62    ///   runs as a single in-memory pass (`batch_size`, `sync`, and
63    ///   `yield_interval_ms` have no effect).
64    ///
65    /// # Returns
66    ///
67    /// Statistics about the vacuum operation.
68    pub fn vacuum(&mut self, _config: VacuumConfig) -> VacuumStats {
69        let start = std::time::Instant::now();
70        let tombstones_found = self.deletion_bitmap.len() as usize;
71
72        if tombstones_found == 0 {
73            return VacuumStats {
74                tombstones_found: 0,
75                completed: true,
76                duration_ms: start.elapsed().as_millis() as u64,
77                ..Default::default()
78            };
79        }
80
81        let mut stats = VacuumStats {
82            tombstones_found,
83            ..Default::default()
84        };
85
86        let (old_to_new, new_row_count) = self.build_compaction_map();
87        self.compact_all_columns(&mut stats);
88        self.remap_primary_index(&old_to_new);
89        self.remap_row_expiry(&old_to_new);
90
91        stats.tombstones_removed = self.deletion_bitmap.len() as usize;
92        self.deletion_bitmap.clear();
93        self.row_count = new_row_count;
94
95        stats.completed = true;
96        stats.duration_ms = start.elapsed().as_millis() as u64;
97        stats
98    }
99
100    /// Builds the old-to-new row index mapping, skipping deleted rows.
101    fn build_compaction_map(&self) -> (HashMap<usize, usize>, usize) {
102        let mut old_to_new: HashMap<usize, usize> = HashMap::new();
103        let mut new_idx = 0;
104        for old_idx in 0..self.row_count {
105            if !self.is_row_deleted_bitmap(old_idx) {
106                old_to_new.insert(old_idx, new_idx);
107                new_idx += 1;
108            }
109        }
110        (old_to_new, new_idx)
111    }
112
113    /// Compacts all columns, removing deleted rows and accumulating reclaimed bytes.
114    fn compact_all_columns(&mut self, stats: &mut VacuumStats) {
115        for column in self.columns.values_mut() {
116            let (new_col, bytes) = Self::compact_column(column, &self.deletion_bitmap);
117            stats.bytes_reclaimed += bytes;
118            *column = new_col;
119        }
120    }
121
122    /// Remaps the primary index to use compacted row indices.
123    fn remap_primary_index(&mut self, old_to_new: &HashMap<usize, usize>) {
124        if self.primary_key_column.is_none() {
125            return;
126        }
127        let mut new_primary_index: HashMap<i64, usize> = HashMap::new();
128        let mut new_row_idx_to_pk: HashMap<usize, i64> = HashMap::new();
129        for (pk, old_idx) in &self.primary_index {
130            if let Some(&new_idx) = old_to_new.get(old_idx) {
131                new_primary_index.insert(*pk, new_idx);
132                new_row_idx_to_pk.insert(new_idx, *pk);
133            }
134        }
135        self.primary_index = new_primary_index;
136        self.row_idx_to_pk = new_row_idx_to_pk;
137    }
138
139    /// Remaps row expiry timestamps to compacted row indices.
140    fn remap_row_expiry(&mut self, old_to_new: &HashMap<usize, usize>) {
141        let mut new_row_expiry: HashMap<usize, u64> = HashMap::new();
142        for (old_idx, expiry) in &self.row_expiry {
143            if let Some(&new_idx) = old_to_new.get(old_idx) {
144                new_row_expiry.insert(new_idx, *expiry);
145            }
146        }
147        self.row_expiry = new_row_expiry;
148    }
149
150    /// Compacts a single column by removing deleted rows.
151    fn compact_column(column: &TypedColumn, deleted: &RoaringBitmap) -> (TypedColumn, u64) {
152        match column {
153            TypedColumn::Int(data) => {
154                let (new_data, bytes) = compact_vec(data, deleted, 8);
155                (TypedColumn::Int(new_data), bytes)
156            }
157            TypedColumn::Float(data) => {
158                let (new_data, bytes) = compact_vec(data, deleted, 8);
159                (TypedColumn::Float(new_data), bytes)
160            }
161            TypedColumn::String(data) => {
162                let (new_data, bytes) = compact_vec(data, deleted, 4);
163                (TypedColumn::String(new_data), bytes)
164            }
165            TypedColumn::Bool(data) => {
166                let (new_data, bytes) = compact_vec(data, deleted, 1);
167                (TypedColumn::Bool(new_data), bytes)
168            }
169            TypedColumn::Array {
170                element_type, data, ..
171            } => {
172                // Estimate ~64 bytes per array cell (SmallVec overhead).
173                let (new_data, bytes) = compact_vec_clone(data, deleted, 64);
174                (
175                    TypedColumn::Array {
176                        element_type: element_type.clone(),
177                        data: new_data,
178                    },
179                    bytes,
180                )
181            }
182            TypedColumn::GeoPoint(data) => {
183                let (new_data, bytes) = compact_vec(data, deleted, 16);
184                (TypedColumn::GeoPoint(new_data), bytes)
185            }
186        }
187    }
188
189    /// Returns whether vacuum is recommended based on tombstone ratio.
190    ///
191    /// # Arguments
192    ///
193    /// * `threshold` - Ratio of deleted rows to trigger vacuum (0.0-1.0)
194    #[must_use]
195    pub fn should_vacuum(&self, threshold: f64) -> bool {
196        if self.row_count == 0 {
197            return false;
198        }
199        let ratio = self.deletion_bitmap.len() as f64 / self.row_count as f64;
200        ratio >= threshold
201    }
202
203    // =========================================================================
204    // EPIC-043 US-002: RoaringBitmap Filtering
205    // =========================================================================
206
207    /// Checks if a row is deleted using RoaringBitmap (O(1) lookup).
208    ///
209    /// Single source of truth for deletion state. Indices >= `u32::MAX` cannot
210    /// be tombstoned (the bitmap is `u32`-indexed) and are reported as live.
211    #[must_use]
212    #[inline]
213    pub fn is_row_deleted_bitmap(&self, row_idx: usize) -> bool {
214        u32::try_from(row_idx).is_ok_and(|idx| self.deletion_bitmap.contains(idx))
215    }
216
217    /// Returns an iterator over live (non-deleted) row indices.
218    ///
219    /// Uses RoaringBitmap for efficient filtering.
220    pub fn live_row_indices(&self) -> impl Iterator<Item = usize> + '_ {
221        (0..self.row_count).filter(|&idx| !self.is_row_deleted_bitmap(idx))
222    }
223
224    /// Returns the deletion bitmap for advanced filtering operations.
225    #[must_use]
226    pub fn deletion_bitmap(&self) -> &RoaringBitmap {
227        &self.deletion_bitmap
228    }
229
230    /// Returns the number of deleted rows using the bitmap (O(1)).
231    #[must_use]
232    pub fn deleted_count_bitmap(&self) -> u64 {
233        self.deletion_bitmap.len()
234    }
235}