1use crate::engine::Table;
18use crate::epoch::{Epoch, MaintenanceReceipt, Snapshot};
19use crate::manifest::RunRef;
20use crate::memtable::Row;
21use crate::sorted_run::RunWriter;
22use crate::{ExecutionControl, MongrelError, Result};
23use std::collections::HashMap;
24use std::path::Path;
25
26impl Table {
27 pub const AUTO_COMPACT_RUN_THRESHOLD: usize = 8;
32
33 pub fn should_compact(&self) -> bool {
39 self.run_refs().len() >= Self::AUTO_COMPACT_RUN_THRESHOLD
40 || (self.ttl().is_some()
41 && !self.run_refs().is_empty()
42 && self.has_expired_run_rows().unwrap_or(false))
43 }
44
45 fn has_expired_run_rows(&self) -> Result<bool> {
46 self.has_expired_run_rows_inner(None)
47 }
48
49 fn has_expired_run_rows_inner(&self, control: Option<&ExecutionControl>) -> Result<bool> {
50 let now_nanos = crate::engine::unix_nanos_now();
51 for (run_index, run) in self.run_refs().iter().enumerate() {
52 if run_index % 256 == 0 {
53 if let Some(control) = control {
54 control.checkpoint()?;
55 }
56 }
57 let mut reader = self.open_reader(run.run_id)?;
58 for (row_index, row) in reader.all_rows()?.iter().enumerate() {
59 if row_index % 256 == 0 {
60 if let Some(control) = control {
61 control.checkpoint()?;
62 }
63 }
64 if self.row_expired_at(row, now_nanos) {
65 return Ok(true);
66 }
67 }
68 }
69 Ok(false)
70 }
71
72 pub fn maybe_compact(&mut self) -> Result<bool> {
78 if !self.should_compact() {
79 return Ok(false);
80 }
81 self.compact()?;
82 Ok(true)
83 }
84
85 pub fn compact(&mut self) -> Result<()> {
90 let control = ExecutionControl::new(None);
91 self.compact_controlled(&control, || true).map(|_| ())
92 }
93
94 #[doc(hidden)]
98 pub fn compact_controlled<F>(
99 &mut self,
100 control: &ExecutionControl,
101 before_publish: F,
102 ) -> Result<bool>
103 where
104 F: FnOnce() -> bool,
105 {
106 self.compact_controlled_with_receipt(control, before_publish)
107 .map(|(changed, _)| changed)
108 }
109
110 #[doc(hidden)]
113 pub fn compact_controlled_with_receipt<F>(
114 &mut self,
115 control: &ExecutionControl,
116 before_publish: F,
117 ) -> Result<(bool, Option<MaintenanceReceipt>)>
118 where
119 F: FnOnce() -> bool,
120 {
121 control.checkpoint()?;
122 let maintenance_epoch = self.current_epoch();
123 let reclaim_ttl = self.ttl().is_some() && self.has_expired_run_rows_inner(Some(control))?;
124 if self.run_refs().len() < 2 && !reclaim_ttl {
125 return Ok((false, None));
126 }
127 let min_active = self.min_active_snapshot();
128 let old_refs: Vec<RunRef> = self.run_refs().to_vec();
129 let now_nanos = crate::engine::unix_nanos_now();
130 let mutable_rows = if self.mutable_run_len() > 0 {
131 self.snapshot_mutable_run()
132 } else {
133 Vec::new()
134 };
135
136 let mut all: HashMap<u64, Vec<Row>> = HashMap::new();
137 let mut scanned = 0_usize;
138 for rr in &old_refs {
139 control.checkpoint()?;
140 let mut reader = self.open_reader(rr.run_id)?;
141 for row in reader.all_rows()? {
142 if scanned.is_multiple_of(256) {
143 control.checkpoint()?;
144 }
145 scanned += 1;
146 all.entry(row.row_id.0).or_default().push(row);
147 }
148 }
149 for row in mutable_rows {
150 if scanned.is_multiple_of(256) {
151 control.checkpoint()?;
152 }
153 scanned += 1;
154 all.entry(row.row_id.0).or_default().push(row);
155 }
156
157 let mut rows = Vec::new();
158 let mut current_live_count = 0u64;
159 for (row_index, (_, mut versions)) in all.into_iter().enumerate() {
160 if row_index % 256 == 0 {
161 control.checkpoint()?;
162 }
163 versions.sort_by_key(|row| row.committed_epoch);
167 let Some(newest) = versions
168 .iter()
169 .max_by(|a, b| cmp_version_order(a, b))
170 .cloned()
171 else {
172 continue;
173 };
174 let newest_epoch = newest.committed_epoch;
175 if !newest.deleted && !self.row_expired_at(&newest, now_nanos) {
176 current_live_count += 1;
177 }
178 for row in select_keep(&versions, min_active) {
179 if self.row_expired_at(&row, now_nanos) {
180 if is_same_version(&row, &newest)
181 && min_active.is_some_and(|epoch| newest_epoch > epoch)
182 {
183 let mut tombstone = row;
184 tombstone.deleted = true;
185 tombstone.columns.clear();
186 rows.push(tombstone);
187 }
188 } else {
189 rows.push(row);
190 }
191 }
192 }
193 rows.sort_by_key(|row| (row.row_id, row.committed_epoch));
194
195 let mut staged_run = None;
196 if !rows.is_empty() {
197 let run_id = self.alloc_run_id()?;
198 let final_name = format!("r-{run_id}.sr");
199 let stage_name = format!(
200 "{final_name}.compact-stage-{}-{}",
201 std::process::id(),
202 std::time::SystemTime::now()
203 .duration_since(std::time::UNIX_EPOCH)
204 .unwrap_or_default()
205 .as_nanos()
206 );
207 let kek = self.kek();
208 let mut writer = RunWriter::new(self.schema(), run_id as u128, maintenance_epoch, 1)
209 .clean(min_active.is_none())
210 .with_zstd_level(self.compaction_zstd_level());
211 if let Some(k) = &kek {
212 writer = writer.with_encryption(k.as_ref(), self.indexable_column_specs());
213 }
214 let header = match self.create_run_entry(Path::new(&stage_name))? {
215 Some(file) => writer.write_file(file, &rows),
216 None => writer.write(self.runs_dir().join(&stage_name), &rows),
217 };
218 let header = match header {
219 Ok(header) => header,
220 Err(error) => {
221 let _ = self.remove_run_entry(Path::new(&stage_name));
222 return Err(error);
223 }
224 };
225 staged_run = Some((
226 stage_name,
227 final_name,
228 RunRef {
229 run_id: run_id as u128,
230 level: 1,
231 epoch_created: header.epoch_created,
232 row_count: header.row_count,
233 },
234 ));
235 }
236
237 if let Err(error) = control.checkpoint() {
238 if let Some((stage_name, _, _)) = staged_run {
239 let _ = self.remove_run_entry(Path::new(&stage_name));
240 }
241 return Err(error);
242 }
243 if !before_publish() {
244 if let Some((stage_name, _, _)) = staged_run {
245 let _ = self.remove_run_entry(Path::new(&stage_name));
246 }
247 return Err(MongrelError::Cancelled);
248 }
249
250 let replacement_ref = if let Some((stage_name, final_name, staged_ref)) = staged_run {
254 self.publish_run_entry(Path::new(&stage_name), Path::new(&final_name))?;
255 Some(staged_ref)
256 } else {
257 None
258 };
259
260 if self.mutable_run_len() > 0 {
261 self.drain_mutable_run();
262 }
263 self.live_count = current_live_count;
264 let retire_epoch = maintenance_epoch.0;
265 if let Some(replacement_ref) = replacement_ref {
266 self.set_run_refs(vec![replacement_ref]);
267 for run in &old_refs {
268 self.retire_run(run.run_id, retire_epoch);
269 }
270 } else {
271 self.set_run_refs(Vec::new());
272 for run in &old_refs {
273 self.retire_run(run.run_id, retire_epoch);
274 }
275 }
276
277 self.prepare_indexes_for_run_replacement();
282 if let Err(error) = self.persist_manifest(maintenance_epoch) {
283 self.poison_after_maintenance_publish_failure();
284 return Err(MongrelError::CommitOutcomeUnknown {
285 epoch: maintenance_epoch.0,
286 message: format!("compaction manifest publication failed: {error}"),
287 });
288 }
289 self.clear_result_cache();
290 self.bump_data_generation();
291
292 if let Err(error) = self
296 .rebuild_indexes_from_runs()
297 .and_then(|_| self.build_learned_ranges())
298 {
299 return Err(MongrelError::DurableCommit {
300 epoch: maintenance_epoch.0,
301 message: format!("compaction committed but index rebuild failed: {error}"),
302 });
303 }
304 self.finish_indexes_for_run_replacement();
305 self.checkpoint_indexes(maintenance_epoch);
306 Ok((
307 true,
308 Some(MaintenanceReceipt {
309 epoch: maintenance_epoch,
310 }),
311 ))
312 }
313}
314
315fn cmp_version_order(a: &Row, b: &Row) -> std::cmp::Ordering {
317 if Snapshot::version_is_newer(
318 a.committed_epoch,
319 a.commit_ts,
320 b.committed_epoch,
321 b.commit_ts,
322 ) {
323 std::cmp::Ordering::Greater
324 } else if Snapshot::version_is_newer(
325 b.committed_epoch,
326 b.commit_ts,
327 a.committed_epoch,
328 a.commit_ts,
329 ) {
330 std::cmp::Ordering::Less
331 } else {
332 a.committed_epoch
333 .cmp(&b.committed_epoch)
334 .then_with(|| a.commit_ts.cmp(&b.commit_ts))
335 }
336}
337
338fn is_same_version(a: &Row, b: &Row) -> bool {
339 a.committed_epoch == b.committed_epoch && a.commit_ts == b.commit_ts
340}
341
342fn select_keep(vers: &[Row], min_active: Option<Epoch>) -> Vec<Row> {
352 let Some(newest) = vers.iter().max_by(|a, b| cmp_version_order(a, b)).cloned() else {
353 return Vec::new();
354 };
355 match min_active {
356 None => {
357 if newest.deleted {
358 Vec::new()
359 } else {
360 vec![newest]
361 }
362 }
363 Some(min_e) => {
364 let recent_start = vers.partition_point(|row| row.committed_epoch < min_e);
365 let mut keep = vers[recent_start..].to_vec();
366 if recent_start > 0 && keep.first().is_none_or(|row| row.committed_epoch > min_e) {
367 let boundary = vers[recent_start - 1].clone();
368 if keep.is_empty() && boundary.deleted {
369 return Vec::new();
370 }
371 keep.insert(0, boundary);
372 }
373 keep
374 }
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
382 use crate::{Database, Snapshot, Value};
383 use tempfile::tempdir;
384
385 fn schema() -> Schema {
386 Schema {
387 schema_id: 1,
388 columns: vec![ColumnDef {
389 id: 1,
390 name: "v".into(),
391 ty: TypeId::Int64,
392 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
393 default_value: None,
394 embedding_source: None,
395 }],
396 indexes: Vec::new(),
397 colocation: vec![],
398 constraints: Default::default(),
399 clustered: false,
400 }
401 }
402
403 #[test]
404 fn compaction_merges_runs_and_gcs_tombstoned_row() {
405 let dir = tempdir().unwrap();
406 let mut db = Table::create(dir.path(), schema(), 1).unwrap();
407 db.set_mutable_run_spill_bytes(1);
409 let mut ids = Vec::new();
410 for i in 1..=5i64 {
411 ids.push(db.put(vec![(1, Value::Int64(i))]).unwrap());
412 }
413 db.flush().unwrap();
414 db.delete(ids[2]).unwrap();
415 db.flush().unwrap();
416 db.put(vec![(1, Value::Int64(60))]).unwrap();
417 db.flush().unwrap();
418 assert_eq!(db.run_count(), 3);
419
420 db.compact().unwrap();
421 assert_eq!(db.run_count(), 1);
422 let rows = db.visible_rows(db.snapshot()).unwrap();
423 let row_ids: Vec<u64> = rows.iter().map(|r| r.row_id.0).collect();
424 assert!(!row_ids.contains(&ids[2].0), "tombstoned row must be GC'd");
425 assert_eq!(rows.len(), 5);
426 }
427
428 #[test]
429 fn pinned_snapshot_survives_compaction() {
430 let dir = tempdir().unwrap();
431 let mut db = Table::create(dir.path(), schema(), 1).unwrap();
432 let r = db.put(vec![(1, Value::Int64(1))]).unwrap();
433 db.flush().unwrap(); let pinned = db.pin_snapshot();
437 assert_eq!(
438 db.get(r, pinned)
439 .and_then(|row| row.columns.get(&1).cloned()),
440 Some(Value::Int64(1))
441 );
442
443 db.delete(r).unwrap();
445 db.commit().unwrap();
446 db.flush().unwrap(); db.compact().unwrap(); assert_eq!(
451 db.get(r, pinned)
452 .and_then(|row| row.columns.get(&1).cloned()),
453 Some(Value::Int64(1))
454 );
455 assert_eq!(
457 db.get(r, db.snapshot())
458 .and_then(|row| row.columns.get(&1).cloned()),
459 None
460 );
461
462 db.unpin_snapshot(pinned);
464 db.compact().unwrap();
465 assert_eq!(
466 db.get(r, db.snapshot())
467 .and_then(|row| row.columns.get(&1).cloned()),
468 None
469 );
470 }
471
472 #[test]
473 fn controlled_compaction_cancel_before_publish_preserves_live_state() {
474 let dir = tempdir().unwrap();
475 let mut table = Table::create(dir.path(), schema(), 1).unwrap();
476 table.set_mutable_run_spill_bytes(1);
477 table.put(vec![(1, Value::Int64(1))]).unwrap();
478 table.flush().unwrap();
479 table.put(vec![(1, Value::Int64(2))]).unwrap();
480 table.flush().unwrap();
481 let before_refs: Vec<_> = table
482 .run_refs()
483 .iter()
484 .map(|run| (run.run_id, run.level, run.epoch_created, run.row_count))
485 .collect();
486
487 let error = table
488 .compact_controlled(&ExecutionControl::new(None), || false)
489 .unwrap_err();
490 assert!(matches!(error, MongrelError::Cancelled));
491 let after_refs: Vec<_> = table
492 .run_refs()
493 .iter()
494 .map(|run| (run.run_id, run.level, run.epoch_created, run.row_count))
495 .collect();
496 assert_eq!(after_refs, before_refs);
497 assert_eq!(table.visible_rows(table.snapshot()).unwrap().len(), 2);
498 assert!(std::fs::read_dir(table.runs_dir())
499 .unwrap()
500 .all(|entry| !entry
501 .unwrap()
502 .file_name()
503 .to_string_lossy()
504 .contains("compact-stage")));
505 }
506
507 #[test]
508 fn compaction_manifest_failure_poisons_standalone_table_until_reopen() {
509 let dir = tempdir().unwrap();
510 let mut table = Table::create(dir.path(), schema(), 1).unwrap();
511 table.set_mutable_run_spill_bytes(1);
512 table.put(vec![(1, Value::Int64(1))]).unwrap();
513 table.flush().unwrap();
514 table.put(vec![(1, Value::Int64(2))]).unwrap();
515 table.flush().unwrap();
516 let manifest = dir.path().join(crate::manifest::MANIFEST_FILENAME);
517 let saved_manifest = dir.path().join("_mf.saved");
518 std::fs::rename(&manifest, &saved_manifest).unwrap();
519 std::fs::create_dir(&manifest).unwrap();
520
521 let error = table.compact().unwrap_err();
522 assert!(matches!(error, MongrelError::CommitOutcomeUnknown { .. }));
523 assert_eq!(table.visible_rows(table.snapshot()).unwrap().len(), 2);
524 assert!(table
525 .put(vec![(1, Value::Int64(3))])
526 .unwrap_err()
527 .to_string()
528 .contains("reopen required"));
529
530 drop(table);
531 std::fs::remove_dir(&manifest).unwrap();
532 std::fs::rename(saved_manifest, manifest).unwrap();
533 let reopened = Table::open(dir.path()).unwrap();
534 assert_eq!(reopened.visible_rows(reopened.snapshot()).unwrap().len(), 2);
535 }
536
537 #[test]
538 fn compaction_manifest_failure_poisons_mounted_database() {
539 let dir = tempdir().unwrap();
540 let db = Database::create(dir.path()).unwrap();
541 let table_id = db.create_table("items", schema()).unwrap();
542 let handle = db.table("items").unwrap();
543 {
544 let mut table = handle.lock();
545 table.set_mutable_run_spill_bytes(1);
546 table.put(vec![(1, Value::Int64(1))]).unwrap();
547 table.flush().unwrap();
548 table.put(vec![(1, Value::Int64(2))]).unwrap();
549 table.flush().unwrap();
550 }
551 let manifest = dir
552 .path()
553 .join("tables")
554 .join(table_id.to_string())
555 .join(crate::manifest::MANIFEST_FILENAME);
556 let saved_manifest = manifest.with_extension("saved");
557 std::fs::rename(&manifest, &saved_manifest).unwrap();
558 std::fs::create_dir(&manifest).unwrap();
559
560 let error = handle.lock().compact().unwrap_err();
561 assert!(matches!(error, MongrelError::CommitOutcomeUnknown { .. }));
562 assert!(db
563 .create_table("blocked", schema())
564 .unwrap_err()
565 .to_string()
566 .contains("database poisoned"));
567 }
568
569 #[test]
570 fn _snapshot_import_used() {
571 let _ = Snapshot::at(Epoch(0));
572 }
573
574 fn hlc(physical_micros: u64) -> mongreldb_types::hlc::HlcTimestamp {
575 mongreldb_types::hlc::HlcTimestamp {
576 physical_micros,
577 logical: 0,
578 node_tiebreaker: 1,
579 }
580 }
581
582 fn stamped(id: u64, epoch: u64, ts: mongreldb_types::hlc::HlcTimestamp, v: i64) -> Row {
583 crate::memtable::Row::new_with_hlc(crate::rowid::RowId(id), Epoch(epoch), ts)
584 .with_column(1, Value::Int64(v))
585 }
586
587 #[test]
591 fn select_keep_preserves_hlc_visibility_under_inverted_epoch_order() {
592 let early = hlc(100);
593 let mid = hlc(150);
594 let late = hlc(200);
595 let mut versions = vec![
597 stamped(1, 1, late, 99), stamped(1, 2, mid, 50), stamped(1, 50, early, 1), ];
601 versions.sort_by_key(|row| row.committed_epoch);
602 let kept = select_keep(&versions, None);
604 assert_eq!(kept.len(), 1);
605 assert_eq!(kept[0].commit_ts, Some(late));
606 assert_eq!(kept[0].committed_epoch, Epoch(1));
607 assert_eq!(kept[0].columns.get(&1), Some(&Value::Int64(99)));
608
609 let kept = select_keep(&versions, Some(Epoch(2)));
613 assert!(kept.iter().any(|r| r.commit_ts == Some(mid)));
614 assert!(kept.iter().any(|r| r.commit_ts == Some(early)));
615 assert!(!kept.iter().any(|r| r.commit_ts == Some(late)));
616
617 let kept = select_keep(&versions, Some(Epoch(10)));
620 assert!(kept.iter().any(|r| r.commit_ts == Some(early)));
621 assert!(
622 kept.iter().any(|r| r.commit_ts == Some(mid)),
623 "boundary version below floor must be retained"
624 );
625 assert!(!kept.iter().any(|r| r.commit_ts == Some(late)));
626 }
627
628 #[test]
629 fn cmp_version_order_prefers_hlc_when_both_stamped() {
630 let early = stamped(1, 99, hlc(100), 1);
631 let late = stamped(1, 1, hlc(200), 2);
632 assert_eq!(cmp_version_order(&early, &late), std::cmp::Ordering::Less);
633 assert_eq!(
634 cmp_version_order(&late, &early),
635 std::cmp::Ordering::Greater
636 );
637 let a = crate::memtable::Row::new(crate::rowid::RowId(1), Epoch(1));
639 let b = crate::memtable::Row::new(crate::rowid::RowId(1), Epoch(3));
640 assert_eq!(cmp_version_order(&a, &b), std::cmp::Ordering::Less);
641 }
642}