1use crate::engine::Table;
8use crate::epoch::{Epoch, MaintenanceReceipt};
9use crate::manifest::RunRef;
10use crate::memtable::Row;
11use crate::sorted_run::RunWriter;
12use crate::{ExecutionControl, MongrelError, Result};
13use std::collections::HashMap;
14use std::path::Path;
15
16impl Table {
17 pub const AUTO_COMPACT_RUN_THRESHOLD: usize = 8;
22
23 pub fn should_compact(&self) -> bool {
29 self.run_refs().len() >= Self::AUTO_COMPACT_RUN_THRESHOLD
30 || (self.ttl().is_some()
31 && !self.run_refs().is_empty()
32 && self.has_expired_run_rows().unwrap_or(false))
33 }
34
35 fn has_expired_run_rows(&self) -> Result<bool> {
36 self.has_expired_run_rows_inner(None)
37 }
38
39 fn has_expired_run_rows_inner(&self, control: Option<&ExecutionControl>) -> Result<bool> {
40 let now_nanos = crate::engine::unix_nanos_now();
41 for (run_index, run) in self.run_refs().iter().enumerate() {
42 if run_index % 256 == 0 {
43 if let Some(control) = control {
44 control.checkpoint()?;
45 }
46 }
47 let mut reader = self.open_reader(run.run_id)?;
48 for (row_index, row) in reader.all_rows()?.iter().enumerate() {
49 if row_index % 256 == 0 {
50 if let Some(control) = control {
51 control.checkpoint()?;
52 }
53 }
54 if self.row_expired_at(row, now_nanos) {
55 return Ok(true);
56 }
57 }
58 }
59 Ok(false)
60 }
61
62 pub fn maybe_compact(&mut self) -> Result<bool> {
68 if !self.should_compact() {
69 return Ok(false);
70 }
71 self.compact()?;
72 Ok(true)
73 }
74
75 pub fn compact(&mut self) -> Result<()> {
80 let control = ExecutionControl::new(None);
81 self.compact_controlled(&control, || true).map(|_| ())
82 }
83
84 #[doc(hidden)]
88 pub fn compact_controlled<F>(
89 &mut self,
90 control: &ExecutionControl,
91 before_publish: F,
92 ) -> Result<bool>
93 where
94 F: FnOnce() -> bool,
95 {
96 self.compact_controlled_with_receipt(control, before_publish)
97 .map(|(changed, _)| changed)
98 }
99
100 #[doc(hidden)]
103 pub fn compact_controlled_with_receipt<F>(
104 &mut self,
105 control: &ExecutionControl,
106 before_publish: F,
107 ) -> Result<(bool, Option<MaintenanceReceipt>)>
108 where
109 F: FnOnce() -> bool,
110 {
111 control.checkpoint()?;
112 let maintenance_epoch = self.current_epoch();
113 let reclaim_ttl = self.ttl().is_some() && self.has_expired_run_rows_inner(Some(control))?;
114 if self.run_refs().len() < 2 && !reclaim_ttl {
115 return Ok((false, None));
116 }
117 let min_active = self.min_active_snapshot();
118 let old_refs: Vec<RunRef> = self.run_refs().to_vec();
119 let now_nanos = crate::engine::unix_nanos_now();
120 let mutable_rows = if self.mutable_run_len() > 0 {
121 self.snapshot_mutable_run()
122 } else {
123 Vec::new()
124 };
125
126 let mut all: HashMap<u64, Vec<Row>> = HashMap::new();
127 let mut scanned = 0_usize;
128 for rr in &old_refs {
129 control.checkpoint()?;
130 let mut reader = self.open_reader(rr.run_id)?;
131 for row in reader.all_rows()? {
132 if scanned.is_multiple_of(256) {
133 control.checkpoint()?;
134 }
135 scanned += 1;
136 all.entry(row.row_id.0).or_default().push(row);
137 }
138 }
139 for row in mutable_rows {
140 if scanned.is_multiple_of(256) {
141 control.checkpoint()?;
142 }
143 scanned += 1;
144 all.entry(row.row_id.0).or_default().push(row);
145 }
146
147 let mut rows = Vec::new();
148 let mut current_live_count = 0u64;
149 for (row_index, (_, mut versions)) in all.into_iter().enumerate() {
150 if row_index % 256 == 0 {
151 control.checkpoint()?;
152 }
153 versions.sort_by_key(|row| row.committed_epoch);
154 let Some(newest) = versions.last() else {
155 continue;
156 };
157 let newest_epoch = newest.committed_epoch;
158 if !newest.deleted && !self.row_expired_at(newest, now_nanos) {
159 current_live_count += 1;
160 }
161 for row in select_keep(&versions, min_active) {
162 if self.row_expired_at(&row, now_nanos) {
163 if row.committed_epoch == newest_epoch
164 && min_active.is_some_and(|epoch| newest_epoch > epoch)
165 {
166 let mut tombstone = row;
167 tombstone.deleted = true;
168 tombstone.columns.clear();
169 rows.push(tombstone);
170 }
171 } else {
172 rows.push(row);
173 }
174 }
175 }
176 rows.sort_by_key(|row| (row.row_id, row.committed_epoch));
177
178 let mut staged_run = None;
179 if !rows.is_empty() {
180 let run_id = self.alloc_run_id()?;
181 let final_name = format!("r-{run_id}.sr");
182 let stage_name = format!(
183 "{final_name}.compact-stage-{}-{}",
184 std::process::id(),
185 std::time::SystemTime::now()
186 .duration_since(std::time::UNIX_EPOCH)
187 .unwrap_or_default()
188 .as_nanos()
189 );
190 let kek = self.kek();
191 let mut writer = RunWriter::new(self.schema(), run_id as u128, maintenance_epoch, 1)
192 .clean(min_active.is_none())
193 .with_zstd_level(self.compaction_zstd_level());
194 if let Some(k) = &kek {
195 writer = writer.with_encryption(k.as_ref(), self.indexable_column_specs());
196 }
197 let header = match self.create_run_entry(Path::new(&stage_name))? {
198 Some(file) => writer.write_file(file, &rows),
199 None => writer.write(self.runs_dir().join(&stage_name), &rows),
200 };
201 let header = match header {
202 Ok(header) => header,
203 Err(error) => {
204 let _ = self.remove_run_entry(Path::new(&stage_name));
205 return Err(error);
206 }
207 };
208 staged_run = Some((
209 stage_name,
210 final_name,
211 RunRef {
212 run_id: run_id as u128,
213 level: 1,
214 epoch_created: header.epoch_created,
215 row_count: header.row_count,
216 },
217 ));
218 }
219
220 if let Err(error) = control.checkpoint() {
221 if let Some((stage_name, _, _)) = staged_run {
222 let _ = self.remove_run_entry(Path::new(&stage_name));
223 }
224 return Err(error);
225 }
226 if !before_publish() {
227 if let Some((stage_name, _, _)) = staged_run {
228 let _ = self.remove_run_entry(Path::new(&stage_name));
229 }
230 return Err(MongrelError::Cancelled);
231 }
232
233 let replacement_ref = if let Some((stage_name, final_name, staged_ref)) = staged_run {
237 self.publish_run_entry(Path::new(&stage_name), Path::new(&final_name))?;
238 Some(staged_ref)
239 } else {
240 None
241 };
242
243 if self.mutable_run_len() > 0 {
244 self.drain_mutable_run();
245 }
246 self.live_count = current_live_count;
247 let retire_epoch = maintenance_epoch.0;
248 if let Some(replacement_ref) = replacement_ref {
249 self.set_run_refs(vec![replacement_ref]);
250 for run in &old_refs {
251 self.retire_run(run.run_id, retire_epoch);
252 }
253 } else {
254 self.set_run_refs(Vec::new());
255 for run in &old_refs {
256 self.retire_run(run.run_id, retire_epoch);
257 }
258 }
259
260 self.prepare_indexes_for_run_replacement();
265 if let Err(error) = self.persist_manifest(maintenance_epoch) {
266 self.poison_after_maintenance_publish_failure();
267 return Err(MongrelError::CommitOutcomeUnknown {
268 epoch: maintenance_epoch.0,
269 message: format!("compaction manifest publication failed: {error}"),
270 });
271 }
272 self.clear_result_cache();
273 self.bump_data_generation();
274
275 if let Err(error) = self
279 .rebuild_indexes_from_runs()
280 .and_then(|_| self.build_learned_ranges())
281 {
282 return Err(MongrelError::DurableCommit {
283 epoch: maintenance_epoch.0,
284 message: format!("compaction committed but index rebuild failed: {error}"),
285 });
286 }
287 self.finish_indexes_for_run_replacement();
288 self.checkpoint_indexes(maintenance_epoch);
289 Ok((
290 true,
291 Some(MaintenanceReceipt {
292 epoch: maintenance_epoch,
293 }),
294 ))
295 }
296}
297
298fn select_keep(vers: &[Row], min_active: Option<Epoch>) -> Vec<Row> {
302 let Some(newest) = vers.last().cloned() else {
303 return Vec::new();
304 };
305 match min_active {
306 None => {
307 if newest.deleted {
308 Vec::new()
309 } else {
310 vec![newest]
311 }
312 }
313 Some(min_e) => {
314 let recent_start = vers.partition_point(|row| row.committed_epoch < min_e);
315 let mut keep = vers[recent_start..].to_vec();
316 if recent_start > 0 && keep.first().is_none_or(|row| row.committed_epoch > min_e) {
317 let boundary = vers[recent_start - 1].clone();
318 if keep.is_empty() && boundary.deleted {
319 return Vec::new();
320 }
321 keep.insert(0, boundary);
322 }
323 keep
324 }
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
332 use crate::{Database, Snapshot, Value};
333 use tempfile::tempdir;
334
335 fn schema() -> Schema {
336 Schema {
337 schema_id: 1,
338 columns: vec![ColumnDef {
339 id: 1,
340 name: "v".into(),
341 ty: TypeId::Int64,
342 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
343 default_value: None,
344 }],
345 indexes: Vec::new(),
346 colocation: vec![],
347 constraints: Default::default(),
348 clustered: false,
349 }
350 }
351
352 #[test]
353 fn compaction_merges_runs_and_gcs_tombstoned_row() {
354 let dir = tempdir().unwrap();
355 let mut db = Table::create(dir.path(), schema(), 1).unwrap();
356 db.set_mutable_run_spill_bytes(1);
358 let mut ids = Vec::new();
359 for i in 1..=5i64 {
360 ids.push(db.put(vec![(1, Value::Int64(i))]).unwrap());
361 }
362 db.flush().unwrap();
363 db.delete(ids[2]).unwrap();
364 db.flush().unwrap();
365 db.put(vec![(1, Value::Int64(60))]).unwrap();
366 db.flush().unwrap();
367 assert_eq!(db.run_count(), 3);
368
369 db.compact().unwrap();
370 assert_eq!(db.run_count(), 1);
371 let rows = db.visible_rows(db.snapshot()).unwrap();
372 let row_ids: Vec<u64> = rows.iter().map(|r| r.row_id.0).collect();
373 assert!(!row_ids.contains(&ids[2].0), "tombstoned row must be GC'd");
374 assert_eq!(rows.len(), 5);
375 }
376
377 #[test]
378 fn pinned_snapshot_survives_compaction() {
379 let dir = tempdir().unwrap();
380 let mut db = Table::create(dir.path(), schema(), 1).unwrap();
381 let r = db.put(vec![(1, Value::Int64(1))]).unwrap();
382 db.flush().unwrap(); let pinned = db.pin_snapshot();
386 assert_eq!(
387 db.get(r, pinned)
388 .and_then(|row| row.columns.get(&1).cloned()),
389 Some(Value::Int64(1))
390 );
391
392 db.delete(r).unwrap();
394 db.commit().unwrap();
395 db.flush().unwrap(); db.compact().unwrap(); assert_eq!(
400 db.get(r, pinned)
401 .and_then(|row| row.columns.get(&1).cloned()),
402 Some(Value::Int64(1))
403 );
404 assert_eq!(
406 db.get(r, db.snapshot())
407 .and_then(|row| row.columns.get(&1).cloned()),
408 None
409 );
410
411 db.unpin_snapshot(pinned);
413 db.compact().unwrap();
414 assert_eq!(
415 db.get(r, db.snapshot())
416 .and_then(|row| row.columns.get(&1).cloned()),
417 None
418 );
419 }
420
421 #[test]
422 fn controlled_compaction_cancel_before_publish_preserves_live_state() {
423 let dir = tempdir().unwrap();
424 let mut table = Table::create(dir.path(), schema(), 1).unwrap();
425 table.set_mutable_run_spill_bytes(1);
426 table.put(vec![(1, Value::Int64(1))]).unwrap();
427 table.flush().unwrap();
428 table.put(vec![(1, Value::Int64(2))]).unwrap();
429 table.flush().unwrap();
430 let before_refs: Vec<_> = table
431 .run_refs()
432 .iter()
433 .map(|run| (run.run_id, run.level, run.epoch_created, run.row_count))
434 .collect();
435
436 let error = table
437 .compact_controlled(&ExecutionControl::new(None), || false)
438 .unwrap_err();
439 assert!(matches!(error, MongrelError::Cancelled));
440 let after_refs: Vec<_> = table
441 .run_refs()
442 .iter()
443 .map(|run| (run.run_id, run.level, run.epoch_created, run.row_count))
444 .collect();
445 assert_eq!(after_refs, before_refs);
446 assert_eq!(table.visible_rows(table.snapshot()).unwrap().len(), 2);
447 assert!(std::fs::read_dir(table.runs_dir())
448 .unwrap()
449 .all(|entry| !entry
450 .unwrap()
451 .file_name()
452 .to_string_lossy()
453 .contains("compact-stage")));
454 }
455
456 #[test]
457 fn compaction_manifest_failure_poisons_standalone_table_until_reopen() {
458 let dir = tempdir().unwrap();
459 let mut table = Table::create(dir.path(), schema(), 1).unwrap();
460 table.set_mutable_run_spill_bytes(1);
461 table.put(vec![(1, Value::Int64(1))]).unwrap();
462 table.flush().unwrap();
463 table.put(vec![(1, Value::Int64(2))]).unwrap();
464 table.flush().unwrap();
465 let manifest = dir.path().join(crate::manifest::MANIFEST_FILENAME);
466 let saved_manifest = dir.path().join("_mf.saved");
467 std::fs::rename(&manifest, &saved_manifest).unwrap();
468 std::fs::create_dir(&manifest).unwrap();
469
470 let error = table.compact().unwrap_err();
471 assert!(matches!(error, MongrelError::CommitOutcomeUnknown { .. }));
472 assert_eq!(table.visible_rows(table.snapshot()).unwrap().len(), 2);
473 assert!(table
474 .put(vec![(1, Value::Int64(3))])
475 .unwrap_err()
476 .to_string()
477 .contains("reopen required"));
478
479 drop(table);
480 std::fs::remove_dir(&manifest).unwrap();
481 std::fs::rename(saved_manifest, manifest).unwrap();
482 let reopened = Table::open(dir.path()).unwrap();
483 assert_eq!(reopened.visible_rows(reopened.snapshot()).unwrap().len(), 2);
484 }
485
486 #[test]
487 fn compaction_manifest_failure_poisons_mounted_database() {
488 let dir = tempdir().unwrap();
489 let db = Database::create(dir.path()).unwrap();
490 let table_id = db.create_table("items", schema()).unwrap();
491 let handle = db.table("items").unwrap();
492 {
493 let mut table = handle.lock();
494 table.set_mutable_run_spill_bytes(1);
495 table.put(vec![(1, Value::Int64(1))]).unwrap();
496 table.flush().unwrap();
497 table.put(vec![(1, Value::Int64(2))]).unwrap();
498 table.flush().unwrap();
499 }
500 let manifest = dir
501 .path()
502 .join("tables")
503 .join(table_id.to_string())
504 .join(crate::manifest::MANIFEST_FILENAME);
505 let saved_manifest = manifest.with_extension("saved");
506 std::fs::rename(&manifest, &saved_manifest).unwrap();
507 std::fs::create_dir(&manifest).unwrap();
508
509 let error = handle.lock().compact().unwrap_err();
510 assert!(matches!(error, MongrelError::CommitOutcomeUnknown { .. }));
511 assert!(db
512 .create_table("blocked", schema())
513 .unwrap_err()
514 .to_string()
515 .contains("database poisoned"));
516 }
517
518 #[test]
519 fn _snapshot_import_used() {
520 let _ = Snapshot::at(Epoch(0));
521 }
522}