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 embedding_source: None,
345 }],
346 indexes: Vec::new(),
347 colocation: vec![],
348 constraints: Default::default(),
349 clustered: false,
350 }
351 }
352
353 #[test]
354 fn compaction_merges_runs_and_gcs_tombstoned_row() {
355 let dir = tempdir().unwrap();
356 let mut db = Table::create(dir.path(), schema(), 1).unwrap();
357 db.set_mutable_run_spill_bytes(1);
359 let mut ids = Vec::new();
360 for i in 1..=5i64 {
361 ids.push(db.put(vec![(1, Value::Int64(i))]).unwrap());
362 }
363 db.flush().unwrap();
364 db.delete(ids[2]).unwrap();
365 db.flush().unwrap();
366 db.put(vec![(1, Value::Int64(60))]).unwrap();
367 db.flush().unwrap();
368 assert_eq!(db.run_count(), 3);
369
370 db.compact().unwrap();
371 assert_eq!(db.run_count(), 1);
372 let rows = db.visible_rows(db.snapshot()).unwrap();
373 let row_ids: Vec<u64> = rows.iter().map(|r| r.row_id.0).collect();
374 assert!(!row_ids.contains(&ids[2].0), "tombstoned row must be GC'd");
375 assert_eq!(rows.len(), 5);
376 }
377
378 #[test]
379 fn pinned_snapshot_survives_compaction() {
380 let dir = tempdir().unwrap();
381 let mut db = Table::create(dir.path(), schema(), 1).unwrap();
382 let r = db.put(vec![(1, Value::Int64(1))]).unwrap();
383 db.flush().unwrap(); let pinned = db.pin_snapshot();
387 assert_eq!(
388 db.get(r, pinned)
389 .and_then(|row| row.columns.get(&1).cloned()),
390 Some(Value::Int64(1))
391 );
392
393 db.delete(r).unwrap();
395 db.commit().unwrap();
396 db.flush().unwrap(); db.compact().unwrap(); assert_eq!(
401 db.get(r, pinned)
402 .and_then(|row| row.columns.get(&1).cloned()),
403 Some(Value::Int64(1))
404 );
405 assert_eq!(
407 db.get(r, db.snapshot())
408 .and_then(|row| row.columns.get(&1).cloned()),
409 None
410 );
411
412 db.unpin_snapshot(pinned);
414 db.compact().unwrap();
415 assert_eq!(
416 db.get(r, db.snapshot())
417 .and_then(|row| row.columns.get(&1).cloned()),
418 None
419 );
420 }
421
422 #[test]
423 fn controlled_compaction_cancel_before_publish_preserves_live_state() {
424 let dir = tempdir().unwrap();
425 let mut table = Table::create(dir.path(), schema(), 1).unwrap();
426 table.set_mutable_run_spill_bytes(1);
427 table.put(vec![(1, Value::Int64(1))]).unwrap();
428 table.flush().unwrap();
429 table.put(vec![(1, Value::Int64(2))]).unwrap();
430 table.flush().unwrap();
431 let before_refs: Vec<_> = table
432 .run_refs()
433 .iter()
434 .map(|run| (run.run_id, run.level, run.epoch_created, run.row_count))
435 .collect();
436
437 let error = table
438 .compact_controlled(&ExecutionControl::new(None), || false)
439 .unwrap_err();
440 assert!(matches!(error, MongrelError::Cancelled));
441 let after_refs: Vec<_> = table
442 .run_refs()
443 .iter()
444 .map(|run| (run.run_id, run.level, run.epoch_created, run.row_count))
445 .collect();
446 assert_eq!(after_refs, before_refs);
447 assert_eq!(table.visible_rows(table.snapshot()).unwrap().len(), 2);
448 assert!(std::fs::read_dir(table.runs_dir())
449 .unwrap()
450 .all(|entry| !entry
451 .unwrap()
452 .file_name()
453 .to_string_lossy()
454 .contains("compact-stage")));
455 }
456
457 #[test]
458 fn compaction_manifest_failure_poisons_standalone_table_until_reopen() {
459 let dir = tempdir().unwrap();
460 let mut table = Table::create(dir.path(), schema(), 1).unwrap();
461 table.set_mutable_run_spill_bytes(1);
462 table.put(vec![(1, Value::Int64(1))]).unwrap();
463 table.flush().unwrap();
464 table.put(vec![(1, Value::Int64(2))]).unwrap();
465 table.flush().unwrap();
466 let manifest = dir.path().join(crate::manifest::MANIFEST_FILENAME);
467 let saved_manifest = dir.path().join("_mf.saved");
468 std::fs::rename(&manifest, &saved_manifest).unwrap();
469 std::fs::create_dir(&manifest).unwrap();
470
471 let error = table.compact().unwrap_err();
472 assert!(matches!(error, MongrelError::CommitOutcomeUnknown { .. }));
473 assert_eq!(table.visible_rows(table.snapshot()).unwrap().len(), 2);
474 assert!(table
475 .put(vec![(1, Value::Int64(3))])
476 .unwrap_err()
477 .to_string()
478 .contains("reopen required"));
479
480 drop(table);
481 std::fs::remove_dir(&manifest).unwrap();
482 std::fs::rename(saved_manifest, manifest).unwrap();
483 let reopened = Table::open(dir.path()).unwrap();
484 assert_eq!(reopened.visible_rows(reopened.snapshot()).unwrap().len(), 2);
485 }
486
487 #[test]
488 fn compaction_manifest_failure_poisons_mounted_database() {
489 let dir = tempdir().unwrap();
490 let db = Database::create(dir.path()).unwrap();
491 let table_id = db.create_table("items", schema()).unwrap();
492 let handle = db.table("items").unwrap();
493 {
494 let mut table = handle.lock();
495 table.set_mutable_run_spill_bytes(1);
496 table.put(vec![(1, Value::Int64(1))]).unwrap();
497 table.flush().unwrap();
498 table.put(vec![(1, Value::Int64(2))]).unwrap();
499 table.flush().unwrap();
500 }
501 let manifest = dir
502 .path()
503 .join("tables")
504 .join(table_id.to_string())
505 .join(crate::manifest::MANIFEST_FILENAME);
506 let saved_manifest = manifest.with_extension("saved");
507 std::fs::rename(&manifest, &saved_manifest).unwrap();
508 std::fs::create_dir(&manifest).unwrap();
509
510 let error = handle.lock().compact().unwrap_err();
511 assert!(matches!(error, MongrelError::CommitOutcomeUnknown { .. }));
512 assert!(db
513 .create_table("blocked", schema())
514 .unwrap_err()
515 .to_string()
516 .contains("database poisoned"));
517 }
518
519 #[test]
520 fn _snapshot_import_used() {
521 let _ = Snapshot::at(Epoch(0));
522 }
523}