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<crate::cache::Sharded<crate::cache::PageCache>>,
62 decoded_cache: Arc<crate::cache::Sharded<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(crate::cache::Sharded::new(
213 crate::cache::CACHE_SHARDS,
214 || {
215 crate::cache::PageCache::new(
216 crate::engine::PAGE_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
217 )
218 },
219 ));
220 let decoded_cache = Arc::new(crate::cache::Sharded::new(
221 crate::cache::CACHE_SHARDS,
222 || {
223 crate::cache::DecodedPageCache::new(
224 crate::engine::DECODED_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
225 )
226 },
227 ));
228 let commit_lock = Arc::new(Mutex::new(()));
229 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
230 let shared_wal = Arc::new(Mutex::new(if existing {
231 crate::wal::SharedWal::open(&root, Epoch(cat.db_epoch), wal_dek.clone())?
232 } else {
233 crate::wal::SharedWal::create_with_dek(&root, Epoch(cat.db_epoch), wal_dek.clone())?
234 }));
235 let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));
239 let group = Arc::new(crate::txn::GroupCommit::new(
240 shared_wal.lock().durable_seq(),
241 ));
242 let txn_ids = Arc::new(Mutex::new(1u64));
246
247 let mut cat = cat;
253 if existing {
254 recover_ddl_from_wal(&root, &mut cat, meta_dek.as_ref(), wal_dek.as_ref())?;
255 }
256
257 let mut tables: HashMap<u64, TableHandle> = HashMap::new();
263 for entry in &cat.tables {
264 if !matches!(entry.state, TableState::Live) {
265 continue;
266 }
267 let tdir = root.join(TABLES_DIR).join(entry.table_id.to_string());
268 let ctx = SharedCtx {
269 epoch: Arc::clone(&epoch),
270 page_cache: Arc::clone(&page_cache),
271 decoded_cache: Arc::clone(&decoded_cache),
272 snapshots: Arc::clone(&snapshots),
273 kek: kek.clone(),
274 commit_lock: Arc::clone(&commit_lock),
275 shared: Some(crate::engine::SharedWalCtx {
276 wal: Arc::clone(&shared_wal),
277 group: Arc::clone(&group),
278 poisoned: Arc::clone(&poisoned),
279 txn_ids: Arc::clone(&txn_ids),
280 }),
281 };
282 let t = Table::open_in(&tdir, ctx)?;
283 tables.insert(entry.table_id, Arc::new(Mutex::new(t)));
284 }
285
286 if existing {
292 recover_shared_wal(&root, &tables, &epoch, wal_dek.as_ref())?;
293 sweep_pending_txn_dirs(&root, &cat);
296 }
297
298 if existing {
302 cat.open_generation = cat.open_generation.wrapping_add(1);
303 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
304 }
305 let next_txn_id = (cat.open_generation << 32) | 1;
306 *txn_ids.lock() = next_txn_id;
308
309 Ok(Self {
310 root,
311 catalog: RwLock::new(cat),
312 epoch,
313 snapshots,
314 page_cache,
315 decoded_cache,
316 commit_lock,
317 shared_wal,
318 next_txn_id: txn_ids,
319 tables: RwLock::new(tables),
320 kek,
321 ddl_lock: Mutex::new(()),
322 meta_dek,
323 conflicts: crate::txn::ConflictIndex::new(),
324 active_txns: crate::txn::ActiveTxns::new(),
325 poisoned,
326 group,
327 spill_threshold: std::sync::atomic::AtomicU64::new(64 * 1024 * 1024),
328 active_spills: Arc::new(crate::retention::ActiveSpills::new()),
329 spill_hook: Mutex::new(None),
330 })
331 }
332
333 pub fn visible_epoch(&self) -> Epoch {
335 self.epoch.visible()
336 }
337
338 pub fn catalog_snapshot(&self) -> Catalog {
340 self.catalog.read().clone()
341 }
342
343 pub fn root(&self) -> &Path {
345 &self.root
346 }
347
348 pub fn table_id(&self, name: &str) -> Result<u64> {
351 let cat = self.catalog.read();
352 cat.live(name)
353 .map(|e| e.table_id)
354 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
355 }
356
357 pub fn begin(&self) -> crate::txn::Transaction<'_> {
359 let txn_id = self.alloc_txn_id();
360 let read = Snapshot::at(self.epoch.visible());
361 crate::txn::Transaction::new(self, txn_id, read)
362 }
363
364 pub fn transaction<T>(
366 &self,
367 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
368 ) -> Result<T> {
369 let mut tx = self.begin();
370 match f(&mut tx) {
371 Ok(out) => {
372 tx.commit()?;
373 Ok(out)
374 }
375 Err(e) => {
376 tx.rollback();
377 Err(e)
378 }
379 }
380 }
381
382 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
385 self.active_txns.register(epoch)
386 }
387
388 fn validate_constraints(
403 &self,
404 staging: &mut Vec<(u64, crate::txn::Staged)>,
405 read_epoch: Epoch,
406 ) -> Result<()> {
407 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
408 use crate::memtable::Row;
409 use crate::txn::Staged;
410 use std::collections::HashSet;
411
412 let snapshot = Snapshot::at(read_epoch);
413 let cat = self.catalog.read();
414
415 let live: Vec<(u64, &str, &crate::schema::Schema)> = cat
417 .tables
418 .iter()
419 .filter(|e| matches!(e.state, TableState::Live))
420 .map(|e| (e.table_id, e.name.as_str(), &e.schema))
421 .collect();
422
423 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
425 if !any_constraints {
426 return Ok(());
427 }
428
429 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
431 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
432 if let Some(r) = rows_cache.get(&table_id) {
433 return Ok(r.clone());
434 }
435 let handle = self.table_by_id(table_id)?;
436 let rows = handle.lock().visible_rows(snapshot)?;
437 rows_cache.insert(table_id, rows.clone());
438 Ok(rows)
439 };
440
441 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
446 loop {
447 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
448 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
449 .iter()
450 .filter_map(|(t, op)| match op {
451 Staged::Delete(rid) => Some((*t, *rid)),
452 _ => None,
453 })
454 .collect();
455 for (table_id, rid) in deletes {
456 if !cascaded.insert((table_id, rid.0)) {
457 continue;
458 }
459 let Some(tname) = live
460 .iter()
461 .find(|(t, _, _)| *t == table_id)
462 .map(|(_, n, _)| *n)
463 else {
464 continue;
465 };
466 let parent_handle = self.table_by_id(table_id)?;
467 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
468 continue;
469 };
470 for (child_id, _child_name, child_schema) in &live {
471 for fk in &child_schema.constraints.foreign_keys {
472 if fk.ref_table != tname {
473 continue;
474 }
475 let Some(parent_key) =
476 encode_composite_key(&fk.ref_columns, &parent_row.columns)
477 else {
478 continue;
479 };
480 match fk.on_delete {
481 FkAction::Restrict => continue,
482 FkAction::Cascade => {
483 let child_rows = load_rows(*child_id)?;
484 for cr in &child_rows {
485 if !cascaded.contains(&(*child_id, cr.row_id.0))
486 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
487 == Some(parent_key.as_slice())
488 {
489 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
490 }
491 }
492 }
493 FkAction::SetNull => {
494 let child_rows = load_rows(*child_id)?;
495 for cr in &child_rows {
496 if !cascaded.contains(&(*child_id, cr.row_id.0))
497 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
498 == Some(parent_key.as_slice())
499 {
500 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
503 .columns
504 .iter()
505 .map(|(k, v)| (*k, v.clone()))
506 .collect();
507 for cid in &fk.columns {
508 cells.retain(|(k, _)| k != cid);
509 cells.push((*cid, crate::memtable::Value::Null));
510 }
511 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
512 new_ops.push((*child_id, Staged::Put(cells)));
513 }
514 }
515 }
516 }
517 }
518 }
519 }
520 if new_ops.is_empty() {
521 break;
522 }
523 staging.extend(new_ops);
524 }
525
526 let staged_deletes: HashSet<(u64, u64)> = staging
530 .iter()
531 .filter_map(|(t, op)| match op {
532 Staged::Delete(rid) => Some((*t, rid.0)),
533 _ => None,
534 })
535 .collect();
536
537 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
539
540 for (table_id, op) in staging.iter() {
542 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id).copied()
543 else {
544 continue;
545 };
546 let cells_map: HashMap<u16, crate::memtable::Value>;
547 match op {
548 Staged::Put(cells) => {
549 cells_map = cells.iter().cloned().collect();
550
551 if !schema.constraints.checks.is_empty() {
553 validate_checks(&schema.constraints.checks, &cells_map)?;
554 }
555
556 for uc in &schema.constraints.uniques {
558 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
559 continue; };
561 let marker = (*table_id, uc.id, key.clone());
562 if !seen_unique.insert(marker) {
563 return Err(MongrelError::Conflict(format!(
564 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
565 uc.name
566 )));
567 }
568 let rows = load_rows(*table_id)?;
569 for r in &rows {
570 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
573 continue;
574 }
575 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
576 if theirs == key {
577 return Err(MongrelError::Conflict(format!(
578 "UNIQUE constraint '{}' on table '{tname}' violated",
579 uc.name
580 )));
581 }
582 }
583 }
584 }
585
586 for fk in &schema.constraints.foreign_keys {
588 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
589 continue; };
591 let Some(parent_id) = cat
592 .tables
593 .iter()
594 .find(|t| t.name == fk.ref_table)
595 .map(|t| t.table_id)
596 else {
597 return Err(MongrelError::InvalidArgument(format!(
598 "FOREIGN KEY '{}' references unknown table '{}'",
599 fk.name, fk.ref_table
600 )));
601 };
602 let parent_rows = load_rows(parent_id)?;
603 let mut found = false;
604 for r in &parent_rows {
605 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
606 continue;
607 }
608 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
609 if pkey == child_key {
610 found = true;
611 break;
612 }
613 }
614 }
615 if !found {
616 return Err(MongrelError::Conflict(format!(
617 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
618 fk.name, fk.ref_table
619 )));
620 }
621 }
622 }
623 Staged::Delete(rid) => {
624 let parent_handle = self.table_by_id(*table_id)?;
628 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
629 continue;
630 };
631 for (child_id, child_name, child_schema) in &live {
632 for fk in &child_schema.constraints.foreign_keys {
633 if fk.ref_table != tname || fk.on_delete != FkAction::Restrict {
634 continue;
635 }
636 let Some(parent_key) =
637 encode_composite_key(&fk.ref_columns, &parent_row.columns)
638 else {
639 continue;
640 };
641 let child_rows = load_rows(*child_id)?;
642 for r in &child_rows {
643 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
646 continue;
647 }
648 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
649 if ck == parent_key {
650 return Err(MongrelError::Conflict(format!(
651 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
652 fk.name
653 )));
654 }
655 }
656 }
657 }
658 }
659 }
660 Staged::Truncate => {
661 for (child_id, child_name, child_schema) in &live {
665 for fk in &child_schema.constraints.foreign_keys {
666 if fk.ref_table != tname {
667 continue;
668 }
669 let child_rows = load_rows(*child_id)?;
670 if child_rows
671 .iter()
672 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
673 {
674 return Err(MongrelError::Conflict(format!(
675 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
676 fk.name
677 )));
678 }
679 }
680 }
681 }
682 }
683 }
684 Ok(())
685 }
686
687 pub(crate) fn commit_transaction(
694 &self,
695 txn_id: u64,
696 read_epoch: Epoch,
697 mut staging: Vec<(u64, crate::txn::Staged)>,
698 ) -> Result<Epoch> {
699 use crate::memtable::Row;
700 use crate::txn::{Staged, StagedOp, WriteKey};
701 use crate::wal::Op;
702 use std::collections::hash_map::DefaultHasher;
703 use std::hash::{Hash, Hasher};
704 use std::sync::atomic::Ordering;
705
706 if self.poisoned.load(Ordering::Relaxed) {
707 return Err(MongrelError::Other(
708 "database poisoned by fsync error".into(),
709 ));
710 }
711
712 let write_keys = {
714 let cat = self.catalog.read();
715 let mut keys: Vec<WriteKey> = Vec::new();
716 for (table_id, staged) in &staging {
717 match staged {
718 Staged::Put(cells) => {
719 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
720 for col in &entry.schema.columns {
721 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
722 if let Some((_, val)) =
723 cells.iter().find(|(id, _)| *id == col.id)
724 {
725 let mut h = DefaultHasher::new();
726 val.encode_key().hash(&mut h);
727 keys.push(WriteKey::Unique {
728 table_id: *table_id,
729 index_id: 0,
730 key_hash: h.finish(),
731 });
732 }
733 }
734 }
735 for uc in &entry.schema.constraints.uniques {
742 if let Some(key_bytes) = crate::constraint::encode_composite_key(
743 &uc.columns,
744 &cells.iter().cloned().collect(),
745 ) {
746 let mut h = DefaultHasher::new();
747 key_bytes.hash(&mut h);
748 keys.push(WriteKey::Unique {
749 table_id: *table_id,
750 index_id: uc.id | 0x8000,
751 key_hash: h.finish(),
752 });
753 }
754 }
755 }
756 }
757 Staged::Delete(rid) => keys.push(WriteKey::Row {
758 table_id: *table_id,
759 row_id: rid.0,
760 }),
761 Staged::Truncate => keys.push(WriteKey::Table {
762 table_id: *table_id,
763 }),
764 }
765 }
766 keys
767 };
768
769 let min_active = self.active_txns.min_read_epoch();
771 if min_active < u64::MAX {
772 self.conflicts.prune_below(Epoch(min_active));
773 }
774
775 if self.conflicts.conflicts(&write_keys, read_epoch) {
779 return Err(MongrelError::Conflict(
780 "write-write conflict (pre-validate, first-committer-wins)".into(),
781 ));
782 }
783 let pre_validate_version = self.conflicts.version();
784
785 {
790 let tables = self.tables.read();
791 for (table_id, staged) in &mut staging {
792 if let Staged::Put(cells) = staged {
793 if let Some(handle) = tables.get(table_id) {
794 let mut t = handle.lock();
795 t.fill_auto_inc(cells)?;
796 }
797 }
798 }
799 }
800
801 self.validate_constraints(&mut staging, read_epoch)?;
808
809 let mut spilled: Vec<SpilledRun> = Vec::new();
813 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
814 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
818 {
819 let mut table_bytes: HashMap<u64, usize> = HashMap::new();
820 for (table_id, staged) in &staging {
821 if let Staged::Put(cells) = staged {
822 *table_bytes.entry(*table_id).or_default() += cells.len() * 16;
823 }
824 }
825 let tables = self.tables.read();
826 for (&table_id, &bytes) in &table_bytes {
827 if bytes as u64
828 <= self
829 .spill_threshold
830 .load(std::sync::atomic::Ordering::Relaxed)
831 {
832 continue;
833 }
834 let Some(handle) = tables.get(&table_id) else {
835 continue;
836 };
837 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
838 let mut t = handle.lock();
839 let tdir = t.table_dir().to_path_buf();
840 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
841 std::fs::create_dir_all(&txn_dir)?;
842 let run_id = t.alloc_run_id() as u128;
843 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
844
845 let mut rows: Vec<Row> = Vec::new();
846 for (tid, staged) in &staging {
847 if *tid != table_id {
848 continue;
849 }
850 if let Staged::Put(cells) = staged {
851 t.validate_cells_not_null(cells)?;
852 let row_id = t.alloc_row_id();
853 let mut row = Row::new(row_id, Epoch(0));
854 for (c, v) in cells {
855 row.columns.insert(*c, v.clone());
856 }
857 rows.push(row);
858 }
859 }
860 let schema = t.schema_ref().clone();
861 let kek = t.kek_ref().cloned();
862 let specs = t.indexable_column_specs();
863 drop(t);
864
865 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
866 .uniform_epoch(true);
867 if let Some(ref kek) = kek {
868 writer = writer.with_encryption(kek.as_ref(), specs);
869 }
870 let header = writer.write(&pending_path, &rows)?;
871 let row_count = header.row_count;
872 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
873 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
874
875 spilled.push(SpilledRun {
876 table_id,
877 run_id,
878 pending_path,
879 rows,
880 row_count,
881 min_rid,
882 max_rid,
883 });
884 spilled_tables.insert(table_id);
885 }
886 }
887
888 if spill_guard.is_some() {
890 if let Some(hook) = self.spill_hook.lock().as_ref() {
891 hook();
892 }
893 }
894
895 let mut prebuilt: Vec<Option<Row>> = Vec::with_capacity(staging.len());
906 {
907 let tables = self.tables.read();
908 for (table_id, staged) in &staging {
909 match staged {
910 Staged::Put(cells) if !spilled_tables.contains(table_id) => {
911 let handle = tables.get(table_id).ok_or_else(|| {
912 MongrelError::NotFound(format!("table {table_id} not mounted"))
913 })?;
914 let mut t = handle.lock();
915 t.validate_cells_not_null(cells)?;
916 let row_id = t.alloc_row_id();
917 drop(t);
918 let mut row = Row::new(row_id, Epoch(0));
919 for (c, v) in cells {
920 row.columns.insert(*c, v.clone());
921 }
922 prebuilt.push(Some(row));
923 }
924 Staged::Put(_) | Staged::Delete(_) | Staged::Truncate => prebuilt.push(None),
925 }
926 }
927 }
928
929 let added_runs: Vec<crate::wal::AddedRun> = spilled
931 .iter()
932 .map(|s| crate::wal::AddedRun {
933 table_id: s.table_id,
934 run_id: s.run_id,
935 row_count: s.row_count,
936 level: 0,
937 min_row_id: s.min_rid,
938 max_row_id: s.max_rid,
939 content_hash: [0u8; 32],
940 })
941 .collect();
942 let (new_epoch, applies, commit_seq) = {
943 let mut wal = self.shared_wal.lock();
944
945 if self.conflicts.version() != pre_validate_version
950 && self.conflicts.conflicts(&write_keys, read_epoch)
951 {
952 drop(wal);
956 for s in &spilled {
957 if let Some(parent) = s.pending_path.parent() {
958 let _ = std::fs::remove_dir_all(parent);
959 }
960 }
961 return Err(MongrelError::Conflict(
962 "write-write conflict (sequencer delta re-check)".into(),
963 ));
964 }
965
966 let new_epoch = self.epoch.bump_assigned();
967 let mut applies: Vec<(u64, Vec<StagedOp>)> = Vec::new();
968
969 for (idx, (table_id, staged)) in staging.iter().enumerate() {
970 if spilled_tables.contains(table_id) && matches!(staged, Staged::Put(_)) {
973 continue;
974 }
975 let mut ops = Vec::new();
976 match staged {
977 Staged::Put(_) => {
978 let mut row = prebuilt[idx].take().expect("prebuilt put row");
980 row.committed_epoch = new_epoch;
981 let payload = bincode::serialize(&vec![row.clone()])
982 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
983 wal.append(
984 txn_id,
985 *table_id,
986 Op::Put {
987 table_id: *table_id,
988 rows: payload,
989 },
990 )?;
991 ops.push(StagedOp::Put(row));
992 }
993 Staged::Delete(rid) => {
994 wal.append(
995 txn_id,
996 *table_id,
997 Op::Delete {
998 table_id: *table_id,
999 row_ids: vec![*rid],
1000 },
1001 )?;
1002 ops.push(StagedOp::Delete(*rid));
1003 }
1004 Staged::Truncate => {
1005 wal.append(
1006 txn_id,
1007 *table_id,
1008 Op::TruncateTable {
1009 table_id: *table_id,
1010 },
1011 )?;
1012 ops.push(StagedOp::Truncate);
1013 }
1014 }
1015 applies.push((*table_id, ops));
1016 }
1017
1018 let commit_seq = wal.append_commit(txn_id, new_epoch, &added_runs)?;
1019
1020 self.conflicts.record(&write_keys, new_epoch);
1025 (new_epoch, applies, commit_seq)
1026 };
1027
1028 self.group
1030 .await_durable(&self.shared_wal, commit_seq)
1031 .inspect_err(|_| {
1032 self.poisoned.store(true, Ordering::Relaxed);
1033 })?;
1034
1035 {
1037 let tables = self.tables.read();
1038 for s in &spilled {
1040 if let Some(handle) = tables.get(&s.table_id) {
1041 let mut t = handle.lock();
1042 let dest = t.run_path(s.run_id as u64);
1043 std::fs::rename(&s.pending_path, &dest)?;
1044 if let Some(parent) = s.pending_path.parent() {
1046 let _ = std::fs::remove_dir_all(parent);
1047 }
1048 t.link_run(crate::manifest::RunRef {
1049 run_id: s.run_id,
1050 level: 0,
1051 epoch_created: new_epoch.0,
1052 row_count: s.row_count,
1053 });
1054 t.apply_run_metadata(&s.rows)?;
1059 t.invalidate_pending_cache();
1060 t.persist_manifest(new_epoch)?;
1061 }
1062 }
1063 for (table_id, ops) in applies {
1065 if let Some(handle) = tables.get(&table_id) {
1066 let mut t = handle.lock();
1067 for op in ops {
1068 match op {
1069 StagedOp::Put(row) => t.apply_put_rows(vec![row])?,
1070 StagedOp::Delete(rid) => t.apply_delete(rid, new_epoch),
1071 StagedOp::Truncate => t.apply_truncate(new_epoch)?,
1072 }
1073 }
1074 t.invalidate_pending_cache();
1075 t.persist_manifest(new_epoch)?;
1076 }
1077 }
1078 }
1079
1080 self.advance_visible(new_epoch);
1081 Ok(new_epoch)
1082 }
1083
1084 fn advance_visible(&self, published: Epoch) {
1090 self.epoch.publish_in_order(published);
1091 }
1092
1093 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
1096 let e = self.epoch.visible();
1097 let g = self.snapshots.register(e);
1098 (Snapshot::at(e), g)
1099 }
1100
1101 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
1104 let e = self.epoch.visible();
1105 let g = self.snapshots.register_owned(e);
1106 (Snapshot::at(e), g)
1107 }
1108
1109 pub fn table_names(&self) -> Vec<String> {
1111 self.catalog
1112 .read()
1113 .tables
1114 .iter()
1115 .filter(|t| matches!(t.state, TableState::Live))
1116 .map(|t| t.name.clone())
1117 .collect()
1118 }
1119
1120 pub fn close(&self) -> Result<()> {
1126 for name in self.table_names() {
1127 if let Ok(handle) = self.table(&name) {
1128 if let Err(e) = handle.lock().close() {
1129 eprintln!("[close] flush failed for {name}: {e}");
1130 }
1131 }
1132 }
1133 Ok(())
1134 }
1135
1136 pub fn compact(&self) -> Result<(usize, usize)> {
1142 let mut compacted = 0;
1143 let mut skipped = 0;
1144 for name in self.table_names() {
1145 let Ok(handle) = self.table(&name) else {
1146 continue;
1147 };
1148 {
1149 let mut t = handle.lock();
1150 let before = t.run_count();
1151 if before < 2 {
1152 skipped += 1;
1153 continue;
1154 }
1155 match t.compact() {
1156 Ok(()) => {
1157 let after = t.run_count();
1158 compacted += 1;
1159 eprintln!("[compact] {name}: {before} -> {after} runs");
1160 }
1161 Err(e) => {
1162 eprintln!("[compact] {name}: compaction failed: {e}");
1163 skipped += 1;
1164 }
1165 }
1166 }
1167 }
1168 Ok((compacted, skipped))
1169 }
1170
1171 pub fn compact_table(&self, name: &str) -> Result<bool> {
1174 let handle = self.table(name)?;
1175 let mut t = handle.lock();
1176 let before = t.run_count();
1177 if before < 2 {
1178 return Ok(false);
1179 }
1180 t.compact()?;
1181 Ok(t.run_count() < before)
1182 }
1183
1184 pub fn table(&self, name: &str) -> Result<TableHandle> {
1186 let cat = self.catalog.read();
1187 let entry = cat
1188 .live(name)
1189 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
1190 let id = entry.table_id;
1191 drop(cat);
1192 self.tables
1193 .read()
1194 .get(&id)
1195 .cloned()
1196 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
1197 }
1198
1199 fn table_by_id(&self, id: u64) -> Result<TableHandle> {
1202 self.tables
1203 .read()
1204 .get(&id)
1205 .cloned()
1206 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
1207 }
1208
1209 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
1215 use crate::wal::DdlOp;
1216 use std::sync::atomic::Ordering;
1217
1218 if self.poisoned.load(Ordering::Relaxed) {
1219 return Err(MongrelError::Other(
1220 "database poisoned by fsync error".into(),
1221 ));
1222 }
1223
1224 let _g = self.ddl_lock.lock();
1225 {
1226 let cat = self.catalog.read();
1227 if cat.live(name).is_some() {
1228 return Err(MongrelError::InvalidArgument(format!(
1229 "table {name:?} already exists"
1230 )));
1231 }
1232 }
1233
1234 let commit_lock = Arc::clone(&self.commit_lock);
1237 let _c = commit_lock.lock();
1238 let table_id = {
1239 let mut cat = self.catalog.write();
1240 let id = cat.next_table_id;
1241 cat.next_table_id += 1;
1242 id
1243 };
1244 let epoch = self.epoch.bump_assigned();
1245 let txn_id = self.alloc_txn_id();
1246
1247 let mut schema = schema;
1251 schema.schema_id = table_id;
1252 schema.validate_auto_increment()?;
1259
1260 let schema_json = DdlOp::encode_schema(&schema)?;
1263 let commit_seq = {
1264 let mut wal = self.shared_wal.lock();
1265 wal.append(
1266 txn_id,
1267 table_id,
1268 crate::wal::Op::Ddl(DdlOp::CreateTable {
1269 table_id,
1270 name: name.to_string(),
1271 schema_json,
1272 }),
1273 )?;
1274 wal.append_commit(txn_id, epoch, &[])?
1275 };
1276 self.group
1277 .await_durable(&self.shared_wal, commit_seq)
1278 .inspect_err(|_| {
1279 self.poisoned.store(true, Ordering::Relaxed);
1280 })?;
1281
1282 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
1284 std::fs::create_dir_all(&tdir)?;
1285 let ctx = SharedCtx {
1286 epoch: Arc::clone(&self.epoch),
1287 page_cache: Arc::clone(&self.page_cache),
1288 decoded_cache: Arc::clone(&self.decoded_cache),
1289 snapshots: Arc::clone(&self.snapshots),
1290 kek: self.kek.clone(),
1291 commit_lock: Arc::clone(&self.commit_lock),
1292 shared: Some(crate::engine::SharedWalCtx {
1293 wal: Arc::clone(&self.shared_wal),
1294 group: Arc::clone(&self.group),
1295 poisoned: Arc::clone(&self.poisoned),
1296 txn_ids: Arc::clone(&self.next_txn_id),
1297 }),
1298 };
1299 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
1300
1301 {
1304 let mut cat = self.catalog.write();
1305 cat.tables.push(CatalogEntry {
1306 table_id,
1307 name: name.to_string(),
1308 schema,
1309 state: TableState::Live,
1310 created_epoch: epoch.0,
1311 });
1312 }
1313 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1314 self.tables
1315 .write()
1316 .insert(table_id, Arc::new(Mutex::new(table)));
1317
1318 self.advance_visible(epoch);
1319 Ok(table_id)
1320 }
1321
1322 pub fn drop_table(&self, name: &str) -> Result<()> {
1324 use crate::wal::DdlOp;
1325 use std::sync::atomic::Ordering;
1326
1327 if self.poisoned.load(Ordering::Relaxed) {
1328 return Err(MongrelError::Other(
1329 "database poisoned by fsync error".into(),
1330 ));
1331 }
1332
1333 let _g = self.ddl_lock.lock();
1334 let table_id = {
1335 let cat = self.catalog.read();
1336 cat.live(name)
1337 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
1338 .table_id
1339 };
1340
1341 let commit_lock = Arc::clone(&self.commit_lock);
1342 let _c = commit_lock.lock();
1343 let epoch = self.epoch.bump_assigned();
1344 let txn_id = self.alloc_txn_id();
1345 let commit_seq = {
1346 let mut wal = self.shared_wal.lock();
1347 wal.append(
1348 txn_id,
1349 table_id,
1350 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
1351 )?;
1352 wal.append_commit(txn_id, epoch, &[])?
1353 };
1354 self.group
1355 .await_durable(&self.shared_wal, commit_seq)
1356 .inspect_err(|_| {
1357 self.poisoned.store(true, Ordering::Relaxed);
1358 })?;
1359
1360 {
1361 let mut cat = self.catalog.write();
1362 let entry = cat
1363 .tables
1364 .iter_mut()
1365 .find(|t| t.table_id == table_id)
1366 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
1367 entry.state = TableState::Dropped { at_epoch: epoch.0 };
1368 }
1369 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1370 self.tables.write().remove(&table_id);
1371
1372 self.advance_visible(epoch);
1373 Ok(())
1374 }
1375
1376 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
1385 use crate::wal::DdlOp;
1386 use std::sync::atomic::Ordering;
1387
1388 if self.poisoned.load(Ordering::Relaxed) {
1389 return Err(MongrelError::Other(
1390 "database poisoned by fsync error".into(),
1391 ));
1392 }
1393
1394 if name == new_name {
1397 return Ok(());
1398 }
1399 if new_name.is_empty() {
1400 return Err(MongrelError::InvalidArgument(
1401 "rename_table: new name must not be empty".into(),
1402 ));
1403 }
1404
1405 let _g = self.ddl_lock.lock();
1406 let table_id = {
1407 let cat = self.catalog.read();
1408 let src = cat
1409 .live(name)
1410 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
1411 if cat.live(new_name).is_some() {
1415 return Err(MongrelError::InvalidArgument(format!(
1416 "rename_table: a table named {new_name:?} already exists"
1417 )));
1418 }
1419 src.table_id
1420 };
1421
1422 let commit_lock = Arc::clone(&self.commit_lock);
1423 let _c = commit_lock.lock();
1424 let epoch = self.epoch.bump_assigned();
1425 let txn_id = self.alloc_txn_id();
1426 let commit_seq = {
1427 let mut wal = self.shared_wal.lock();
1428 wal.append(
1429 txn_id,
1430 table_id,
1431 crate::wal::Op::Ddl(DdlOp::RenameTable {
1432 table_id,
1433 new_name: new_name.to_string(),
1434 }),
1435 )?;
1436 wal.append_commit(txn_id, epoch, &[])?
1437 };
1438 self.group
1439 .await_durable(&self.shared_wal, commit_seq)
1440 .inspect_err(|_| {
1441 self.poisoned.store(true, Ordering::Relaxed);
1442 })?;
1443
1444 {
1445 let mut cat = self.catalog.write();
1446 let entry = cat
1447 .tables
1448 .iter_mut()
1449 .find(|t| t.table_id == table_id)
1450 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
1451 entry.name = new_name.to_string();
1452 }
1453 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1454 self.advance_visible(epoch);
1457 Ok(())
1458 }
1459
1460 pub fn alter_column(
1461 &self,
1462 table_name: &str,
1463 column_name: &str,
1464 change: AlterColumn,
1465 ) -> Result<ColumnDef> {
1466 use crate::wal::DdlOp;
1467 use std::sync::atomic::Ordering;
1468
1469 if self.poisoned.load(Ordering::Relaxed) {
1470 return Err(MongrelError::Other(
1471 "database poisoned by fsync error".into(),
1472 ));
1473 }
1474
1475 let _g = self.ddl_lock.lock();
1476 let table_id = {
1477 let cat = self.catalog.read();
1478 cat.live(table_name)
1479 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
1480 .table_id
1481 };
1482 let handle =
1483 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
1484 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
1485 })?;
1486 let mut table = handle.lock();
1487 let column = table.prepare_alter_column(column_name, &change)?;
1488 if table
1489 .schema()
1490 .columns
1491 .iter()
1492 .find(|c| c.id == column.id)
1493 .is_some_and(|c| c == &column)
1494 {
1495 return Ok(column);
1496 }
1497
1498 let commit_lock = Arc::clone(&self.commit_lock);
1499 let _c = commit_lock.lock();
1500 let epoch = self.epoch.bump_assigned();
1501 let txn_id = self.alloc_txn_id();
1502 let column_json = DdlOp::encode_column(&column)?;
1503 let commit_seq = {
1504 let mut wal = self.shared_wal.lock();
1505 wal.append(
1506 txn_id,
1507 table_id,
1508 crate::wal::Op::Ddl(DdlOp::AlterTable {
1509 table_id,
1510 column_json,
1511 }),
1512 )?;
1513 wal.append_commit(txn_id, epoch, &[])?
1514 };
1515 self.group
1516 .await_durable(&self.shared_wal, commit_seq)
1517 .inspect_err(|_| {
1518 self.poisoned.store(true, Ordering::Relaxed);
1519 })?;
1520
1521 table.apply_altered_column(column.clone())?;
1522 let schema = table.schema().clone();
1523 drop(table);
1524
1525 {
1526 let mut cat = self.catalog.write();
1527 let entry = cat
1528 .tables
1529 .iter_mut()
1530 .find(|t| t.table_id == table_id)
1531 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
1532 entry.schema = schema;
1533 }
1534 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1535
1536 self.advance_visible(epoch);
1537 Ok(column)
1538 }
1539
1540 pub fn gc(&self) -> Result<usize> {
1546 let min_active = self.snapshots.min_active(self.epoch.visible()).0;
1547 let mut reclaimed = 0;
1548
1549 let cat = self.catalog.read();
1551 for entry in &cat.tables {
1552 if let TableState::Dropped { at_epoch } = entry.state {
1553 if at_epoch <= min_active {
1554 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
1555 if tdir.exists() {
1556 std::fs::remove_dir_all(&tdir)?;
1557 reclaimed += 1;
1558 }
1559 }
1560 }
1561 }
1562 drop(cat);
1563
1564 let cat = self.catalog.read();
1569 for entry in &cat.tables {
1570 if !matches!(entry.state, TableState::Live) {
1571 continue;
1572 }
1573 let txn_dir = self
1574 .root
1575 .join(TABLES_DIR)
1576 .join(entry.table_id.to_string())
1577 .join("_txn");
1578 if !txn_dir.exists() {
1579 continue;
1580 }
1581 for sub in std::fs::read_dir(&txn_dir)? {
1582 let sub = sub?;
1583 let name = sub.file_name();
1584 let Some(name) = name.to_str() else { continue };
1585 let is_active = name
1587 .parse::<u64>()
1588 .map(|id| self.active_spills.is_active(id))
1589 .unwrap_or(false);
1590 if is_active {
1591 continue;
1592 }
1593 std::fs::remove_dir_all(sub.path())?;
1594 reclaimed += 1;
1595 }
1596 }
1597 drop(cat);
1598
1599 let tables = self.tables.read();
1603 for handle in tables.values() {
1604 reclaimed += handle.lock().reap_retiring(Epoch(min_active))?;
1605 }
1606
1607 let all_durable = self.active_spills.is_idle()
1615 && tables.values().all(|h| {
1616 let g = h.lock();
1617 g.memtable_len() == 0 && g.mutable_run_len() == 0
1618 });
1619 drop(tables);
1620 if all_durable {
1621 reclaimed += self.shared_wal.lock().gc_segments(u64::MAX)?;
1622 }
1623
1624 Ok(reclaimed)
1625 }
1626 fn alloc_txn_id(&self) -> u64 {
1627 let mut g = self.next_txn_id.lock();
1628 let id = *g;
1629 *g = g.wrapping_add(1);
1630 id
1631 }
1632
1633 pub fn set_spill_threshold(&self, bytes: u64) {
1637 self.spill_threshold
1638 .store(bytes, std::sync::atomic::Ordering::Relaxed);
1639 }
1640
1641 #[doc(hidden)]
1645 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
1646 *self.spill_hook.lock() = Some(Box::new(f));
1647 }
1648
1649 #[doc(hidden)]
1653 pub fn __wal_group_sync_count(&self) -> u64 {
1654 self.shared_wal.lock().group_sync_count()
1655 }
1656
1657 #[doc(hidden)]
1660 pub fn __poison(&self) {
1661 self.poisoned
1662 .store(true, std::sync::atomic::Ordering::Relaxed);
1663 }
1664
1665 pub fn check(&self) -> Vec<CheckIssue> {
1678 let mut issues = Vec::new();
1679 let cat = self.catalog.read();
1680 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
1681 for entry in &cat.tables {
1682 if !matches!(entry.state, TableState::Live) {
1683 continue;
1684 }
1685 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
1686 let mut err = |sev: &str, desc: String| {
1687 issues.push(CheckIssue {
1688 table_id: entry.table_id,
1689 table_name: entry.name.clone(),
1690 severity: sev.into(),
1691 description: desc,
1692 });
1693 };
1694 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
1695 Ok(m) => m,
1696 Err(e) => {
1697 err("error", format!("manifest read failed: {e}"));
1698 continue;
1699 }
1700 };
1701 if m.flushed_epoch > m.current_epoch {
1702 err(
1703 "error",
1704 format!(
1705 "flushed_epoch {} exceeds current_epoch {} (impossible)",
1706 m.flushed_epoch, m.current_epoch
1707 ),
1708 );
1709 }
1710
1711 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
1712 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
1713 for rr in &m.runs {
1714 referenced.insert(rr.run_id);
1715 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
1716 if !run_path.exists() {
1717 err("error", format!("missing run file: r-{}.sr", rr.run_id));
1718 continue;
1719 }
1720 match crate::sorted_run::RunReader::open(
1721 &run_path,
1722 entry.schema.clone(),
1723 self.kek.clone(),
1724 ) {
1725 Ok(reader) => {
1726 if reader.row_count() as u64 != rr.row_count {
1727 err(
1728 "error",
1729 format!(
1730 "run r-{} row count mismatch: manifest {} vs run {}",
1731 rr.run_id,
1732 rr.row_count,
1733 reader.row_count()
1734 ),
1735 );
1736 }
1737 }
1738 Err(e) => {
1739 err(
1740 "error",
1741 format!("run r-{} integrity check failed: {e}", rr.run_id),
1742 );
1743 }
1744 }
1745 }
1746
1747 for r in &m.retiring {
1751 referenced.insert(r.run_id);
1752 }
1753
1754 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
1756 for ent in rd.flatten() {
1757 let p = ent.path();
1758 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
1759 continue;
1760 }
1761 let run_id = p
1762 .file_stem()
1763 .and_then(|s| s.to_str())
1764 .and_then(|s| s.strip_prefix("r-"))
1765 .and_then(|s| s.parse::<u128>().ok());
1766 if let Some(id) = run_id {
1767 if !referenced.contains(&id) {
1768 err(
1769 "warning",
1770 format!("orphan run file r-{id}.sr not referenced by the manifest"),
1771 );
1772 }
1773 }
1774 }
1775 }
1776 }
1777
1778 for (seg, msg) in self.shared_wal.lock().verify_segments() {
1785 issues.push(CheckIssue {
1786 table_id: WAL_TABLE_ID,
1787 table_name: "<wal>".into(),
1788 severity: "error".into(),
1789 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
1790 });
1791 }
1792 issues
1793 }
1794
1795 pub fn doctor(&self) -> Result<Vec<u64>> {
1799 let _ddl = self.ddl_lock.lock();
1802 let issues = self.check();
1803 let bad_tables: std::collections::HashSet<u64> = issues
1808 .iter()
1809 .filter(|i| i.severity == "error" && i.table_id != WAL_TABLE_ID)
1810 .map(|i| i.table_id)
1811 .collect();
1812 if bad_tables.is_empty() {
1813 return Ok(Vec::new());
1814 }
1815
1816 let qdir = self.root.join("_quarantine");
1817 std::fs::create_dir_all(&qdir)?;
1818 let mut quarantined = Vec::new();
1819 for &table_id in &bad_tables {
1820 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
1821 if tdir.exists() {
1822 let dest = qdir.join(table_id.to_string());
1823 std::fs::rename(&tdir, &dest)?;
1824 }
1825 {
1826 let mut cat = self.catalog.write();
1827 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
1828 entry.state = TableState::Dropped {
1829 at_epoch: self.epoch.visible().0,
1830 };
1831 }
1832 }
1833 self.tables.write().remove(&table_id);
1835 quarantined.push(table_id);
1836 }
1837 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1838 Ok(quarantined)
1839 }
1840
1841 #[allow(dead_code)]
1843 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
1844 self.kek.as_ref()
1845 }
1846
1847 #[allow(dead_code)]
1849 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
1850 &self.epoch
1851 }
1852
1853 #[allow(dead_code)]
1855 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
1856 &self.snapshots
1857 }
1858}
1859
1860fn recover_shared_wal(
1869 root: &Path,
1870 tables: &HashMap<u64, TableHandle>,
1871 epoch: &EpochAuthority,
1872 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
1873) -> Result<()> {
1874 use crate::memtable::Row;
1875 use crate::rowid::RowId;
1876 use crate::wal::{Op, SharedWal};
1877
1878 let records = SharedWal::replay_with_dek(root, wal_dek)?;
1879
1880 let mut committed: HashMap<u64, u64> = HashMap::new();
1882 let mut spilled_to_link: Vec<(
1883 u64, u64, Vec<crate::wal::AddedRun>,
1886 )> = Vec::new();
1887 for r in &records {
1888 if let Op::TxnCommit {
1889 epoch: ce,
1890 ref added_runs,
1891 } = r.op
1892 {
1893 committed.insert(r.txn_id, ce);
1894 if !added_runs.is_empty() {
1895 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
1896 }
1897 }
1898 }
1899
1900 type TableStage = (Vec<Row>, Vec<(RowId, Epoch)>, Option<Epoch>, Epoch);
1902 let mut stage: HashMap<u64, TableStage> = HashMap::new();
1903 let mut max_epoch = epoch.visible().0;
1904 for r in records {
1905 let Some(&ce) = committed.get(&r.txn_id) else {
1906 continue; };
1908 let commit_epoch = Epoch(ce);
1909 max_epoch = max_epoch.max(ce);
1910 match r.op {
1911 Op::Put { table_id, rows } => {
1912 let skip = tables
1914 .get(&table_id)
1915 .map(|h| h.lock().flushed_epoch() >= ce)
1916 .unwrap_or(true);
1917 if skip {
1918 continue;
1919 }
1920 let rows: Vec<Row> = match bincode::deserialize(&rows) {
1921 Ok(v) => v,
1922 Err(_) => continue,
1923 };
1924 let rows: Vec<Row> = rows
1927 .into_iter()
1928 .map(|mut row| {
1929 row.committed_epoch = commit_epoch;
1930 row
1931 })
1932 .collect();
1933 let entry = stage
1934 .entry(table_id)
1935 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
1936 entry.0.extend(rows);
1937 entry.3 = commit_epoch;
1938 }
1939 Op::Delete { table_id, row_ids } => {
1940 let skip = tables
1941 .get(&table_id)
1942 .map(|h| h.lock().flushed_epoch() >= ce)
1943 .unwrap_or(true);
1944 if skip {
1945 continue;
1946 }
1947 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
1948 let entry = stage
1949 .entry(table_id)
1950 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
1951 entry.1.extend(dels);
1952 entry.3 = commit_epoch;
1953 }
1954 Op::TruncateTable { table_id } => {
1955 let skip = tables
1956 .get(&table_id)
1957 .map(|h| h.lock().flushed_epoch() >= ce)
1958 .unwrap_or(true);
1959 if skip {
1960 continue;
1961 }
1962 stage.insert(
1963 table_id,
1964 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
1965 );
1966 }
1967 _ => {}
1968 }
1969 }
1970 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
1971 let Some(handle) = tables.get(&table_id) else {
1972 continue;
1973 };
1974 let mut t = handle.lock();
1975 if let Some(epoch) = truncate_epoch {
1976 t.apply_truncate(epoch)?;
1977 }
1978 t.recover_apply(rows, deletes)?;
1979 if truncate_epoch.is_some() {
1980 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
1981 t.live_count = rows.len() as u64;
1982 t.persist_manifest(table_epoch)?;
1983 }
1984 }
1985
1986 for (txn_id, ce, added_runs) in &spilled_to_link {
1990 for ar in added_runs {
1991 let Some(handle) = tables.get(&ar.table_id) else {
1992 continue;
1993 };
1994 let mut t = handle.lock();
1995 let dest = t.run_path(ar.run_id as u64);
1996 if !dest.exists() {
1997 let pending = root
1998 .join(TABLES_DIR)
1999 .join(ar.table_id.to_string())
2000 .join("_txn")
2001 .join(txn_id.to_string())
2002 .join(format!("r-{}.sr", ar.run_id));
2003 if pending.exists() {
2004 if let Some(parent) = pending.parent() {
2005 std::fs::rename(&pending, &dest)?;
2006 let _ = std::fs::remove_dir_all(parent);
2007 }
2008 }
2009 }
2010 if t.run_path(ar.run_id as u64).exists() {
2016 t.recover_spilled_run(crate::manifest::RunRef {
2017 run_id: ar.run_id,
2018 level: ar.level,
2019 epoch_created: *ce,
2020 row_count: ar.row_count,
2021 });
2022 }
2023 }
2024 }
2025
2026 epoch.advance_recovered(Epoch(max_epoch));
2027 Ok(())
2028}
2029
2030fn recover_ddl_from_wal(
2036 root: &Path,
2037 cat: &mut Catalog,
2038 meta_dek: Option<&[u8; META_DEK_LEN]>,
2039 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
2040) -> Result<()> {
2041 use crate::wal::{DdlOp, Op, SharedWal};
2042
2043 let records = match SharedWal::replay_with_dek(root, wal_dek) {
2044 Ok(r) => r,
2045 Err(_) => return Ok(()),
2046 };
2047
2048 let mut committed: HashMap<u64, u64> = HashMap::new();
2049 for r in &records {
2050 if let Op::TxnCommit { epoch: ce, .. } = r.op {
2051 committed.insert(r.txn_id, ce);
2052 }
2053 }
2054
2055 let mut changed = false;
2056 for r in records {
2057 let Some(&ce) = committed.get(&r.txn_id) else {
2058 continue;
2059 };
2060 match r.op {
2061 Op::Ddl(DdlOp::CreateTable {
2062 table_id,
2063 ref name,
2064 ref schema_json,
2065 }) => {
2066 if cat.tables.iter().any(|t| t.table_id == table_id) {
2067 continue;
2068 }
2069 let schema = DdlOp::decode_schema(schema_json)?;
2070 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
2071 if !tdir.exists() {
2072 std::fs::create_dir_all(tdir.join(crate::engine::WAL_DIR))?;
2073 std::fs::create_dir_all(tdir.join(crate::engine::RUNS_DIR))?;
2074 crate::engine::write_schema(&tdir, &schema)?;
2075 let mut m = crate::manifest::Manifest::new(table_id, schema.schema_id);
2081 crate::manifest::write_atomic(&tdir, &mut m, meta_dek)?;
2082 }
2083 cat.tables.push(CatalogEntry {
2084 table_id,
2085 name: name.clone(),
2086 schema,
2087 state: TableState::Live,
2088 created_epoch: ce,
2089 });
2090 cat.next_table_id = cat.next_table_id.max(table_id + 1);
2091 changed = true;
2092 }
2093 Op::Ddl(DdlOp::DropTable { table_id }) => {
2094 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
2095 if matches!(entry.state, TableState::Live) {
2096 entry.state = TableState::Dropped { at_epoch: ce };
2097 changed = true;
2098 }
2099 }
2100 }
2101 Op::Ddl(DdlOp::RenameTable {
2102 table_id,
2103 ref new_name,
2104 }) => {
2105 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
2106 if entry.name != *new_name {
2107 entry.name = new_name.clone();
2108 changed = true;
2109 }
2110 }
2111 }
2115 Op::Ddl(DdlOp::AlterTable {
2116 table_id,
2117 ref column_json,
2118 }) => {
2119 let column = DdlOp::decode_column(column_json)?;
2120 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
2121 if apply_recovered_column_def(&mut entry.schema, column) {
2122 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
2123 if tdir.exists() {
2124 crate::engine::write_schema(&tdir, &entry.schema)?;
2125 }
2126 changed = true;
2127 }
2128 }
2129 }
2130 _ => {}
2131 }
2132 }
2133
2134 if changed {
2135 catalog::write_atomic(root, cat, meta_dek)?;
2136 }
2137 Ok(())
2138}
2139
2140fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> bool {
2141 match schema.columns.iter_mut().find(|c| c.id == column.id) {
2142 Some(existing) if *existing == column => false,
2143 Some(existing) => {
2144 *existing = column;
2145 schema.schema_id = schema.schema_id.saturating_add(1);
2146 true
2147 }
2148 None => {
2149 schema.columns.push(column);
2150 schema.schema_id = schema.schema_id.saturating_add(1);
2151 true
2152 }
2153 }
2154}
2155
2156fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
2161 for entry in &cat.tables {
2162 let txn_dir = root
2163 .join(TABLES_DIR)
2164 .join(entry.table_id.to_string())
2165 .join("_txn");
2166 if txn_dir.exists() {
2167 let _ = std::fs::remove_dir_all(&txn_dir);
2168 }
2169 }
2170}