velesdb_core/column_store/
vacuum.rs1use super::types::{TypedColumn, VacuumConfig, VacuumStats};
7use super::ColumnStore;
8
9use roaring::RoaringBitmap;
10use std::collections::HashMap;
11
12#[inline]
16fn is_deleted(deleted: &RoaringBitmap, idx: usize) -> bool {
17 u32::try_from(idx).is_ok_and(|i| deleted.contains(i))
18}
19
20fn 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
34fn 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 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 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 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 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 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 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 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 #[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 #[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 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 #[must_use]
226 pub fn deletion_bitmap(&self) -> &RoaringBitmap {
227 &self.deletion_bitmap
228 }
229
230 #[must_use]
232 pub fn deleted_count_bitmap(&self) -> u64 {
233 self.deletion_bitmap.len()
234 }
235}