1use crate::catalog::{self, Catalog, CatalogEntry, TableState, META_DEK_LEN};
10use crate::engine::{SharedCtx, Table};
11use crate::epoch::{Epoch, EpochAuthority, Snapshot};
12use crate::error::{MongrelError, Result};
13use crate::retention::{OwnedSnapshotGuard, SnapshotGuard, SnapshotRegistry};
14use crate::schema::{AlterColumn, ColumnDef, Schema};
15use parking_lot::{Mutex, RwLock};
16use std::collections::HashMap;
17use std::path::{Path, PathBuf};
18use std::sync::Arc;
19
20pub const TABLES_DIR: &str = "tables";
21pub const META_DIR: &str = "_meta";
22pub const KEYS_FILENAME: &str = "keys";
23
24pub const WAL_TABLE_ID: u64 = u64::MAX;
28
29struct SpilledRun {
31 table_id: u64,
32 run_id: u128,
33 pending_path: PathBuf,
34 rows: Vec<crate::memtable::Row>,
35 row_count: u64,
36 min_rid: u64,
37 max_rid: u64,
38}
39
40#[derive(Debug, Clone)]
42pub struct CheckIssue {
43 pub table_id: u64,
44 pub table_name: String,
45 pub severity: String,
46 pub description: String,
47}
48
49pub type TableHandle = Arc<Mutex<Table>>;
53
54pub struct Database {
57 root: PathBuf,
58 catalog: RwLock<Catalog>,
59 epoch: Arc<EpochAuthority>,
60 snapshots: Arc<SnapshotRegistry>,
61 page_cache: Arc<parking_lot::Mutex<crate::cache::PageCache>>,
62 decoded_cache: Arc<parking_lot::Mutex<crate::cache::DecodedPageCache>>,
63 commit_lock: Arc<Mutex<()>>,
64 shared_wal: Arc<Mutex<crate::wal::SharedWal>>,
69 next_txn_id: Arc<Mutex<u64>>,
73 tables: RwLock<HashMap<u64, TableHandle>>,
74 kek: Option<Arc<crate::encryption::Kek>>,
75 ddl_lock: Mutex<()>,
78 meta_dek: Option<[u8; META_DEK_LEN]>,
79 spill_threshold: std::sync::atomic::AtomicU64,
82 conflicts: crate::txn::ConflictIndex,
85 active_txns: crate::txn::ActiveTxns,
88 poisoned: Arc<std::sync::atomic::AtomicBool>,
91 group: Arc<crate::txn::GroupCommit>,
95 active_spills: Arc<crate::retention::ActiveSpills>,
98 #[doc(hidden)]
102 spill_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
103}
104
105impl Database {
106 pub fn create(root: impl AsRef<Path>) -> Result<Self> {
108 Self::create_inner(root, None)
109 }
110
111 #[cfg(feature = "encryption")]
114 pub fn create_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
115 let root = root.as_ref();
116 std::fs::create_dir_all(root)?;
117 std::fs::create_dir_all(root.join(META_DIR))?;
118 let salt = crate::encryption::random_salt();
119 std::fs::write(root.join(META_DIR).join(KEYS_FILENAME), salt)?;
120 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
121 Self::create_inner(root, Some(kek))
122 }
123
124 #[cfg(feature = "encryption")]
127 pub fn create_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
128 let root = root.as_ref();
129 std::fs::create_dir_all(root)?;
130 std::fs::create_dir_all(root.join(META_DIR))?;
131 let salt = crate::encryption::random_salt();
132 std::fs::write(root.join(META_DIR).join(KEYS_FILENAME), salt)?;
133 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
134 Self::create_inner(root, Some(kek))
135 }
136
137 fn create_inner(
138 root: impl AsRef<Path>,
139 kek: Option<Arc<crate::encryption::Kek>>,
140 ) -> Result<Self> {
141 let root = root.as_ref().to_path_buf();
142 std::fs::create_dir_all(&root)?;
143 std::fs::create_dir_all(root.join(TABLES_DIR))?;
144 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
145 let cat = Catalog::empty();
146 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
147 Self::finish_open(root, cat, kek, meta_dek, false)
148 }
149
150 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
152 Self::open_inner(root, None, None)
153 }
154
155 #[cfg(feature = "encryption")]
157 pub fn open_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
158 let root = root.as_ref();
159 let salt_bytes = std::fs::read(root.join(META_DIR).join(KEYS_FILENAME))
160 .map_err(|e| MongrelError::NotFound(format!("encryption salt file: {e}")))?;
161 let mut salt = [0u8; crate::encryption::SALT_LEN];
162 salt.copy_from_slice(&salt_bytes);
163 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
164 Self::open_inner(root, Some(kek), None)
165 }
166
167 #[cfg(feature = "encryption")]
169 pub fn open_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
170 let root = root.as_ref();
171 let salt_path = root.join(META_DIR).join(KEYS_FILENAME);
172 let salt_bytes = std::fs::read(&salt_path).map_err(|e| {
173 MongrelError::NotFound(format!(
174 "encryption salt file {:?}: {e} (database not encrypted, or corrupted)",
175 salt_path
176 ))
177 })?;
178 if salt_bytes.len() != crate::encryption::SALT_LEN {
179 return Err(MongrelError::InvalidArgument(format!(
180 "salt file is {} bytes, expected {}",
181 salt_bytes.len(),
182 crate::encryption::SALT_LEN
183 )));
184 }
185 let mut salt = [0u8; crate::encryption::SALT_LEN];
186 salt.copy_from_slice(&salt_bytes);
187 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
188 Self::open_inner(root, Some(kek), None)
189 }
190
191 fn open_inner(
192 root: impl AsRef<Path>,
193 kek: Option<Arc<crate::encryption::Kek>>,
194 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
195 ) -> Result<Self> {
196 let root = root.as_ref().to_path_buf();
197 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
198 let cat = catalog::read(&root, meta_dek.as_ref())?
199 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
200 Self::finish_open(root, cat, kek, meta_dek, true)
201 }
202
203 fn finish_open(
204 root: PathBuf,
205 cat: Catalog,
206 kek: Option<Arc<crate::encryption::Kek>>,
207 meta_dek: Option<[u8; META_DEK_LEN]>,
208 existing: bool,
209 ) -> Result<Self> {
210 let epoch = Arc::new(EpochAuthority::new(cat.db_epoch));
211 let snapshots = Arc::new(SnapshotRegistry::new());
212 let page_cache = Arc::new(parking_lot::Mutex::new(crate::cache::PageCache::new(
213 crate::engine::PAGE_CACHE_CAPACITY,
214 )));
215 let decoded_cache = Arc::new(parking_lot::Mutex::new(
216 crate::cache::DecodedPageCache::new(crate::engine::DECODED_CACHE_CAPACITY),
217 ));
218 let commit_lock = Arc::new(Mutex::new(()));
219 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
220 let shared_wal = Arc::new(Mutex::new(if existing {
221 crate::wal::SharedWal::open(&root, Epoch(cat.db_epoch), wal_dek.clone())?
222 } else {
223 crate::wal::SharedWal::create_with_dek(&root, Epoch(cat.db_epoch), wal_dek.clone())?
224 }));
225 let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));
229 let group = Arc::new(crate::txn::GroupCommit::new(
230 shared_wal.lock().durable_seq(),
231 ));
232 let txn_ids = Arc::new(Mutex::new(1u64));
236
237 let mut cat = cat;
243 if existing {
244 recover_ddl_from_wal(&root, &mut cat, meta_dek.as_ref(), wal_dek.as_ref())?;
245 }
246
247 let mut tables: HashMap<u64, TableHandle> = HashMap::new();
253 for entry in &cat.tables {
254 if !matches!(entry.state, TableState::Live) {
255 continue;
256 }
257 let tdir = root.join(TABLES_DIR).join(entry.table_id.to_string());
258 let ctx = SharedCtx {
259 epoch: Arc::clone(&epoch),
260 page_cache: Arc::clone(&page_cache),
261 decoded_cache: Arc::clone(&decoded_cache),
262 snapshots: Arc::clone(&snapshots),
263 kek: kek.clone(),
264 commit_lock: Arc::clone(&commit_lock),
265 shared: Some(crate::engine::SharedWalCtx {
266 wal: Arc::clone(&shared_wal),
267 group: Arc::clone(&group),
268 poisoned: Arc::clone(&poisoned),
269 txn_ids: Arc::clone(&txn_ids),
270 }),
271 };
272 let t = Table::open_in(&tdir, ctx)?;
273 tables.insert(entry.table_id, Arc::new(Mutex::new(t)));
274 }
275
276 if existing {
282 recover_shared_wal(&root, &tables, &epoch, wal_dek.as_ref())?;
283 sweep_pending_txn_dirs(&root, &cat);
286 }
287
288 if existing {
292 cat.open_generation = cat.open_generation.wrapping_add(1);
293 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
294 }
295 let next_txn_id = (cat.open_generation << 32) | 1;
296 *txn_ids.lock() = next_txn_id;
298
299 Ok(Self {
300 root,
301 catalog: RwLock::new(cat),
302 epoch,
303 snapshots,
304 page_cache,
305 decoded_cache,
306 commit_lock,
307 shared_wal,
308 next_txn_id: txn_ids,
309 tables: RwLock::new(tables),
310 kek,
311 ddl_lock: Mutex::new(()),
312 meta_dek,
313 conflicts: crate::txn::ConflictIndex::new(),
314 active_txns: crate::txn::ActiveTxns::new(),
315 poisoned,
316 group,
317 spill_threshold: std::sync::atomic::AtomicU64::new(64 * 1024 * 1024),
318 active_spills: Arc::new(crate::retention::ActiveSpills::new()),
319 spill_hook: Mutex::new(None),
320 })
321 }
322
323 pub fn visible_epoch(&self) -> Epoch {
325 self.epoch.visible()
326 }
327
328 pub fn catalog_snapshot(&self) -> Catalog {
330 self.catalog.read().clone()
331 }
332
333 pub fn root(&self) -> &Path {
335 &self.root
336 }
337
338 pub fn table_id(&self, name: &str) -> Result<u64> {
341 let cat = self.catalog.read();
342 cat.live(name)
343 .map(|e| e.table_id)
344 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
345 }
346
347 pub fn begin(&self) -> crate::txn::Transaction<'_> {
349 let txn_id = self.alloc_txn_id();
350 let read = Snapshot::at(self.epoch.visible());
351 crate::txn::Transaction::new(self, txn_id, read)
352 }
353
354 pub fn transaction<T>(
356 &self,
357 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
358 ) -> Result<T> {
359 let mut tx = self.begin();
360 match f(&mut tx) {
361 Ok(out) => {
362 tx.commit()?;
363 Ok(out)
364 }
365 Err(e) => {
366 tx.rollback();
367 Err(e)
368 }
369 }
370 }
371
372 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
375 self.active_txns.register(epoch)
376 }
377
378 fn validate_constraints(
393 &self,
394 staging: &mut Vec<(u64, crate::txn::Staged)>,
395 read_epoch: Epoch,
396 ) -> Result<()> {
397 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
398 use crate::memtable::Row;
399 use crate::txn::Staged;
400 use std::collections::HashSet;
401
402 let snapshot = Snapshot::at(read_epoch);
403 let cat = self.catalog.read();
404
405 let live: Vec<(u64, &str, &crate::schema::Schema)> = cat
407 .tables
408 .iter()
409 .filter(|e| matches!(e.state, TableState::Live))
410 .map(|e| (e.table_id, e.name.as_str(), &e.schema))
411 .collect();
412
413 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
415 if !any_constraints {
416 return Ok(());
417 }
418
419 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
421 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
422 if let Some(r) = rows_cache.get(&table_id) {
423 return Ok(r.clone());
424 }
425 let handle = self.table_by_id(table_id)?;
426 let rows = handle.lock().visible_rows(snapshot)?;
427 rows_cache.insert(table_id, rows.clone());
428 Ok(rows)
429 };
430
431 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
436 loop {
437 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
438 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
439 .iter()
440 .filter_map(|(t, op)| match op {
441 Staged::Delete(rid) => Some((*t, *rid)),
442 _ => None,
443 })
444 .collect();
445 for (table_id, rid) in deletes {
446 if !cascaded.insert((table_id, rid.0)) {
447 continue;
448 }
449 let Some(tname) = live
450 .iter()
451 .find(|(t, _, _)| *t == table_id)
452 .map(|(_, n, _)| *n)
453 else {
454 continue;
455 };
456 let parent_handle = self.table_by_id(table_id)?;
457 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
458 continue;
459 };
460 for (child_id, _child_name, child_schema) in &live {
461 for fk in &child_schema.constraints.foreign_keys {
462 if fk.ref_table != tname {
463 continue;
464 }
465 let Some(parent_key) =
466 encode_composite_key(&fk.ref_columns, &parent_row.columns)
467 else {
468 continue;
469 };
470 match fk.on_delete {
471 FkAction::Restrict => continue,
472 FkAction::Cascade => {
473 let child_rows = load_rows(*child_id)?;
474 for cr in &child_rows {
475 if !cascaded.contains(&(*child_id, cr.row_id.0))
476 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
477 == Some(parent_key.as_slice())
478 {
479 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
480 }
481 }
482 }
483 FkAction::SetNull => {
484 let child_rows = load_rows(*child_id)?;
485 for cr in &child_rows {
486 if !cascaded.contains(&(*child_id, cr.row_id.0))
487 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
488 == Some(parent_key.as_slice())
489 {
490 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
493 .columns
494 .iter()
495 .map(|(k, v)| (*k, v.clone()))
496 .collect();
497 for cid in &fk.columns {
498 cells.retain(|(k, _)| k != cid);
499 cells.push((*cid, crate::memtable::Value::Null));
500 }
501 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
502 new_ops.push((*child_id, Staged::Put(cells)));
503 }
504 }
505 }
506 }
507 }
508 }
509 }
510 if new_ops.is_empty() {
511 break;
512 }
513 staging.extend(new_ops);
514 }
515
516 let staged_deletes: HashSet<(u64, u64)> = staging
520 .iter()
521 .filter_map(|(t, op)| match op {
522 Staged::Delete(rid) => Some((*t, rid.0)),
523 _ => None,
524 })
525 .collect();
526
527 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
529
530 for (table_id, op) in staging.iter() {
532 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id).copied()
533 else {
534 continue;
535 };
536 let cells_map: HashMap<u16, crate::memtable::Value>;
537 match op {
538 Staged::Put(cells) => {
539 cells_map = cells.iter().cloned().collect();
540
541 if !schema.constraints.checks.is_empty() {
543 validate_checks(&schema.constraints.checks, &cells_map)?;
544 }
545
546 for uc in &schema.constraints.uniques {
548 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
549 continue; };
551 let marker = (*table_id, uc.id, key.clone());
552 if !seen_unique.insert(marker) {
553 return Err(MongrelError::Conflict(format!(
554 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
555 uc.name
556 )));
557 }
558 let rows = load_rows(*table_id)?;
559 for r in &rows {
560 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
563 continue;
564 }
565 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
566 if theirs == key {
567 return Err(MongrelError::Conflict(format!(
568 "UNIQUE constraint '{}' on table '{tname}' violated",
569 uc.name
570 )));
571 }
572 }
573 }
574 }
575
576 for fk in &schema.constraints.foreign_keys {
578 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
579 continue; };
581 let Some(parent_id) = cat
582 .tables
583 .iter()
584 .find(|t| t.name == fk.ref_table)
585 .map(|t| t.table_id)
586 else {
587 return Err(MongrelError::InvalidArgument(format!(
588 "FOREIGN KEY '{}' references unknown table '{}'",
589 fk.name, fk.ref_table
590 )));
591 };
592 let parent_rows = load_rows(parent_id)?;
593 let mut found = false;
594 for r in &parent_rows {
595 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
596 continue;
597 }
598 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
599 if pkey == child_key {
600 found = true;
601 break;
602 }
603 }
604 }
605 if !found {
606 return Err(MongrelError::Conflict(format!(
607 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
608 fk.name, fk.ref_table
609 )));
610 }
611 }
612 }
613 Staged::Delete(rid) => {
614 let parent_handle = self.table_by_id(*table_id)?;
618 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
619 continue;
620 };
621 for (child_id, child_name, child_schema) in &live {
622 for fk in &child_schema.constraints.foreign_keys {
623 if fk.ref_table != tname || fk.on_delete != FkAction::Restrict {
624 continue;
625 }
626 let Some(parent_key) =
627 encode_composite_key(&fk.ref_columns, &parent_row.columns)
628 else {
629 continue;
630 };
631 let child_rows = load_rows(*child_id)?;
632 for r in &child_rows {
633 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
636 continue;
637 }
638 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
639 if ck == parent_key {
640 return Err(MongrelError::Conflict(format!(
641 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
642 fk.name
643 )));
644 }
645 }
646 }
647 }
648 }
649 }
650 Staged::Truncate => {
651 for (child_id, child_name, child_schema) in &live {
655 for fk in &child_schema.constraints.foreign_keys {
656 if fk.ref_table != tname {
657 continue;
658 }
659 let child_rows = load_rows(*child_id)?;
660 if child_rows
661 .iter()
662 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
663 {
664 return Err(MongrelError::Conflict(format!(
665 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
666 fk.name
667 )));
668 }
669 }
670 }
671 }
672 }
673 }
674 Ok(())
675 }
676
677 pub(crate) fn commit_transaction(
684 &self,
685 txn_id: u64,
686 read_epoch: Epoch,
687 mut staging: Vec<(u64, crate::txn::Staged)>,
688 ) -> Result<Epoch> {
689 use crate::memtable::Row;
690 use crate::txn::{Staged, StagedOp, WriteKey};
691 use crate::wal::Op;
692 use std::collections::hash_map::DefaultHasher;
693 use std::hash::{Hash, Hasher};
694 use std::sync::atomic::Ordering;
695
696 if self.poisoned.load(Ordering::Relaxed) {
697 return Err(MongrelError::Other(
698 "database poisoned by fsync error".into(),
699 ));
700 }
701
702 let write_keys = {
704 let cat = self.catalog.read();
705 let mut keys: Vec<WriteKey> = Vec::new();
706 for (table_id, staged) in &staging {
707 match staged {
708 Staged::Put(cells) => {
709 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
710 for col in &entry.schema.columns {
711 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
712 if let Some((_, val)) =
713 cells.iter().find(|(id, _)| *id == col.id)
714 {
715 let mut h = DefaultHasher::new();
716 val.encode_key().hash(&mut h);
717 keys.push(WriteKey::Unique {
718 table_id: *table_id,
719 index_id: 0,
720 key_hash: h.finish(),
721 });
722 }
723 }
724 }
725 for uc in &entry.schema.constraints.uniques {
732 if let Some(key_bytes) = crate::constraint::encode_composite_key(
733 &uc.columns,
734 &cells.iter().cloned().collect(),
735 ) {
736 let mut h = DefaultHasher::new();
737 key_bytes.hash(&mut h);
738 keys.push(WriteKey::Unique {
739 table_id: *table_id,
740 index_id: uc.id | 0x8000,
741 key_hash: h.finish(),
742 });
743 }
744 }
745 }
746 }
747 Staged::Delete(rid) => keys.push(WriteKey::Row {
748 table_id: *table_id,
749 row_id: rid.0,
750 }),
751 Staged::Truncate => keys.push(WriteKey::Table {
752 table_id: *table_id,
753 }),
754 }
755 }
756 keys
757 };
758
759 let min_active = self.active_txns.min_read_epoch();
761 if min_active < u64::MAX {
762 self.conflicts.prune_below(Epoch(min_active));
763 }
764
765 if self.conflicts.conflicts(&write_keys, read_epoch) {
769 return Err(MongrelError::Conflict(
770 "write-write conflict (pre-validate, first-committer-wins)".into(),
771 ));
772 }
773 let pre_validate_version = self.conflicts.version();
774
775 {
780 let tables = self.tables.read();
781 for (table_id, staged) in &mut staging {
782 if let Staged::Put(cells) = staged {
783 if let Some(handle) = tables.get(table_id) {
784 let mut t = handle.lock();
785 t.fill_auto_inc(cells)?;
786 }
787 }
788 }
789 }
790
791 self.validate_constraints(&mut staging, read_epoch)?;
798
799 let mut spilled: Vec<SpilledRun> = Vec::new();
803 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
804 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
808 {
809 let mut table_bytes: HashMap<u64, usize> = HashMap::new();
810 for (table_id, staged) in &staging {
811 if let Staged::Put(cells) = staged {
812 *table_bytes.entry(*table_id).or_default() += cells.len() * 16;
813 }
814 }
815 let tables = self.tables.read();
816 for (&table_id, &bytes) in &table_bytes {
817 if bytes as u64
818 <= self
819 .spill_threshold
820 .load(std::sync::atomic::Ordering::Relaxed)
821 {
822 continue;
823 }
824 let Some(handle) = tables.get(&table_id) else {
825 continue;
826 };
827 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
828 let mut t = handle.lock();
829 let tdir = t.table_dir().to_path_buf();
830 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
831 std::fs::create_dir_all(&txn_dir)?;
832 let run_id = t.alloc_run_id() as u128;
833 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
834
835 let mut rows: Vec<Row> = Vec::new();
836 for (tid, staged) in &staging {
837 if *tid != table_id {
838 continue;
839 }
840 if let Staged::Put(cells) = staged {
841 t.validate_cells_not_null(cells)?;
842 let row_id = t.alloc_row_id();
843 let mut row = Row::new(row_id, Epoch(0));
844 for (c, v) in cells {
845 row.columns.insert(*c, v.clone());
846 }
847 rows.push(row);
848 }
849 }
850 let schema = t.schema_ref().clone();
851 let kek = t.kek_ref().cloned();
852 let specs = t.indexable_column_specs();
853 drop(t);
854
855 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
856 .uniform_epoch(true);
857 if let Some(ref kek) = kek {
858 writer = writer.with_encryption(kek.as_ref(), specs);
859 }
860 let header = writer.write(&pending_path, &rows)?;
861 let row_count = header.row_count;
862 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
863 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
864
865 spilled.push(SpilledRun {
866 table_id,
867 run_id,
868 pending_path,
869 rows,
870 row_count,
871 min_rid,
872 max_rid,
873 });
874 spilled_tables.insert(table_id);
875 }
876 }
877
878 if spill_guard.is_some() {
880 if let Some(hook) = self.spill_hook.lock().as_ref() {
881 hook();
882 }
883 }
884
885 let mut prebuilt: Vec<Option<Row>> = Vec::with_capacity(staging.len());
896 {
897 let tables = self.tables.read();
898 for (table_id, staged) in &staging {
899 match staged {
900 Staged::Put(cells) if !spilled_tables.contains(table_id) => {
901 let handle = tables.get(table_id).ok_or_else(|| {
902 MongrelError::NotFound(format!("table {table_id} not mounted"))
903 })?;
904 let mut t = handle.lock();
905 t.validate_cells_not_null(cells)?;
906 let row_id = t.alloc_row_id();
907 drop(t);
908 let mut row = Row::new(row_id, Epoch(0));
909 for (c, v) in cells {
910 row.columns.insert(*c, v.clone());
911 }
912 prebuilt.push(Some(row));
913 }
914 Staged::Put(_) | Staged::Delete(_) | Staged::Truncate => prebuilt.push(None),
915 }
916 }
917 }
918
919 let added_runs: Vec<crate::wal::AddedRun> = spilled
921 .iter()
922 .map(|s| crate::wal::AddedRun {
923 table_id: s.table_id,
924 run_id: s.run_id,
925 row_count: s.row_count,
926 level: 0,
927 min_row_id: s.min_rid,
928 max_row_id: s.max_rid,
929 content_hash: [0u8; 32],
930 })
931 .collect();
932 let (new_epoch, applies, commit_seq) = {
933 let mut wal = self.shared_wal.lock();
934
935 if self.conflicts.version() != pre_validate_version
940 && self.conflicts.conflicts(&write_keys, read_epoch)
941 {
942 drop(wal);
946 for s in &spilled {
947 if let Some(parent) = s.pending_path.parent() {
948 let _ = std::fs::remove_dir_all(parent);
949 }
950 }
951 return Err(MongrelError::Conflict(
952 "write-write conflict (sequencer delta re-check)".into(),
953 ));
954 }
955
956 let new_epoch = self.epoch.bump_assigned();
957 let mut applies: Vec<(u64, Vec<StagedOp>)> = Vec::new();
958
959 for (idx, (table_id, staged)) in staging.iter().enumerate() {
960 if spilled_tables.contains(table_id) && matches!(staged, Staged::Put(_)) {
963 continue;
964 }
965 let mut ops = Vec::new();
966 match staged {
967 Staged::Put(_) => {
968 let mut row = prebuilt[idx].take().expect("prebuilt put row");
970 row.committed_epoch = new_epoch;
971 let payload = bincode::serialize(&vec![row.clone()])
972 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
973 wal.append(
974 txn_id,
975 *table_id,
976 Op::Put {
977 table_id: *table_id,
978 rows: payload,
979 },
980 )?;
981 ops.push(StagedOp::Put(row));
982 }
983 Staged::Delete(rid) => {
984 wal.append(
985 txn_id,
986 *table_id,
987 Op::Delete {
988 table_id: *table_id,
989 row_ids: vec![*rid],
990 },
991 )?;
992 ops.push(StagedOp::Delete(*rid));
993 }
994 Staged::Truncate => {
995 wal.append(
996 txn_id,
997 *table_id,
998 Op::TruncateTable {
999 table_id: *table_id,
1000 },
1001 )?;
1002 ops.push(StagedOp::Truncate);
1003 }
1004 }
1005 applies.push((*table_id, ops));
1006 }
1007
1008 let commit_seq = wal.append_commit(txn_id, new_epoch, &added_runs)?;
1009
1010 self.conflicts.record(&write_keys, new_epoch);
1015 (new_epoch, applies, commit_seq)
1016 };
1017
1018 self.group
1020 .await_durable(&self.shared_wal, commit_seq)
1021 .inspect_err(|_| {
1022 self.poisoned.store(true, Ordering::Relaxed);
1023 })?;
1024
1025 {
1027 let tables = self.tables.read();
1028 for s in &spilled {
1030 if let Some(handle) = tables.get(&s.table_id) {
1031 let mut t = handle.lock();
1032 let dest = t.run_path(s.run_id as u64);
1033 std::fs::rename(&s.pending_path, &dest)?;
1034 if let Some(parent) = s.pending_path.parent() {
1036 let _ = std::fs::remove_dir_all(parent);
1037 }
1038 t.link_run(crate::manifest::RunRef {
1039 run_id: s.run_id,
1040 level: 0,
1041 epoch_created: new_epoch.0,
1042 row_count: s.row_count,
1043 });
1044 t.apply_run_metadata(&s.rows)?;
1049 t.invalidate_pending_cache();
1050 t.persist_manifest(new_epoch)?;
1051 }
1052 }
1053 for (table_id, ops) in applies {
1055 if let Some(handle) = tables.get(&table_id) {
1056 let mut t = handle.lock();
1057 for op in ops {
1058 match op {
1059 StagedOp::Put(row) => t.apply_put_rows(vec![row])?,
1060 StagedOp::Delete(rid) => t.apply_delete(rid, new_epoch),
1061 StagedOp::Truncate => t.apply_truncate(new_epoch)?,
1062 }
1063 }
1064 t.invalidate_pending_cache();
1065 t.persist_manifest(new_epoch)?;
1066 }
1067 }
1068 }
1069
1070 self.advance_visible(new_epoch);
1071 Ok(new_epoch)
1072 }
1073
1074 fn advance_visible(&self, published: Epoch) {
1080 self.epoch.publish_in_order(published);
1081 }
1082
1083 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
1086 let e = self.epoch.visible();
1087 let g = self.snapshots.register(e);
1088 (Snapshot::at(e), g)
1089 }
1090
1091 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
1094 let e = self.epoch.visible();
1095 let g = self.snapshots.register_owned(e);
1096 (Snapshot::at(e), g)
1097 }
1098
1099 pub fn table_names(&self) -> Vec<String> {
1101 self.catalog
1102 .read()
1103 .tables
1104 .iter()
1105 .filter(|t| matches!(t.state, TableState::Live))
1106 .map(|t| t.name.clone())
1107 .collect()
1108 }
1109
1110 pub fn table(&self, name: &str) -> Result<TableHandle> {
1112 let cat = self.catalog.read();
1113 let entry = cat
1114 .live(name)
1115 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
1116 let id = entry.table_id;
1117 drop(cat);
1118 self.tables
1119 .read()
1120 .get(&id)
1121 .cloned()
1122 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
1123 }
1124
1125 fn table_by_id(&self, id: u64) -> Result<TableHandle> {
1128 self.tables
1129 .read()
1130 .get(&id)
1131 .cloned()
1132 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
1133 }
1134
1135 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
1141 use crate::wal::DdlOp;
1142 use std::sync::atomic::Ordering;
1143
1144 if self.poisoned.load(Ordering::Relaxed) {
1145 return Err(MongrelError::Other(
1146 "database poisoned by fsync error".into(),
1147 ));
1148 }
1149
1150 let _g = self.ddl_lock.lock();
1151 {
1152 let cat = self.catalog.read();
1153 if cat.live(name).is_some() {
1154 return Err(MongrelError::InvalidArgument(format!(
1155 "table {name:?} already exists"
1156 )));
1157 }
1158 }
1159
1160 let commit_lock = Arc::clone(&self.commit_lock);
1163 let _c = commit_lock.lock();
1164 let table_id = {
1165 let mut cat = self.catalog.write();
1166 let id = cat.next_table_id;
1167 cat.next_table_id += 1;
1168 id
1169 };
1170 let epoch = self.epoch.bump_assigned();
1171 let txn_id = self.alloc_txn_id();
1172
1173 let mut schema = schema;
1177 schema.schema_id = table_id;
1178 schema.validate_auto_increment()?;
1185
1186 let schema_json = DdlOp::encode_schema(&schema)?;
1189 let commit_seq = {
1190 let mut wal = self.shared_wal.lock();
1191 wal.append(
1192 txn_id,
1193 table_id,
1194 crate::wal::Op::Ddl(DdlOp::CreateTable {
1195 table_id,
1196 name: name.to_string(),
1197 schema_json,
1198 }),
1199 )?;
1200 wal.append_commit(txn_id, epoch, &[])?
1201 };
1202 self.group
1203 .await_durable(&self.shared_wal, commit_seq)
1204 .inspect_err(|_| {
1205 self.poisoned.store(true, Ordering::Relaxed);
1206 })?;
1207
1208 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
1210 std::fs::create_dir_all(&tdir)?;
1211 let ctx = SharedCtx {
1212 epoch: Arc::clone(&self.epoch),
1213 page_cache: Arc::clone(&self.page_cache),
1214 decoded_cache: Arc::clone(&self.decoded_cache),
1215 snapshots: Arc::clone(&self.snapshots),
1216 kek: self.kek.clone(),
1217 commit_lock: Arc::clone(&self.commit_lock),
1218 shared: Some(crate::engine::SharedWalCtx {
1219 wal: Arc::clone(&self.shared_wal),
1220 group: Arc::clone(&self.group),
1221 poisoned: Arc::clone(&self.poisoned),
1222 txn_ids: Arc::clone(&self.next_txn_id),
1223 }),
1224 };
1225 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
1226
1227 {
1230 let mut cat = self.catalog.write();
1231 cat.tables.push(CatalogEntry {
1232 table_id,
1233 name: name.to_string(),
1234 schema,
1235 state: TableState::Live,
1236 created_epoch: epoch.0,
1237 });
1238 }
1239 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1240 self.tables
1241 .write()
1242 .insert(table_id, Arc::new(Mutex::new(table)));
1243
1244 self.advance_visible(epoch);
1245 Ok(table_id)
1246 }
1247
1248 pub fn drop_table(&self, name: &str) -> Result<()> {
1250 use crate::wal::DdlOp;
1251 use std::sync::atomic::Ordering;
1252
1253 if self.poisoned.load(Ordering::Relaxed) {
1254 return Err(MongrelError::Other(
1255 "database poisoned by fsync error".into(),
1256 ));
1257 }
1258
1259 let _g = self.ddl_lock.lock();
1260 let table_id = {
1261 let cat = self.catalog.read();
1262 cat.live(name)
1263 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
1264 .table_id
1265 };
1266
1267 let commit_lock = Arc::clone(&self.commit_lock);
1268 let _c = commit_lock.lock();
1269 let epoch = self.epoch.bump_assigned();
1270 let txn_id = self.alloc_txn_id();
1271 let commit_seq = {
1272 let mut wal = self.shared_wal.lock();
1273 wal.append(
1274 txn_id,
1275 table_id,
1276 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
1277 )?;
1278 wal.append_commit(txn_id, epoch, &[])?
1279 };
1280 self.group
1281 .await_durable(&self.shared_wal, commit_seq)
1282 .inspect_err(|_| {
1283 self.poisoned.store(true, Ordering::Relaxed);
1284 })?;
1285
1286 {
1287 let mut cat = self.catalog.write();
1288 let entry = cat
1289 .tables
1290 .iter_mut()
1291 .find(|t| t.table_id == table_id)
1292 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
1293 entry.state = TableState::Dropped { at_epoch: epoch.0 };
1294 }
1295 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1296 self.tables.write().remove(&table_id);
1297
1298 self.advance_visible(epoch);
1299 Ok(())
1300 }
1301
1302 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
1311 use crate::wal::DdlOp;
1312 use std::sync::atomic::Ordering;
1313
1314 if self.poisoned.load(Ordering::Relaxed) {
1315 return Err(MongrelError::Other(
1316 "database poisoned by fsync error".into(),
1317 ));
1318 }
1319
1320 if name == new_name {
1323 return Ok(());
1324 }
1325 if new_name.is_empty() {
1326 return Err(MongrelError::InvalidArgument(
1327 "rename_table: new name must not be empty".into(),
1328 ));
1329 }
1330
1331 let _g = self.ddl_lock.lock();
1332 let table_id = {
1333 let cat = self.catalog.read();
1334 let src = cat
1335 .live(name)
1336 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
1337 if cat.live(new_name).is_some() {
1341 return Err(MongrelError::InvalidArgument(format!(
1342 "rename_table: a table named {new_name:?} already exists"
1343 )));
1344 }
1345 src.table_id
1346 };
1347
1348 let commit_lock = Arc::clone(&self.commit_lock);
1349 let _c = commit_lock.lock();
1350 let epoch = self.epoch.bump_assigned();
1351 let txn_id = self.alloc_txn_id();
1352 let commit_seq = {
1353 let mut wal = self.shared_wal.lock();
1354 wal.append(
1355 txn_id,
1356 table_id,
1357 crate::wal::Op::Ddl(DdlOp::RenameTable {
1358 table_id,
1359 new_name: new_name.to_string(),
1360 }),
1361 )?;
1362 wal.append_commit(txn_id, epoch, &[])?
1363 };
1364 self.group
1365 .await_durable(&self.shared_wal, commit_seq)
1366 .inspect_err(|_| {
1367 self.poisoned.store(true, Ordering::Relaxed);
1368 })?;
1369
1370 {
1371 let mut cat = self.catalog.write();
1372 let entry = cat
1373 .tables
1374 .iter_mut()
1375 .find(|t| t.table_id == table_id)
1376 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
1377 entry.name = new_name.to_string();
1378 }
1379 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1380 self.advance_visible(epoch);
1383 Ok(())
1384 }
1385
1386 pub fn alter_column(
1387 &self,
1388 table_name: &str,
1389 column_name: &str,
1390 change: AlterColumn,
1391 ) -> Result<ColumnDef> {
1392 use crate::wal::DdlOp;
1393 use std::sync::atomic::Ordering;
1394
1395 if self.poisoned.load(Ordering::Relaxed) {
1396 return Err(MongrelError::Other(
1397 "database poisoned by fsync error".into(),
1398 ));
1399 }
1400
1401 let _g = self.ddl_lock.lock();
1402 let table_id = {
1403 let cat = self.catalog.read();
1404 cat.live(table_name)
1405 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
1406 .table_id
1407 };
1408 let handle =
1409 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
1410 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
1411 })?;
1412 let mut table = handle.lock();
1413 let column = table.prepare_alter_column(column_name, &change)?;
1414 if table
1415 .schema()
1416 .columns
1417 .iter()
1418 .find(|c| c.id == column.id)
1419 .is_some_and(|c| c == &column)
1420 {
1421 return Ok(column);
1422 }
1423
1424 let commit_lock = Arc::clone(&self.commit_lock);
1425 let _c = commit_lock.lock();
1426 let epoch = self.epoch.bump_assigned();
1427 let txn_id = self.alloc_txn_id();
1428 let column_json = DdlOp::encode_column(&column)?;
1429 let commit_seq = {
1430 let mut wal = self.shared_wal.lock();
1431 wal.append(
1432 txn_id,
1433 table_id,
1434 crate::wal::Op::Ddl(DdlOp::AlterTable {
1435 table_id,
1436 column_json,
1437 }),
1438 )?;
1439 wal.append_commit(txn_id, epoch, &[])?
1440 };
1441 self.group
1442 .await_durable(&self.shared_wal, commit_seq)
1443 .inspect_err(|_| {
1444 self.poisoned.store(true, Ordering::Relaxed);
1445 })?;
1446
1447 table.apply_altered_column(column.clone())?;
1448 let schema = table.schema().clone();
1449 drop(table);
1450
1451 {
1452 let mut cat = self.catalog.write();
1453 let entry = cat
1454 .tables
1455 .iter_mut()
1456 .find(|t| t.table_id == table_id)
1457 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
1458 entry.schema = schema;
1459 }
1460 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1461
1462 self.advance_visible(epoch);
1463 Ok(column)
1464 }
1465
1466 pub fn gc(&self) -> Result<usize> {
1472 let min_active = self.snapshots.min_active(self.epoch.visible()).0;
1473 let mut reclaimed = 0;
1474
1475 let cat = self.catalog.read();
1477 for entry in &cat.tables {
1478 if let TableState::Dropped { at_epoch } = entry.state {
1479 if at_epoch <= min_active {
1480 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
1481 if tdir.exists() {
1482 std::fs::remove_dir_all(&tdir)?;
1483 reclaimed += 1;
1484 }
1485 }
1486 }
1487 }
1488 drop(cat);
1489
1490 let cat = self.catalog.read();
1495 for entry in &cat.tables {
1496 if !matches!(entry.state, TableState::Live) {
1497 continue;
1498 }
1499 let txn_dir = self
1500 .root
1501 .join(TABLES_DIR)
1502 .join(entry.table_id.to_string())
1503 .join("_txn");
1504 if !txn_dir.exists() {
1505 continue;
1506 }
1507 for sub in std::fs::read_dir(&txn_dir)? {
1508 let sub = sub?;
1509 let name = sub.file_name();
1510 let Some(name) = name.to_str() else { continue };
1511 let is_active = name
1513 .parse::<u64>()
1514 .map(|id| self.active_spills.is_active(id))
1515 .unwrap_or(false);
1516 if is_active {
1517 continue;
1518 }
1519 std::fs::remove_dir_all(sub.path())?;
1520 reclaimed += 1;
1521 }
1522 }
1523 drop(cat);
1524
1525 let tables = self.tables.read();
1529 for handle in tables.values() {
1530 reclaimed += handle.lock().reap_retiring(Epoch(min_active))?;
1531 }
1532
1533 let all_durable = self.active_spills.is_idle()
1541 && tables.values().all(|h| {
1542 let g = h.lock();
1543 g.memtable_len() == 0 && g.mutable_run_len() == 0
1544 });
1545 drop(tables);
1546 if all_durable {
1547 reclaimed += self.shared_wal.lock().gc_segments(u64::MAX)?;
1548 }
1549
1550 Ok(reclaimed)
1551 }
1552 fn alloc_txn_id(&self) -> u64 {
1553 let mut g = self.next_txn_id.lock();
1554 let id = *g;
1555 *g = g.wrapping_add(1);
1556 id
1557 }
1558
1559 pub fn set_spill_threshold(&self, bytes: u64) {
1563 self.spill_threshold
1564 .store(bytes, std::sync::atomic::Ordering::Relaxed);
1565 }
1566
1567 #[doc(hidden)]
1571 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
1572 *self.spill_hook.lock() = Some(Box::new(f));
1573 }
1574
1575 #[doc(hidden)]
1579 pub fn __wal_group_sync_count(&self) -> u64 {
1580 self.shared_wal.lock().group_sync_count()
1581 }
1582
1583 #[doc(hidden)]
1586 pub fn __poison(&self) {
1587 self.poisoned
1588 .store(true, std::sync::atomic::Ordering::Relaxed);
1589 }
1590
1591 pub fn check(&self) -> Vec<CheckIssue> {
1604 let mut issues = Vec::new();
1605 let cat = self.catalog.read();
1606 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
1607 for entry in &cat.tables {
1608 if !matches!(entry.state, TableState::Live) {
1609 continue;
1610 }
1611 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
1612 let mut err = |sev: &str, desc: String| {
1613 issues.push(CheckIssue {
1614 table_id: entry.table_id,
1615 table_name: entry.name.clone(),
1616 severity: sev.into(),
1617 description: desc,
1618 });
1619 };
1620 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
1621 Ok(m) => m,
1622 Err(e) => {
1623 err("error", format!("manifest read failed: {e}"));
1624 continue;
1625 }
1626 };
1627 if m.flushed_epoch > m.current_epoch {
1628 err(
1629 "error",
1630 format!(
1631 "flushed_epoch {} exceeds current_epoch {} (impossible)",
1632 m.flushed_epoch, m.current_epoch
1633 ),
1634 );
1635 }
1636
1637 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
1638 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
1639 for rr in &m.runs {
1640 referenced.insert(rr.run_id);
1641 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
1642 if !run_path.exists() {
1643 err("error", format!("missing run file: r-{}.sr", rr.run_id));
1644 continue;
1645 }
1646 match crate::sorted_run::RunReader::open(
1647 &run_path,
1648 entry.schema.clone(),
1649 self.kek.clone(),
1650 ) {
1651 Ok(reader) => {
1652 if reader.row_count() as u64 != rr.row_count {
1653 err(
1654 "error",
1655 format!(
1656 "run r-{} row count mismatch: manifest {} vs run {}",
1657 rr.run_id,
1658 rr.row_count,
1659 reader.row_count()
1660 ),
1661 );
1662 }
1663 }
1664 Err(e) => {
1665 err(
1666 "error",
1667 format!("run r-{} integrity check failed: {e}", rr.run_id),
1668 );
1669 }
1670 }
1671 }
1672
1673 for r in &m.retiring {
1677 referenced.insert(r.run_id);
1678 }
1679
1680 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
1682 for ent in rd.flatten() {
1683 let p = ent.path();
1684 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
1685 continue;
1686 }
1687 let run_id = p
1688 .file_stem()
1689 .and_then(|s| s.to_str())
1690 .and_then(|s| s.strip_prefix("r-"))
1691 .and_then(|s| s.parse::<u128>().ok());
1692 if let Some(id) = run_id {
1693 if !referenced.contains(&id) {
1694 err(
1695 "warning",
1696 format!("orphan run file r-{id}.sr not referenced by the manifest"),
1697 );
1698 }
1699 }
1700 }
1701 }
1702 }
1703
1704 for (seg, msg) in self.shared_wal.lock().verify_segments() {
1711 issues.push(CheckIssue {
1712 table_id: WAL_TABLE_ID,
1713 table_name: "<wal>".into(),
1714 severity: "error".into(),
1715 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
1716 });
1717 }
1718 issues
1719 }
1720
1721 pub fn doctor(&self) -> Result<Vec<u64>> {
1725 let _ddl = self.ddl_lock.lock();
1728 let issues = self.check();
1729 let bad_tables: std::collections::HashSet<u64> = issues
1734 .iter()
1735 .filter(|i| i.severity == "error" && i.table_id != WAL_TABLE_ID)
1736 .map(|i| i.table_id)
1737 .collect();
1738 if bad_tables.is_empty() {
1739 return Ok(Vec::new());
1740 }
1741
1742 let qdir = self.root.join("_quarantine");
1743 std::fs::create_dir_all(&qdir)?;
1744 let mut quarantined = Vec::new();
1745 for &table_id in &bad_tables {
1746 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
1747 if tdir.exists() {
1748 let dest = qdir.join(table_id.to_string());
1749 std::fs::rename(&tdir, &dest)?;
1750 }
1751 {
1752 let mut cat = self.catalog.write();
1753 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
1754 entry.state = TableState::Dropped {
1755 at_epoch: self.epoch.visible().0,
1756 };
1757 }
1758 }
1759 self.tables.write().remove(&table_id);
1761 quarantined.push(table_id);
1762 }
1763 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1764 Ok(quarantined)
1765 }
1766
1767 #[allow(dead_code)]
1769 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
1770 self.kek.as_ref()
1771 }
1772
1773 #[allow(dead_code)]
1775 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
1776 &self.epoch
1777 }
1778
1779 #[allow(dead_code)]
1781 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
1782 &self.snapshots
1783 }
1784}
1785
1786fn recover_shared_wal(
1795 root: &Path,
1796 tables: &HashMap<u64, TableHandle>,
1797 epoch: &EpochAuthority,
1798 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
1799) -> Result<()> {
1800 use crate::memtable::Row;
1801 use crate::rowid::RowId;
1802 use crate::wal::{Op, SharedWal};
1803
1804 let records = SharedWal::replay_with_dek(root, wal_dek)?;
1805
1806 let mut committed: HashMap<u64, u64> = HashMap::new();
1808 let mut spilled_to_link: Vec<(
1809 u64, u64, Vec<crate::wal::AddedRun>,
1812 )> = Vec::new();
1813 for r in &records {
1814 if let Op::TxnCommit {
1815 epoch: ce,
1816 ref added_runs,
1817 } = r.op
1818 {
1819 committed.insert(r.txn_id, ce);
1820 if !added_runs.is_empty() {
1821 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
1822 }
1823 }
1824 }
1825
1826 type TableStage = (Vec<Row>, Vec<(RowId, Epoch)>, Option<Epoch>, Epoch);
1828 let mut stage: HashMap<u64, TableStage> = HashMap::new();
1829 let mut max_epoch = epoch.visible().0;
1830 for r in records {
1831 let Some(&ce) = committed.get(&r.txn_id) else {
1832 continue; };
1834 let commit_epoch = Epoch(ce);
1835 max_epoch = max_epoch.max(ce);
1836 match r.op {
1837 Op::Put { table_id, rows } => {
1838 let skip = tables
1840 .get(&table_id)
1841 .map(|h| h.lock().flushed_epoch() >= ce)
1842 .unwrap_or(true);
1843 if skip {
1844 continue;
1845 }
1846 let rows: Vec<Row> = match bincode::deserialize(&rows) {
1847 Ok(v) => v,
1848 Err(_) => continue,
1849 };
1850 let rows: Vec<Row> = rows
1853 .into_iter()
1854 .map(|mut row| {
1855 row.committed_epoch = commit_epoch;
1856 row
1857 })
1858 .collect();
1859 let entry = stage
1860 .entry(table_id)
1861 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
1862 entry.0.extend(rows);
1863 entry.3 = commit_epoch;
1864 }
1865 Op::Delete { table_id, row_ids } => {
1866 let skip = tables
1867 .get(&table_id)
1868 .map(|h| h.lock().flushed_epoch() >= ce)
1869 .unwrap_or(true);
1870 if skip {
1871 continue;
1872 }
1873 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
1874 let entry = stage
1875 .entry(table_id)
1876 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
1877 entry.1.extend(dels);
1878 entry.3 = commit_epoch;
1879 }
1880 Op::TruncateTable { table_id } => {
1881 let skip = tables
1882 .get(&table_id)
1883 .map(|h| h.lock().flushed_epoch() >= ce)
1884 .unwrap_or(true);
1885 if skip {
1886 continue;
1887 }
1888 stage.insert(
1889 table_id,
1890 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
1891 );
1892 }
1893 _ => {}
1894 }
1895 }
1896
1897 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
1898 let Some(handle) = tables.get(&table_id) else {
1899 continue;
1900 };
1901 let mut t = handle.lock();
1902 if let Some(epoch) = truncate_epoch {
1903 t.apply_truncate(epoch)?;
1904 }
1905 t.recover_apply(rows, deletes)?;
1906 if truncate_epoch.is_some() {
1907 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
1908 t.live_count = rows.len() as u64;
1909 t.persist_manifest(table_epoch)?;
1910 }
1911 }
1912
1913 for (txn_id, ce, added_runs) in &spilled_to_link {
1917 for ar in added_runs {
1918 let Some(handle) = tables.get(&ar.table_id) else {
1919 continue;
1920 };
1921 let mut t = handle.lock();
1922 let dest = t.run_path(ar.run_id as u64);
1923 if !dest.exists() {
1924 let pending = root
1925 .join(TABLES_DIR)
1926 .join(ar.table_id.to_string())
1927 .join("_txn")
1928 .join(txn_id.to_string())
1929 .join(format!("r-{}.sr", ar.run_id));
1930 if pending.exists() {
1931 if let Some(parent) = pending.parent() {
1932 std::fs::rename(&pending, &dest)?;
1933 let _ = std::fs::remove_dir_all(parent);
1934 }
1935 }
1936 }
1937 if t.run_path(ar.run_id as u64).exists() {
1943 t.recover_spilled_run(crate::manifest::RunRef {
1944 run_id: ar.run_id,
1945 level: ar.level,
1946 epoch_created: *ce,
1947 row_count: ar.row_count,
1948 });
1949 }
1950 }
1951 }
1952
1953 epoch.advance_recovered(Epoch(max_epoch));
1954 Ok(())
1955}
1956
1957fn recover_ddl_from_wal(
1963 root: &Path,
1964 cat: &mut Catalog,
1965 meta_dek: Option<&[u8; META_DEK_LEN]>,
1966 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
1967) -> Result<()> {
1968 use crate::wal::{DdlOp, Op, SharedWal};
1969
1970 let records = match SharedWal::replay_with_dek(root, wal_dek) {
1971 Ok(r) => r,
1972 Err(_) => return Ok(()),
1973 };
1974
1975 let mut committed: HashMap<u64, u64> = HashMap::new();
1976 for r in &records {
1977 if let Op::TxnCommit { epoch: ce, .. } = r.op {
1978 committed.insert(r.txn_id, ce);
1979 }
1980 }
1981
1982 let mut changed = false;
1983 for r in records {
1984 let Some(&ce) = committed.get(&r.txn_id) else {
1985 continue;
1986 };
1987 match r.op {
1988 Op::Ddl(DdlOp::CreateTable {
1989 table_id,
1990 ref name,
1991 ref schema_json,
1992 }) => {
1993 if cat.tables.iter().any(|t| t.table_id == table_id) {
1994 continue;
1995 }
1996 let schema = DdlOp::decode_schema(schema_json)?;
1997 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
1998 if !tdir.exists() {
1999 std::fs::create_dir_all(tdir.join(crate::engine::WAL_DIR))?;
2000 std::fs::create_dir_all(tdir.join(crate::engine::RUNS_DIR))?;
2001 crate::engine::write_schema(&tdir, &schema)?;
2002 let mut m = crate::manifest::Manifest::new(table_id, schema.schema_id);
2008 crate::manifest::write_atomic(&tdir, &mut m, meta_dek)?;
2009 }
2010 cat.tables.push(CatalogEntry {
2011 table_id,
2012 name: name.clone(),
2013 schema,
2014 state: TableState::Live,
2015 created_epoch: ce,
2016 });
2017 cat.next_table_id = cat.next_table_id.max(table_id + 1);
2018 changed = true;
2019 }
2020 Op::Ddl(DdlOp::DropTable { table_id }) => {
2021 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
2022 if matches!(entry.state, TableState::Live) {
2023 entry.state = TableState::Dropped { at_epoch: ce };
2024 changed = true;
2025 }
2026 }
2027 }
2028 Op::Ddl(DdlOp::RenameTable {
2029 table_id,
2030 ref new_name,
2031 }) => {
2032 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
2033 if entry.name != *new_name {
2034 entry.name = new_name.clone();
2035 changed = true;
2036 }
2037 }
2038 }
2042 Op::Ddl(DdlOp::AlterTable {
2043 table_id,
2044 ref column_json,
2045 }) => {
2046 let column = DdlOp::decode_column(column_json)?;
2047 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
2048 if apply_recovered_column_def(&mut entry.schema, column) {
2049 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
2050 if tdir.exists() {
2051 crate::engine::write_schema(&tdir, &entry.schema)?;
2052 }
2053 changed = true;
2054 }
2055 }
2056 }
2057 _ => {}
2058 }
2059 }
2060
2061 if changed {
2062 catalog::write_atomic(root, cat, meta_dek)?;
2063 }
2064 Ok(())
2065}
2066
2067fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> bool {
2068 match schema.columns.iter_mut().find(|c| c.id == column.id) {
2069 Some(existing) if *existing == column => false,
2070 Some(existing) => {
2071 *existing = column;
2072 schema.schema_id = schema.schema_id.saturating_add(1);
2073 true
2074 }
2075 None => {
2076 schema.columns.push(column);
2077 schema.schema_id = schema.schema_id.saturating_add(1);
2078 true
2079 }
2080 }
2081}
2082
2083fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
2088 for entry in &cat.tables {
2089 let txn_dir = root
2090 .join(TABLES_DIR)
2091 .join(entry.table_id.to_string())
2092 .join("_txn");
2093 if txn_dir.exists() {
2094 let _ = std::fs::remove_dir_all(&txn_dir);
2095 }
2096 }
2097}