mongreldb_core/
compaction.rs1use crate::engine::Table;
8use crate::epoch::Epoch;
9use crate::manifest::RunRef;
10use crate::memtable::Row;
11use crate::sorted_run::RunWriter;
12use crate::Result;
13use std::collections::HashMap;
14
15impl Table {
16 pub const AUTO_COMPACT_RUN_THRESHOLD: usize = 8;
21
22 pub fn should_compact(&self) -> bool {
28 self.run_refs().len() >= Self::AUTO_COMPACT_RUN_THRESHOLD
29 || (self.ttl().is_some()
30 && !self.run_refs().is_empty()
31 && self.has_expired_run_rows().unwrap_or(false))
32 }
33
34 fn has_expired_run_rows(&self) -> Result<bool> {
35 let now_nanos = crate::engine::unix_nanos_now();
36 for run in self.run_refs() {
37 let mut reader = self.open_reader(run.run_id)?;
38 if reader
39 .all_rows()?
40 .iter()
41 .any(|row| self.row_expired_at(row, now_nanos))
42 {
43 return Ok(true);
44 }
45 }
46 Ok(false)
47 }
48
49 pub fn maybe_compact(&mut self) -> Result<bool> {
55 if !self.should_compact() {
56 return Ok(false);
57 }
58 self.compact()?;
59 Ok(true)
60 }
61
62 pub fn compact(&mut self) -> Result<()> {
67 let reclaim_ttl = self.ttl().is_some() && self.has_expired_run_rows()?;
68 if self.run_refs().len() < 2 && !reclaim_ttl {
69 return Ok(());
70 }
71 let min_active = self.min_active_snapshot();
72 let old_refs: Vec<RunRef> = self.run_refs().to_vec();
73 let now_nanos = crate::engine::unix_nanos_now();
74
75 let mutable_rows = if self.mutable_run_len() > 0 {
80 self.drain_mutable_run()
81 } else {
82 Vec::new()
83 };
84
85 let mut all: HashMap<u64, Vec<Row>> = HashMap::new();
87 for rr in &old_refs {
88 let mut reader = self.open_reader(rr.run_id)?;
89 for row in reader.all_rows()? {
90 all.entry(row.row_id.0).or_default().push(row);
91 }
92 }
93 for row in mutable_rows {
95 all.entry(row.row_id.0).or_default().push(row);
96 }
97
98 let mut rows: Vec<Row> = Vec::new();
99 let mut expired_reclaimed = false;
100 let mut current_live_count = 0u64;
101 for (_, mut vers) in all {
102 vers.sort_by_key(|r| r.committed_epoch);
103 let newest = vers.last().expect("at least one version");
104 let newest_epoch = newest.committed_epoch;
105 if !newest.deleted && !self.row_expired_at(newest, now_nanos) {
106 current_live_count += 1;
107 }
108 for row in select_keep(&vers, min_active) {
109 if self.row_expired_at(&row, now_nanos) {
110 expired_reclaimed = true;
111 if row.committed_epoch == newest_epoch
116 && min_active.is_some_and(|epoch| newest_epoch > epoch)
117 {
118 let mut tombstone = row;
119 tombstone.deleted = true;
120 tombstone.columns.clear();
121 rows.push(tombstone);
122 }
123 } else {
124 rows.push(row);
125 }
126 }
127 }
128 rows.sort_by_key(|r| (r.row_id, r.committed_epoch));
129
130 self.live_count = current_live_count;
133
134 let retire_epoch = self.current_epoch().0;
135 if rows.is_empty() {
136 self.set_run_refs(Vec::new());
142 for rr in &old_refs {
143 self.retire_run(rr.run_id, retire_epoch);
144 }
145 self.persist_manifest(self.current_epoch())?;
146 self.mark_indexes_incomplete();
149 self.bump_data_generation();
150 return Ok(());
151 }
152
153 let run_id = self.next_run_id();
154 self.bump_next_run_id();
155 let path = self.run_path(run_id);
156 let kek = self.kek();
157 let mut writer = RunWriter::new(self.schema(), run_id as u128, self.current_epoch(), 1)
158 .clean(min_active.is_none())
159 .with_zstd_level(self.compaction_zstd_level());
160 if let Some(k) = &kek {
161 writer = writer.with_encryption(k.as_ref(), self.indexable_column_specs());
162 }
163 let header = writer.write(&path, &rows)?;
164
165 self.set_run_refs(vec![RunRef {
173 run_id: run_id as u128,
174 level: 1,
175 epoch_created: header.epoch_created,
176 row_count: header.row_count,
177 }]);
178 for rr in &old_refs {
179 self.retire_run(rr.run_id, retire_epoch);
180 }
181 self.persist_manifest(self.current_epoch())?;
182 self.rebuild_indexes_from_runs()?;
185 self.build_learned_ranges()?;
189 self.clear_result_cache();
190 let _ = expired_reclaimed;
191 self.checkpoint_indexes(self.current_epoch());
192 self.bump_data_generation();
193 Ok(())
194 }
195}
196
197fn select_keep(vers: &[Row], min_active: Option<Epoch>) -> Vec<Row> {
201 let newest = vers.last().expect("at least one version").clone();
202 match min_active {
203 None => {
204 if newest.deleted {
205 Vec::new()
206 } else {
207 vec![newest]
208 }
209 }
210 Some(min_e) => {
211 let recent_start = vers.partition_point(|row| row.committed_epoch < min_e);
212 let mut keep = vers[recent_start..].to_vec();
213 if recent_start > 0 && keep.first().map_or(true, |row| row.committed_epoch > min_e) {
214 let boundary = vers[recent_start - 1].clone();
215 if keep.is_empty() && boundary.deleted {
216 return Vec::new();
217 }
218 keep.insert(0, boundary);
219 }
220 keep
221 }
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
229 use crate::{Snapshot, Value};
230 use tempfile::tempdir;
231
232 fn schema() -> Schema {
233 Schema {
234 schema_id: 1,
235 columns: vec![ColumnDef {
236 id: 1,
237 name: "v".into(),
238 ty: TypeId::Int64,
239 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
240 default_value: None,
241 }],
242 indexes: Vec::new(),
243 colocation: vec![],
244 constraints: Default::default(),
245 clustered: false,
246 }
247 }
248
249 #[test]
250 fn compaction_merges_runs_and_gcs_tombstoned_row() {
251 let dir = tempdir().unwrap();
252 let mut db = Table::create(dir.path(), schema(), 1).unwrap();
253 db.set_mutable_run_spill_bytes(1);
255 let mut ids = Vec::new();
256 for i in 1..=5i64 {
257 ids.push(db.put(vec![(1, Value::Int64(i))]).unwrap());
258 }
259 db.flush().unwrap();
260 db.delete(ids[2]).unwrap();
261 db.flush().unwrap();
262 db.put(vec![(1, Value::Int64(60))]).unwrap();
263 db.flush().unwrap();
264 assert_eq!(db.run_count(), 3);
265
266 db.compact().unwrap();
267 assert_eq!(db.run_count(), 1);
268 let rows = db.visible_rows(db.snapshot()).unwrap();
269 let row_ids: Vec<u64> = rows.iter().map(|r| r.row_id.0).collect();
270 assert!(!row_ids.contains(&ids[2].0), "tombstoned row must be GC'd");
271 assert_eq!(rows.len(), 5);
272 }
273
274 #[test]
275 fn pinned_snapshot_survives_compaction() {
276 let dir = tempdir().unwrap();
277 let mut db = Table::create(dir.path(), schema(), 1).unwrap();
278 let r = db.put(vec![(1, Value::Int64(1))]).unwrap();
279 db.flush().unwrap(); let pinned = db.pin_snapshot();
283 assert_eq!(
284 db.get(r, pinned)
285 .and_then(|row| row.columns.get(&1).cloned()),
286 Some(Value::Int64(1))
287 );
288
289 db.delete(r).unwrap();
291 db.commit().unwrap();
292 db.flush().unwrap(); db.compact().unwrap(); assert_eq!(
297 db.get(r, pinned)
298 .and_then(|row| row.columns.get(&1).cloned()),
299 Some(Value::Int64(1))
300 );
301 assert_eq!(
303 db.get(r, db.snapshot())
304 .and_then(|row| row.columns.get(&1).cloned()),
305 None
306 );
307
308 db.unpin_snapshot(pinned);
310 db.compact().unwrap();
311 assert_eq!(
312 db.get(r, db.snapshot())
313 .and_then(|row| row.columns.get(&1).cloned()),
314 None
315 );
316 }
317
318 #[test]
319 fn _snapshot_import_used() {
320 let _ = Snapshot::at(Epoch(0));
321 }
322}