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 fn compact(&mut self) -> Result<()> {
20 if self.run_refs().len() < 2 {
21 return Ok(());
22 }
23 let min_active = self.min_active_snapshot();
24 let old_refs: Vec<RunRef> = self.run_refs().to_vec();
25
26 let mutable_rows = if self.mutable_run_len() > 0 {
31 self.drain_mutable_run()
32 } else {
33 Vec::new()
34 };
35
36 let mut all: HashMap<u64, Vec<Row>> = HashMap::new();
38 for rr in &old_refs {
39 let mut reader = self.open_reader(rr.run_id)?;
40 for row in reader.all_rows()? {
41 all.entry(row.row_id.0).or_default().push(row);
42 }
43 }
44 for row in mutable_rows {
46 all.entry(row.row_id.0).or_default().push(row);
47 }
48
49 let mut rows: Vec<Row> = Vec::new();
50 for (_, mut vers) in all {
51 vers.sort_by_key(|r| r.committed_epoch);
52 rows.extend(select_keep(&vers, min_active));
53 }
54 rows.sort_by_key(|r| (r.row_id, r.committed_epoch));
55
56 self.live_count = rows.iter().filter(|r| !r.deleted).count() as u64;
58
59 let retire_epoch = self.current_epoch().0;
60 if rows.is_empty() {
61 self.set_run_refs(Vec::new());
67 for rr in &old_refs {
68 self.retire_run(rr.run_id, retire_epoch);
69 }
70 self.persist_manifest(self.current_epoch())?;
71 self.invalidate_index_checkpoint();
74 return Ok(());
75 }
76
77 let run_id = self.next_run_id();
78 self.bump_next_run_id();
79 let path = self.run_path(run_id);
80 let kek = self.kek();
81 let mut writer = RunWriter::new(self.schema(), run_id as u128, self.current_epoch(), 1)
82 .clean(min_active.is_none())
83 .with_zstd_level(self.compaction_zstd_level());
84 if let Some(k) = &kek {
85 writer = writer.with_encryption(k.as_ref(), self.indexable_column_specs());
86 }
87 let header = writer.write(&path, &rows)?;
88
89 self.set_run_refs(vec![RunRef {
97 run_id: run_id as u128,
98 level: 1,
99 epoch_created: header.epoch_created,
100 row_count: header.row_count,
101 }]);
102 for rr in &old_refs {
103 self.retire_run(rr.run_id, retire_epoch);
104 }
105 self.persist_manifest(self.current_epoch())?;
106 self.build_learned_ranges()?;
110 self.clear_result_cache();
111 self.checkpoint_indexes(self.current_epoch());
112 Ok(())
113 }
114}
115
116fn select_keep(vers: &[Row], min_active: Option<Epoch>) -> Vec<Row> {
121 let newest = vers.last().expect("at least one version").clone();
122 match min_active {
123 None => {
124 if newest.deleted {
125 Vec::new()
126 } else {
127 vec![newest]
128 }
129 }
130 Some(min_e) => {
131 let mut keep = vec![newest.clone()];
132 if let Some(v) = vers.iter().rev().find(|v| v.committed_epoch <= min_e) {
134 if v.committed_epoch != newest.committed_epoch {
135 keep.push(v.clone());
136 }
137 }
138 keep
139 }
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
147 use crate::{Snapshot, Value};
148 use tempfile::tempdir;
149
150 fn schema() -> Schema {
151 Schema {
152 schema_id: 1,
153 columns: vec![ColumnDef {
154 id: 1,
155 name: "v".into(),
156 ty: TypeId::Int64,
157 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
158 }],
159 indexes: Vec::new(),
160 colocation: vec![],
161 constraints: Default::default(),
162 }
163 }
164
165 #[test]
166 fn compaction_merges_runs_and_gcs_tombstoned_row() {
167 let dir = tempdir().unwrap();
168 let mut db = Table::create(dir.path(), schema(), 1).unwrap();
169 db.set_mutable_run_spill_bytes(1);
171 let mut ids = Vec::new();
172 for i in 1..=5i64 {
173 ids.push(db.put(vec![(1, Value::Int64(i))]).unwrap());
174 }
175 db.flush().unwrap();
176 db.delete(ids[2]).unwrap();
177 db.flush().unwrap();
178 db.put(vec![(1, Value::Int64(60))]).unwrap();
179 db.flush().unwrap();
180 assert_eq!(db.run_count(), 3);
181
182 db.compact().unwrap();
183 assert_eq!(db.run_count(), 1);
184 let rows = db.visible_rows(db.snapshot()).unwrap();
185 let row_ids: Vec<u64> = rows.iter().map(|r| r.row_id.0).collect();
186 assert!(!row_ids.contains(&ids[2].0), "tombstoned row must be GC'd");
187 assert_eq!(rows.len(), 5);
188 }
189
190 #[test]
191 fn pinned_snapshot_survives_compaction() {
192 let dir = tempdir().unwrap();
193 let mut db = Table::create(dir.path(), schema(), 1).unwrap();
194 let r = db.put(vec![(1, Value::Int64(1))]).unwrap();
195 db.flush().unwrap(); let pinned = db.pin_snapshot();
199 assert_eq!(
200 db.get(r, pinned)
201 .and_then(|row| row.columns.get(&1).cloned()),
202 Some(Value::Int64(1))
203 );
204
205 db.delete(r).unwrap();
207 db.commit().unwrap();
208 db.flush().unwrap(); db.compact().unwrap(); assert_eq!(
213 db.get(r, pinned)
214 .and_then(|row| row.columns.get(&1).cloned()),
215 Some(Value::Int64(1))
216 );
217 assert_eq!(
219 db.get(r, db.snapshot())
220 .and_then(|row| row.columns.get(&1).cloned()),
221 None
222 );
223
224 db.unpin_snapshot(pinned);
226 db.compact().unwrap();
227 assert_eq!(
228 db.get(r, db.snapshot())
229 .and_then(|row| row.columns.get(&1).cloned()),
230 None
231 );
232 }
233
234 #[test]
235 fn _snapshot_import_used() {
236 let _ = Snapshot::at(Epoch(0));
237 }
238}