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 table_id(&self, name: &str) -> Result<u64> {
336 let cat = self.catalog.read();
337 cat.live(name)
338 .map(|e| e.table_id)
339 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
340 }
341
342 pub fn begin(&self) -> crate::txn::Transaction<'_> {
344 let txn_id = self.alloc_txn_id();
345 let read = Snapshot::at(self.epoch.visible());
346 crate::txn::Transaction::new(self, txn_id, read)
347 }
348
349 pub fn transaction<T>(
351 &self,
352 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
353 ) -> Result<T> {
354 let mut tx = self.begin();
355 match f(&mut tx) {
356 Ok(out) => {
357 tx.commit()?;
358 Ok(out)
359 }
360 Err(e) => {
361 tx.rollback();
362 Err(e)
363 }
364 }
365 }
366
367 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
370 self.active_txns.register(epoch)
371 }
372
373 fn validate_constraints(
386 &self,
387 staging: &[(u64, crate::txn::Staged)],
388 read_epoch: Epoch,
389 ) -> Result<()> {
390 use crate::constraint::{encode_composite_key, validate_checks};
391 use crate::memtable::Row;
392 use crate::txn::Staged;
393 use std::collections::HashSet;
394
395 let snapshot = Snapshot::at(read_epoch);
396 let cat = self.catalog.read();
397
398 let live: Vec<(u64, &str, &crate::schema::Schema)> = cat
400 .tables
401 .iter()
402 .filter(|e| matches!(e.state, TableState::Live))
403 .map(|e| (e.table_id, e.name.as_str(), &e.schema))
404 .collect();
405
406 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
408 if !any_constraints {
409 return Ok(());
410 }
411
412 let staged_deletes: HashSet<(u64, u64)> = staging
417 .iter()
418 .filter_map(|(t, op)| match op {
419 Staged::Delete(rid) => Some((*t, rid.0)),
420 _ => None,
421 })
422 .collect();
423
424 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
426 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
427 if let Some(r) = rows_cache.get(&table_id) {
428 return Ok(r.clone());
429 }
430 let handle = self.table_by_id(table_id)?;
431 let rows = handle.lock().visible_rows(snapshot)?;
432 rows_cache.insert(table_id, rows.clone());
433 Ok(rows)
434 };
435
436 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
438
439 for (table_id, op) in staging {
440 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id).copied()
441 else {
442 continue;
443 };
444 let cells_map: HashMap<u16, crate::memtable::Value>;
445 match op {
446 Staged::Put(cells) => {
447 cells_map = cells.iter().cloned().collect();
448
449 if !schema.constraints.checks.is_empty() {
451 validate_checks(&schema.constraints.checks, &cells_map)?;
452 }
453
454 for uc in &schema.constraints.uniques {
456 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
457 continue; };
459 let marker = (*table_id, uc.id, key.clone());
460 if !seen_unique.insert(marker) {
461 return Err(MongrelError::Conflict(format!(
462 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
463 uc.name
464 )));
465 }
466 let rows = load_rows(*table_id)?;
467 for r in &rows {
468 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
471 continue;
472 }
473 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
474 if theirs == key {
475 return Err(MongrelError::Conflict(format!(
476 "UNIQUE constraint '{}' on table '{tname}' violated",
477 uc.name
478 )));
479 }
480 }
481 }
482 }
483
484 for fk in &schema.constraints.foreign_keys {
486 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
487 continue; };
489 let Some(parent_id) = cat
490 .tables
491 .iter()
492 .find(|t| t.name == fk.ref_table)
493 .map(|t| t.table_id)
494 else {
495 return Err(MongrelError::InvalidArgument(format!(
496 "FOREIGN KEY '{}' references unknown table '{}'",
497 fk.name, fk.ref_table
498 )));
499 };
500 let parent_rows = load_rows(parent_id)?;
501 let mut found = false;
502 for r in &parent_rows {
503 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
504 if pkey == child_key {
505 found = true;
506 break;
507 }
508 }
509 }
510 if !found {
511 return Err(MongrelError::Conflict(format!(
512 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
513 fk.name, fk.ref_table
514 )));
515 }
516 }
517 }
518 Staged::Delete(rid) => {
519 let parent_handle = self.table_by_id(*table_id)?;
523 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
524 continue;
525 };
526 for (child_id, child_name, child_schema) in &live {
528 for fk in &child_schema.constraints.foreign_keys {
529 if fk.ref_table != tname {
530 continue;
531 }
532 let Some(parent_key) =
533 encode_composite_key(&fk.ref_columns, &parent_row.columns)
534 else {
535 continue;
536 };
537 let child_rows = load_rows(*child_id)?;
538 for r in &child_rows {
539 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
540 if ck == parent_key {
541 return Err(MongrelError::Conflict(format!(
542 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
543 fk.name
544 )));
545 }
546 }
547 }
548 }
549 }
550 }
551 Staged::Truncate => {
552 for (child_id, child_name, child_schema) in &live {
555 for fk in &child_schema.constraints.foreign_keys {
556 if fk.ref_table != tname {
557 continue;
558 }
559 let child_rows = load_rows(*child_id)?;
560 if child_rows
561 .iter()
562 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
563 {
564 return Err(MongrelError::Conflict(format!(
565 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
566 fk.name
567 )));
568 }
569 }
570 }
571 }
572 }
573 }
574 Ok(())
575 }
576
577 pub(crate) fn commit_transaction(
584 &self,
585 txn_id: u64,
586 read_epoch: Epoch,
587 mut staging: Vec<(u64, crate::txn::Staged)>,
588 ) -> Result<Epoch> {
589 use crate::memtable::Row;
590 use crate::txn::{Staged, StagedOp, WriteKey};
591 use crate::wal::Op;
592 use std::collections::hash_map::DefaultHasher;
593 use std::hash::{Hash, Hasher};
594 use std::sync::atomic::Ordering;
595
596 if self.poisoned.load(Ordering::Relaxed) {
597 return Err(MongrelError::Other(
598 "database poisoned by fsync error".into(),
599 ));
600 }
601
602 let write_keys = {
604 let cat = self.catalog.read();
605 let mut keys: Vec<WriteKey> = Vec::new();
606 for (table_id, staged) in &staging {
607 match staged {
608 Staged::Put(cells) => {
609 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
610 for col in &entry.schema.columns {
611 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
612 if let Some((_, val)) =
613 cells.iter().find(|(id, _)| *id == col.id)
614 {
615 let mut h = DefaultHasher::new();
616 val.encode_key().hash(&mut h);
617 keys.push(WriteKey::Unique {
618 table_id: *table_id,
619 index_id: 0,
620 key_hash: h.finish(),
621 });
622 }
623 }
624 }
625 for uc in &entry.schema.constraints.uniques {
632 if let Some(key_bytes) = crate::constraint::encode_composite_key(
633 &uc.columns,
634 &cells.iter().cloned().collect(),
635 ) {
636 let mut h = DefaultHasher::new();
637 key_bytes.hash(&mut h);
638 keys.push(WriteKey::Unique {
639 table_id: *table_id,
640 index_id: uc.id | 0x8000,
641 key_hash: h.finish(),
642 });
643 }
644 }
645 }
646 }
647 Staged::Delete(rid) => keys.push(WriteKey::Row {
648 table_id: *table_id,
649 row_id: rid.0,
650 }),
651 Staged::Truncate => keys.push(WriteKey::Table {
652 table_id: *table_id,
653 }),
654 }
655 }
656 keys
657 };
658
659 let min_active = self.active_txns.min_read_epoch();
661 if min_active < u64::MAX {
662 self.conflicts.prune_below(Epoch(min_active));
663 }
664
665 if self.conflicts.conflicts(&write_keys, read_epoch) {
669 return Err(MongrelError::Conflict(
670 "write-write conflict (pre-validate, first-committer-wins)".into(),
671 ));
672 }
673 let pre_validate_version = self.conflicts.version();
674
675 {
680 let tables = self.tables.read();
681 for (table_id, staged) in &mut staging {
682 if let Staged::Put(cells) = staged {
683 if let Some(handle) = tables.get(table_id) {
684 let mut t = handle.lock();
685 t.fill_auto_inc(cells)?;
686 }
687 }
688 }
689 }
690
691 self.validate_constraints(&staging, read_epoch)?;
698
699 let mut spilled: Vec<SpilledRun> = Vec::new();
703 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
704 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
708 {
709 let mut table_bytes: HashMap<u64, usize> = HashMap::new();
710 for (table_id, staged) in &staging {
711 if let Staged::Put(cells) = staged {
712 *table_bytes.entry(*table_id).or_default() += cells.len() * 16;
713 }
714 }
715 let tables = self.tables.read();
716 for (&table_id, &bytes) in &table_bytes {
717 if bytes as u64
718 <= self
719 .spill_threshold
720 .load(std::sync::atomic::Ordering::Relaxed)
721 {
722 continue;
723 }
724 let Some(handle) = tables.get(&table_id) else {
725 continue;
726 };
727 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
728 let mut t = handle.lock();
729 let tdir = t.table_dir().to_path_buf();
730 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
731 std::fs::create_dir_all(&txn_dir)?;
732 let run_id = t.alloc_run_id() as u128;
733 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
734
735 let mut rows: Vec<Row> = Vec::new();
736 for (tid, staged) in &staging {
737 if *tid != table_id {
738 continue;
739 }
740 if let Staged::Put(cells) = staged {
741 t.validate_cells_not_null(cells)?;
742 let row_id = t.alloc_row_id();
743 let mut row = Row::new(row_id, Epoch(0));
744 for (c, v) in cells {
745 row.columns.insert(*c, v.clone());
746 }
747 rows.push(row);
748 }
749 }
750 let schema = t.schema_ref().clone();
751 let kek = t.kek_ref().cloned();
752 let specs = t.indexable_column_specs();
753 drop(t);
754
755 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
756 .uniform_epoch(true);
757 if let Some(ref kek) = kek {
758 writer = writer.with_encryption(kek.as_ref(), specs);
759 }
760 let header = writer.write(&pending_path, &rows)?;
761 let row_count = header.row_count;
762 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
763 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
764
765 spilled.push(SpilledRun {
766 table_id,
767 run_id,
768 pending_path,
769 rows,
770 row_count,
771 min_rid,
772 max_rid,
773 });
774 spilled_tables.insert(table_id);
775 }
776 }
777
778 if spill_guard.is_some() {
780 if let Some(hook) = self.spill_hook.lock().as_ref() {
781 hook();
782 }
783 }
784
785 let mut prebuilt: Vec<Option<Row>> = Vec::with_capacity(staging.len());
796 {
797 let tables = self.tables.read();
798 for (table_id, staged) in &staging {
799 match staged {
800 Staged::Put(cells) if !spilled_tables.contains(table_id) => {
801 let handle = tables.get(table_id).ok_or_else(|| {
802 MongrelError::NotFound(format!("table {table_id} not mounted"))
803 })?;
804 let mut t = handle.lock();
805 t.validate_cells_not_null(cells)?;
806 let row_id = t.alloc_row_id();
807 drop(t);
808 let mut row = Row::new(row_id, Epoch(0));
809 for (c, v) in cells {
810 row.columns.insert(*c, v.clone());
811 }
812 prebuilt.push(Some(row));
813 }
814 Staged::Put(_) | Staged::Delete(_) | Staged::Truncate => prebuilt.push(None),
815 }
816 }
817 }
818
819 let added_runs: Vec<crate::wal::AddedRun> = spilled
821 .iter()
822 .map(|s| crate::wal::AddedRun {
823 table_id: s.table_id,
824 run_id: s.run_id,
825 row_count: s.row_count,
826 level: 0,
827 min_row_id: s.min_rid,
828 max_row_id: s.max_rid,
829 content_hash: [0u8; 32],
830 })
831 .collect();
832 let (new_epoch, applies, commit_seq) = {
833 let mut wal = self.shared_wal.lock();
834
835 if self.conflicts.version() != pre_validate_version
840 && self.conflicts.conflicts(&write_keys, read_epoch)
841 {
842 drop(wal);
846 for s in &spilled {
847 if let Some(parent) = s.pending_path.parent() {
848 let _ = std::fs::remove_dir_all(parent);
849 }
850 }
851 return Err(MongrelError::Conflict(
852 "write-write conflict (sequencer delta re-check)".into(),
853 ));
854 }
855
856 let new_epoch = self.epoch.bump_assigned();
857 let mut applies: Vec<(u64, Vec<StagedOp>)> = Vec::new();
858
859 for (idx, (table_id, staged)) in staging.iter().enumerate() {
860 if spilled_tables.contains(table_id) && matches!(staged, Staged::Put(_)) {
863 continue;
864 }
865 let mut ops = Vec::new();
866 match staged {
867 Staged::Put(_) => {
868 let mut row = prebuilt[idx].take().expect("prebuilt put row");
870 row.committed_epoch = new_epoch;
871 let payload = bincode::serialize(&vec![row.clone()])
872 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
873 wal.append(
874 txn_id,
875 *table_id,
876 Op::Put {
877 table_id: *table_id,
878 rows: payload,
879 },
880 )?;
881 ops.push(StagedOp::Put(row));
882 }
883 Staged::Delete(rid) => {
884 wal.append(
885 txn_id,
886 *table_id,
887 Op::Delete {
888 table_id: *table_id,
889 row_ids: vec![*rid],
890 },
891 )?;
892 ops.push(StagedOp::Delete(*rid));
893 }
894 Staged::Truncate => {
895 wal.append(
896 txn_id,
897 *table_id,
898 Op::TruncateTable {
899 table_id: *table_id,
900 },
901 )?;
902 ops.push(StagedOp::Truncate);
903 }
904 }
905 applies.push((*table_id, ops));
906 }
907
908 let commit_seq = wal.append_commit(txn_id, new_epoch, &added_runs)?;
909
910 self.conflicts.record(&write_keys, new_epoch);
915 (new_epoch, applies, commit_seq)
916 };
917
918 self.group
920 .await_durable(&self.shared_wal, commit_seq)
921 .inspect_err(|_| {
922 self.poisoned.store(true, Ordering::Relaxed);
923 })?;
924
925 {
927 let tables = self.tables.read();
928 for s in &spilled {
930 if let Some(handle) = tables.get(&s.table_id) {
931 let mut t = handle.lock();
932 let dest = t.run_path(s.run_id as u64);
933 std::fs::rename(&s.pending_path, &dest)?;
934 if let Some(parent) = s.pending_path.parent() {
936 let _ = std::fs::remove_dir_all(parent);
937 }
938 t.link_run(crate::manifest::RunRef {
939 run_id: s.run_id,
940 level: 0,
941 epoch_created: new_epoch.0,
942 row_count: s.row_count,
943 });
944 t.apply_run_metadata(&s.rows)?;
949 t.invalidate_pending_cache();
950 t.persist_manifest(new_epoch)?;
951 }
952 }
953 for (table_id, ops) in applies {
955 if let Some(handle) = tables.get(&table_id) {
956 let mut t = handle.lock();
957 for op in ops {
958 match op {
959 StagedOp::Put(row) => t.apply_put_rows(vec![row])?,
960 StagedOp::Delete(rid) => t.apply_delete(rid, new_epoch),
961 StagedOp::Truncate => t.apply_truncate(new_epoch)?,
962 }
963 }
964 t.invalidate_pending_cache();
965 t.persist_manifest(new_epoch)?;
966 }
967 }
968 }
969
970 self.advance_visible(new_epoch);
971 Ok(new_epoch)
972 }
973
974 fn advance_visible(&self, published: Epoch) {
980 self.epoch.publish_in_order(published);
981 }
982
983 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
986 let e = self.epoch.visible();
987 let g = self.snapshots.register(e);
988 (Snapshot::at(e), g)
989 }
990
991 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
994 let e = self.epoch.visible();
995 let g = self.snapshots.register_owned(e);
996 (Snapshot::at(e), g)
997 }
998
999 pub fn table_names(&self) -> Vec<String> {
1001 self.catalog
1002 .read()
1003 .tables
1004 .iter()
1005 .filter(|t| matches!(t.state, TableState::Live))
1006 .map(|t| t.name.clone())
1007 .collect()
1008 }
1009
1010 pub fn table(&self, name: &str) -> Result<TableHandle> {
1012 let cat = self.catalog.read();
1013 let entry = cat
1014 .live(name)
1015 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
1016 let id = entry.table_id;
1017 drop(cat);
1018 self.tables
1019 .read()
1020 .get(&id)
1021 .cloned()
1022 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
1023 }
1024
1025 fn table_by_id(&self, id: u64) -> Result<TableHandle> {
1028 self.tables
1029 .read()
1030 .get(&id)
1031 .cloned()
1032 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
1033 }
1034
1035 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
1041 use crate::wal::DdlOp;
1042 use std::sync::atomic::Ordering;
1043
1044 if self.poisoned.load(Ordering::Relaxed) {
1045 return Err(MongrelError::Other(
1046 "database poisoned by fsync error".into(),
1047 ));
1048 }
1049
1050 let _g = self.ddl_lock.lock();
1051 {
1052 let cat = self.catalog.read();
1053 if cat.live(name).is_some() {
1054 return Err(MongrelError::InvalidArgument(format!(
1055 "table {name:?} already exists"
1056 )));
1057 }
1058 }
1059
1060 let commit_lock = Arc::clone(&self.commit_lock);
1063 let _c = commit_lock.lock();
1064 let table_id = {
1065 let mut cat = self.catalog.write();
1066 let id = cat.next_table_id;
1067 cat.next_table_id += 1;
1068 id
1069 };
1070 let epoch = self.epoch.bump_assigned();
1071 let txn_id = self.alloc_txn_id();
1072
1073 let mut schema = schema;
1077 schema.schema_id = table_id;
1078 schema.validate_auto_increment()?;
1085
1086 let schema_json = DdlOp::encode_schema(&schema)?;
1089 let commit_seq = {
1090 let mut wal = self.shared_wal.lock();
1091 wal.append(
1092 txn_id,
1093 table_id,
1094 crate::wal::Op::Ddl(DdlOp::CreateTable {
1095 table_id,
1096 name: name.to_string(),
1097 schema_json,
1098 }),
1099 )?;
1100 wal.append_commit(txn_id, epoch, &[])?
1101 };
1102 self.group
1103 .await_durable(&self.shared_wal, commit_seq)
1104 .inspect_err(|_| {
1105 self.poisoned.store(true, Ordering::Relaxed);
1106 })?;
1107
1108 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
1110 std::fs::create_dir_all(&tdir)?;
1111 let ctx = SharedCtx {
1112 epoch: Arc::clone(&self.epoch),
1113 page_cache: Arc::clone(&self.page_cache),
1114 decoded_cache: Arc::clone(&self.decoded_cache),
1115 snapshots: Arc::clone(&self.snapshots),
1116 kek: self.kek.clone(),
1117 commit_lock: Arc::clone(&self.commit_lock),
1118 shared: Some(crate::engine::SharedWalCtx {
1119 wal: Arc::clone(&self.shared_wal),
1120 group: Arc::clone(&self.group),
1121 poisoned: Arc::clone(&self.poisoned),
1122 txn_ids: Arc::clone(&self.next_txn_id),
1123 }),
1124 };
1125 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
1126
1127 {
1130 let mut cat = self.catalog.write();
1131 cat.tables.push(CatalogEntry {
1132 table_id,
1133 name: name.to_string(),
1134 schema,
1135 state: TableState::Live,
1136 created_epoch: epoch.0,
1137 });
1138 }
1139 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1140 self.tables
1141 .write()
1142 .insert(table_id, Arc::new(Mutex::new(table)));
1143
1144 self.advance_visible(epoch);
1145 Ok(table_id)
1146 }
1147
1148 pub fn drop_table(&self, name: &str) -> Result<()> {
1150 use crate::wal::DdlOp;
1151 use std::sync::atomic::Ordering;
1152
1153 if self.poisoned.load(Ordering::Relaxed) {
1154 return Err(MongrelError::Other(
1155 "database poisoned by fsync error".into(),
1156 ));
1157 }
1158
1159 let _g = self.ddl_lock.lock();
1160 let table_id = {
1161 let cat = self.catalog.read();
1162 cat.live(name)
1163 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
1164 .table_id
1165 };
1166
1167 let commit_lock = Arc::clone(&self.commit_lock);
1168 let _c = commit_lock.lock();
1169 let epoch = self.epoch.bump_assigned();
1170 let txn_id = self.alloc_txn_id();
1171 let commit_seq = {
1172 let mut wal = self.shared_wal.lock();
1173 wal.append(
1174 txn_id,
1175 table_id,
1176 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
1177 )?;
1178 wal.append_commit(txn_id, epoch, &[])?
1179 };
1180 self.group
1181 .await_durable(&self.shared_wal, commit_seq)
1182 .inspect_err(|_| {
1183 self.poisoned.store(true, Ordering::Relaxed);
1184 })?;
1185
1186 {
1187 let mut cat = self.catalog.write();
1188 let entry = cat
1189 .tables
1190 .iter_mut()
1191 .find(|t| t.table_id == table_id)
1192 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
1193 entry.state = TableState::Dropped { at_epoch: epoch.0 };
1194 }
1195 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1196 self.tables.write().remove(&table_id);
1197
1198 self.advance_visible(epoch);
1199 Ok(())
1200 }
1201
1202 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
1211 use crate::wal::DdlOp;
1212 use std::sync::atomic::Ordering;
1213
1214 if self.poisoned.load(Ordering::Relaxed) {
1215 return Err(MongrelError::Other(
1216 "database poisoned by fsync error".into(),
1217 ));
1218 }
1219
1220 if name == new_name {
1223 return Ok(());
1224 }
1225 if new_name.is_empty() {
1226 return Err(MongrelError::InvalidArgument(
1227 "rename_table: new name must not be empty".into(),
1228 ));
1229 }
1230
1231 let _g = self.ddl_lock.lock();
1232 let table_id = {
1233 let cat = self.catalog.read();
1234 let src = cat
1235 .live(name)
1236 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
1237 if cat.live(new_name).is_some() {
1241 return Err(MongrelError::InvalidArgument(format!(
1242 "rename_table: a table named {new_name:?} already exists"
1243 )));
1244 }
1245 src.table_id
1246 };
1247
1248 let commit_lock = Arc::clone(&self.commit_lock);
1249 let _c = commit_lock.lock();
1250 let epoch = self.epoch.bump_assigned();
1251 let txn_id = self.alloc_txn_id();
1252 let commit_seq = {
1253 let mut wal = self.shared_wal.lock();
1254 wal.append(
1255 txn_id,
1256 table_id,
1257 crate::wal::Op::Ddl(DdlOp::RenameTable {
1258 table_id,
1259 new_name: new_name.to_string(),
1260 }),
1261 )?;
1262 wal.append_commit(txn_id, epoch, &[])?
1263 };
1264 self.group
1265 .await_durable(&self.shared_wal, commit_seq)
1266 .inspect_err(|_| {
1267 self.poisoned.store(true, Ordering::Relaxed);
1268 })?;
1269
1270 {
1271 let mut cat = self.catalog.write();
1272 let entry = cat
1273 .tables
1274 .iter_mut()
1275 .find(|t| t.table_id == table_id)
1276 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
1277 entry.name = new_name.to_string();
1278 }
1279 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1280 self.advance_visible(epoch);
1283 Ok(())
1284 }
1285
1286 pub fn alter_column(
1287 &self,
1288 table_name: &str,
1289 column_name: &str,
1290 change: AlterColumn,
1291 ) -> Result<ColumnDef> {
1292 use crate::wal::DdlOp;
1293 use std::sync::atomic::Ordering;
1294
1295 if self.poisoned.load(Ordering::Relaxed) {
1296 return Err(MongrelError::Other(
1297 "database poisoned by fsync error".into(),
1298 ));
1299 }
1300
1301 let _g = self.ddl_lock.lock();
1302 let table_id = {
1303 let cat = self.catalog.read();
1304 cat.live(table_name)
1305 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
1306 .table_id
1307 };
1308 let handle =
1309 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
1310 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
1311 })?;
1312 let mut table = handle.lock();
1313 let column = table.prepare_alter_column(column_name, &change)?;
1314 if table
1315 .schema()
1316 .columns
1317 .iter()
1318 .find(|c| c.id == column.id)
1319 .is_some_and(|c| c == &column)
1320 {
1321 return Ok(column);
1322 }
1323
1324 let commit_lock = Arc::clone(&self.commit_lock);
1325 let _c = commit_lock.lock();
1326 let epoch = self.epoch.bump_assigned();
1327 let txn_id = self.alloc_txn_id();
1328 let column_json = DdlOp::encode_column(&column)?;
1329 let commit_seq = {
1330 let mut wal = self.shared_wal.lock();
1331 wal.append(
1332 txn_id,
1333 table_id,
1334 crate::wal::Op::Ddl(DdlOp::AlterTable {
1335 table_id,
1336 column_json,
1337 }),
1338 )?;
1339 wal.append_commit(txn_id, epoch, &[])?
1340 };
1341 self.group
1342 .await_durable(&self.shared_wal, commit_seq)
1343 .inspect_err(|_| {
1344 self.poisoned.store(true, Ordering::Relaxed);
1345 })?;
1346
1347 table.apply_altered_column(column.clone())?;
1348 let schema = table.schema().clone();
1349 drop(table);
1350
1351 {
1352 let mut cat = self.catalog.write();
1353 let entry = cat
1354 .tables
1355 .iter_mut()
1356 .find(|t| t.table_id == table_id)
1357 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
1358 entry.schema = schema;
1359 }
1360 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1361
1362 self.advance_visible(epoch);
1363 Ok(column)
1364 }
1365
1366 pub fn gc(&self) -> Result<usize> {
1372 let min_active = self.snapshots.min_active(self.epoch.visible()).0;
1373 let mut reclaimed = 0;
1374
1375 let cat = self.catalog.read();
1377 for entry in &cat.tables {
1378 if let TableState::Dropped { at_epoch } = entry.state {
1379 if at_epoch <= min_active {
1380 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
1381 if tdir.exists() {
1382 std::fs::remove_dir_all(&tdir)?;
1383 reclaimed += 1;
1384 }
1385 }
1386 }
1387 }
1388 drop(cat);
1389
1390 let cat = self.catalog.read();
1395 for entry in &cat.tables {
1396 if !matches!(entry.state, TableState::Live) {
1397 continue;
1398 }
1399 let txn_dir = self
1400 .root
1401 .join(TABLES_DIR)
1402 .join(entry.table_id.to_string())
1403 .join("_txn");
1404 if !txn_dir.exists() {
1405 continue;
1406 }
1407 for sub in std::fs::read_dir(&txn_dir)? {
1408 let sub = sub?;
1409 let name = sub.file_name();
1410 let Some(name) = name.to_str() else { continue };
1411 let is_active = name
1413 .parse::<u64>()
1414 .map(|id| self.active_spills.is_active(id))
1415 .unwrap_or(false);
1416 if is_active {
1417 continue;
1418 }
1419 std::fs::remove_dir_all(sub.path())?;
1420 reclaimed += 1;
1421 }
1422 }
1423 drop(cat);
1424
1425 let tables = self.tables.read();
1429 for handle in tables.values() {
1430 reclaimed += handle.lock().reap_retiring(Epoch(min_active))?;
1431 }
1432
1433 let all_durable = self.active_spills.is_idle()
1441 && tables.values().all(|h| {
1442 let g = h.lock();
1443 g.memtable_len() == 0 && g.mutable_run_len() == 0
1444 });
1445 drop(tables);
1446 if all_durable {
1447 reclaimed += self.shared_wal.lock().gc_segments(u64::MAX)?;
1448 }
1449
1450 Ok(reclaimed)
1451 }
1452 fn alloc_txn_id(&self) -> u64 {
1453 let mut g = self.next_txn_id.lock();
1454 let id = *g;
1455 *g = g.wrapping_add(1);
1456 id
1457 }
1458
1459 pub fn set_spill_threshold(&self, bytes: u64) {
1463 self.spill_threshold
1464 .store(bytes, std::sync::atomic::Ordering::Relaxed);
1465 }
1466
1467 #[doc(hidden)]
1471 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
1472 *self.spill_hook.lock() = Some(Box::new(f));
1473 }
1474
1475 #[doc(hidden)]
1479 pub fn __wal_group_sync_count(&self) -> u64 {
1480 self.shared_wal.lock().group_sync_count()
1481 }
1482
1483 #[doc(hidden)]
1486 pub fn __poison(&self) {
1487 self.poisoned
1488 .store(true, std::sync::atomic::Ordering::Relaxed);
1489 }
1490
1491 pub fn check(&self) -> Vec<CheckIssue> {
1504 let mut issues = Vec::new();
1505 let cat = self.catalog.read();
1506 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
1507 for entry in &cat.tables {
1508 if !matches!(entry.state, TableState::Live) {
1509 continue;
1510 }
1511 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
1512 let mut err = |sev: &str, desc: String| {
1513 issues.push(CheckIssue {
1514 table_id: entry.table_id,
1515 table_name: entry.name.clone(),
1516 severity: sev.into(),
1517 description: desc,
1518 });
1519 };
1520 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
1521 Ok(m) => m,
1522 Err(e) => {
1523 err("error", format!("manifest read failed: {e}"));
1524 continue;
1525 }
1526 };
1527 if m.flushed_epoch > m.current_epoch {
1528 err(
1529 "error",
1530 format!(
1531 "flushed_epoch {} exceeds current_epoch {} (impossible)",
1532 m.flushed_epoch, m.current_epoch
1533 ),
1534 );
1535 }
1536
1537 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
1538 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
1539 for rr in &m.runs {
1540 referenced.insert(rr.run_id);
1541 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
1542 if !run_path.exists() {
1543 err("error", format!("missing run file: r-{}.sr", rr.run_id));
1544 continue;
1545 }
1546 match crate::sorted_run::RunReader::open(
1547 &run_path,
1548 entry.schema.clone(),
1549 self.kek.clone(),
1550 ) {
1551 Ok(reader) => {
1552 if reader.row_count() as u64 != rr.row_count {
1553 err(
1554 "error",
1555 format!(
1556 "run r-{} row count mismatch: manifest {} vs run {}",
1557 rr.run_id,
1558 rr.row_count,
1559 reader.row_count()
1560 ),
1561 );
1562 }
1563 }
1564 Err(e) => {
1565 err(
1566 "error",
1567 format!("run r-{} integrity check failed: {e}", rr.run_id),
1568 );
1569 }
1570 }
1571 }
1572
1573 for r in &m.retiring {
1577 referenced.insert(r.run_id);
1578 }
1579
1580 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
1582 for ent in rd.flatten() {
1583 let p = ent.path();
1584 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
1585 continue;
1586 }
1587 let run_id = p
1588 .file_stem()
1589 .and_then(|s| s.to_str())
1590 .and_then(|s| s.strip_prefix("r-"))
1591 .and_then(|s| s.parse::<u128>().ok());
1592 if let Some(id) = run_id {
1593 if !referenced.contains(&id) {
1594 err(
1595 "warning",
1596 format!("orphan run file r-{id}.sr not referenced by the manifest"),
1597 );
1598 }
1599 }
1600 }
1601 }
1602 }
1603
1604 for (seg, msg) in self.shared_wal.lock().verify_segments() {
1611 issues.push(CheckIssue {
1612 table_id: WAL_TABLE_ID,
1613 table_name: "<wal>".into(),
1614 severity: "error".into(),
1615 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
1616 });
1617 }
1618 issues
1619 }
1620
1621 pub fn doctor(&self) -> Result<Vec<u64>> {
1625 let _ddl = self.ddl_lock.lock();
1628 let issues = self.check();
1629 let bad_tables: std::collections::HashSet<u64> = issues
1634 .iter()
1635 .filter(|i| i.severity == "error" && i.table_id != WAL_TABLE_ID)
1636 .map(|i| i.table_id)
1637 .collect();
1638 if bad_tables.is_empty() {
1639 return Ok(Vec::new());
1640 }
1641
1642 let qdir = self.root.join("_quarantine");
1643 std::fs::create_dir_all(&qdir)?;
1644 let mut quarantined = Vec::new();
1645 for &table_id in &bad_tables {
1646 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
1647 if tdir.exists() {
1648 let dest = qdir.join(table_id.to_string());
1649 std::fs::rename(&tdir, &dest)?;
1650 }
1651 {
1652 let mut cat = self.catalog.write();
1653 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
1654 entry.state = TableState::Dropped {
1655 at_epoch: self.epoch.visible().0,
1656 };
1657 }
1658 }
1659 self.tables.write().remove(&table_id);
1661 quarantined.push(table_id);
1662 }
1663 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1664 Ok(quarantined)
1665 }
1666
1667 #[allow(dead_code)]
1669 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
1670 self.kek.as_ref()
1671 }
1672
1673 #[allow(dead_code)]
1675 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
1676 &self.epoch
1677 }
1678
1679 #[allow(dead_code)]
1681 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
1682 &self.snapshots
1683 }
1684}
1685
1686fn recover_shared_wal(
1695 root: &Path,
1696 tables: &HashMap<u64, TableHandle>,
1697 epoch: &EpochAuthority,
1698 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
1699) -> Result<()> {
1700 use crate::memtable::Row;
1701 use crate::rowid::RowId;
1702 use crate::wal::{Op, SharedWal};
1703
1704 let records = SharedWal::replay_with_dek(root, wal_dek)?;
1705
1706 let mut committed: HashMap<u64, u64> = HashMap::new();
1708 let mut spilled_to_link: Vec<(
1709 u64, u64, Vec<crate::wal::AddedRun>,
1712 )> = Vec::new();
1713 for r in &records {
1714 if let Op::TxnCommit {
1715 epoch: ce,
1716 ref added_runs,
1717 } = r.op
1718 {
1719 committed.insert(r.txn_id, ce);
1720 if !added_runs.is_empty() {
1721 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
1722 }
1723 }
1724 }
1725
1726 type TableStage = (Vec<Row>, Vec<(RowId, Epoch)>, Option<Epoch>, Epoch);
1728 let mut stage: HashMap<u64, TableStage> = HashMap::new();
1729 let mut max_epoch = epoch.visible().0;
1730 for r in records {
1731 let Some(&ce) = committed.get(&r.txn_id) else {
1732 continue; };
1734 let commit_epoch = Epoch(ce);
1735 max_epoch = max_epoch.max(ce);
1736 match r.op {
1737 Op::Put { table_id, rows } => {
1738 let skip = tables
1740 .get(&table_id)
1741 .map(|h| h.lock().flushed_epoch() >= ce)
1742 .unwrap_or(true);
1743 if skip {
1744 continue;
1745 }
1746 let rows: Vec<Row> = match bincode::deserialize(&rows) {
1747 Ok(v) => v,
1748 Err(_) => continue,
1749 };
1750 let rows: Vec<Row> = rows
1753 .into_iter()
1754 .map(|mut row| {
1755 row.committed_epoch = commit_epoch;
1756 row
1757 })
1758 .collect();
1759 let entry = stage
1760 .entry(table_id)
1761 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
1762 entry.0.extend(rows);
1763 entry.3 = commit_epoch;
1764 }
1765 Op::Delete { table_id, row_ids } => {
1766 let skip = tables
1767 .get(&table_id)
1768 .map(|h| h.lock().flushed_epoch() >= ce)
1769 .unwrap_or(true);
1770 if skip {
1771 continue;
1772 }
1773 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
1774 let entry = stage
1775 .entry(table_id)
1776 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
1777 entry.1.extend(dels);
1778 entry.3 = commit_epoch;
1779 }
1780 Op::TruncateTable { table_id } => {
1781 let skip = tables
1782 .get(&table_id)
1783 .map(|h| h.lock().flushed_epoch() >= ce)
1784 .unwrap_or(true);
1785 if skip {
1786 continue;
1787 }
1788 stage.insert(
1789 table_id,
1790 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
1791 );
1792 }
1793 _ => {}
1794 }
1795 }
1796
1797 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
1798 let Some(handle) = tables.get(&table_id) else {
1799 continue;
1800 };
1801 let mut t = handle.lock();
1802 if let Some(epoch) = truncate_epoch {
1803 t.apply_truncate(epoch)?;
1804 }
1805 t.recover_apply(rows, deletes)?;
1806 if truncate_epoch.is_some() {
1807 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
1808 t.live_count = rows.len() as u64;
1809 t.persist_manifest(table_epoch)?;
1810 }
1811 }
1812
1813 for (txn_id, ce, added_runs) in &spilled_to_link {
1817 for ar in added_runs {
1818 let Some(handle) = tables.get(&ar.table_id) else {
1819 continue;
1820 };
1821 let mut t = handle.lock();
1822 let dest = t.run_path(ar.run_id as u64);
1823 if !dest.exists() {
1824 let pending = root
1825 .join(TABLES_DIR)
1826 .join(ar.table_id.to_string())
1827 .join("_txn")
1828 .join(txn_id.to_string())
1829 .join(format!("r-{}.sr", ar.run_id));
1830 if pending.exists() {
1831 if let Some(parent) = pending.parent() {
1832 std::fs::rename(&pending, &dest)?;
1833 let _ = std::fs::remove_dir_all(parent);
1834 }
1835 }
1836 }
1837 if t.run_path(ar.run_id as u64).exists() {
1843 t.recover_spilled_run(crate::manifest::RunRef {
1844 run_id: ar.run_id,
1845 level: ar.level,
1846 epoch_created: *ce,
1847 row_count: ar.row_count,
1848 });
1849 }
1850 }
1851 }
1852
1853 epoch.advance_recovered(Epoch(max_epoch));
1854 Ok(())
1855}
1856
1857fn recover_ddl_from_wal(
1863 root: &Path,
1864 cat: &mut Catalog,
1865 meta_dek: Option<&[u8; META_DEK_LEN]>,
1866 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
1867) -> Result<()> {
1868 use crate::wal::{DdlOp, Op, SharedWal};
1869
1870 let records = match SharedWal::replay_with_dek(root, wal_dek) {
1871 Ok(r) => r,
1872 Err(_) => return Ok(()),
1873 };
1874
1875 let mut committed: HashMap<u64, u64> = HashMap::new();
1876 for r in &records {
1877 if let Op::TxnCommit { epoch: ce, .. } = r.op {
1878 committed.insert(r.txn_id, ce);
1879 }
1880 }
1881
1882 let mut changed = false;
1883 for r in records {
1884 let Some(&ce) = committed.get(&r.txn_id) else {
1885 continue;
1886 };
1887 match r.op {
1888 Op::Ddl(DdlOp::CreateTable {
1889 table_id,
1890 ref name,
1891 ref schema_json,
1892 }) => {
1893 if cat.tables.iter().any(|t| t.table_id == table_id) {
1894 continue;
1895 }
1896 let schema = DdlOp::decode_schema(schema_json)?;
1897 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
1898 if !tdir.exists() {
1899 std::fs::create_dir_all(tdir.join(crate::engine::WAL_DIR))?;
1900 std::fs::create_dir_all(tdir.join(crate::engine::RUNS_DIR))?;
1901 crate::engine::write_schema(&tdir, &schema)?;
1902 let mut m = crate::manifest::Manifest::new(table_id, schema.schema_id);
1908 crate::manifest::write_atomic(&tdir, &mut m, meta_dek)?;
1909 }
1910 cat.tables.push(CatalogEntry {
1911 table_id,
1912 name: name.clone(),
1913 schema,
1914 state: TableState::Live,
1915 created_epoch: ce,
1916 });
1917 cat.next_table_id = cat.next_table_id.max(table_id + 1);
1918 changed = true;
1919 }
1920 Op::Ddl(DdlOp::DropTable { table_id }) => {
1921 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
1922 if matches!(entry.state, TableState::Live) {
1923 entry.state = TableState::Dropped { at_epoch: ce };
1924 changed = true;
1925 }
1926 }
1927 }
1928 Op::Ddl(DdlOp::RenameTable {
1929 table_id,
1930 ref new_name,
1931 }) => {
1932 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
1933 if entry.name != *new_name {
1934 entry.name = new_name.clone();
1935 changed = true;
1936 }
1937 }
1938 }
1942 Op::Ddl(DdlOp::AlterTable {
1943 table_id,
1944 ref column_json,
1945 }) => {
1946 let column = DdlOp::decode_column(column_json)?;
1947 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
1948 if apply_recovered_column_def(&mut entry.schema, column) {
1949 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
1950 if tdir.exists() {
1951 crate::engine::write_schema(&tdir, &entry.schema)?;
1952 }
1953 changed = true;
1954 }
1955 }
1956 }
1957 _ => {}
1958 }
1959 }
1960
1961 if changed {
1962 catalog::write_atomic(root, cat, meta_dek)?;
1963 }
1964 Ok(())
1965}
1966
1967fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> bool {
1968 match schema.columns.iter_mut().find(|c| c.id == column.id) {
1969 Some(existing) if *existing == column => false,
1970 Some(existing) => {
1971 *existing = column;
1972 schema.schema_id = schema.schema_id.saturating_add(1);
1973 true
1974 }
1975 None => {
1976 schema.columns.push(column);
1977 schema.schema_id = schema.schema_id.saturating_add(1);
1978 true
1979 }
1980 }
1981}
1982
1983fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
1988 for entry in &cat.tables {
1989 let txn_dir = root
1990 .join(TABLES_DIR)
1991 .join(entry.table_id.to_string())
1992 .join("_txn");
1993 if txn_dir.exists() {
1994 let _ = std::fs::remove_dir_all(&txn_dir);
1995 }
1996 }
1997}