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::procedure::{
14 ProcedureCallOutput, ProcedureCallResult, ProcedureCallRow, ProcedureCondition, ProcedureEntry,
15 ProcedureStep, ProcedureValue, StoredProcedure,
16};
17use crate::retention::{OwnedSnapshotGuard, SnapshotGuard, SnapshotRegistry};
18use crate::schema::{AlterColumn, ColumnDef, Schema};
19use parking_lot::{Mutex, RwLock};
20use std::collections::HashMap;
21use std::path::{Path, PathBuf};
22use std::sync::Arc;
23
24pub const TABLES_DIR: &str = "tables";
25pub const META_DIR: &str = "_meta";
26pub const KEYS_FILENAME: &str = "keys";
27
28pub const WAL_TABLE_ID: u64 = u64::MAX;
32
33struct SpilledRun {
35 table_id: u64,
36 run_id: u128,
37 pending_path: PathBuf,
38 rows: Vec<crate::memtable::Row>,
39 row_count: u64,
40 min_rid: u64,
41 max_rid: u64,
42}
43
44#[derive(Debug, Clone)]
46pub struct CheckIssue {
47 pub table_id: u64,
48 pub table_name: String,
49 pub severity: String,
50 pub description: String,
51}
52
53pub type TableHandle = Arc<Mutex<Table>>;
57
58pub struct Database {
61 root: PathBuf,
62 catalog: RwLock<Catalog>,
63 epoch: Arc<EpochAuthority>,
64 snapshots: Arc<SnapshotRegistry>,
65 page_cache: Arc<crate::cache::Sharded<crate::cache::PageCache>>,
66 decoded_cache: Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>,
67 commit_lock: Arc<Mutex<()>>,
68 shared_wal: Arc<Mutex<crate::wal::SharedWal>>,
73 next_txn_id: Arc<Mutex<u64>>,
77 tables: RwLock<HashMap<u64, TableHandle>>,
78 kek: Option<Arc<crate::encryption::Kek>>,
79 ddl_lock: Mutex<()>,
82 meta_dek: Option<[u8; META_DEK_LEN]>,
83 spill_threshold: std::sync::atomic::AtomicU64,
86 conflicts: crate::txn::ConflictIndex,
89 active_txns: crate::txn::ActiveTxns,
92 poisoned: Arc<std::sync::atomic::AtomicBool>,
95 group: Arc<crate::txn::GroupCommit>,
99 active_spills: Arc<crate::retention::ActiveSpills>,
102 #[doc(hidden)]
106 spill_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
107}
108
109impl Database {
110 pub fn create(root: impl AsRef<Path>) -> Result<Self> {
112 Self::create_inner(root, None)
113 }
114
115 #[cfg(feature = "encryption")]
118 pub fn create_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
119 let root = root.as_ref();
120 std::fs::create_dir_all(root)?;
121 std::fs::create_dir_all(root.join(META_DIR))?;
122 let salt = crate::encryption::random_salt();
123 std::fs::write(root.join(META_DIR).join(KEYS_FILENAME), salt)?;
124 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
125 Self::create_inner(root, Some(kek))
126 }
127
128 #[cfg(feature = "encryption")]
131 pub fn create_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
132 let root = root.as_ref();
133 std::fs::create_dir_all(root)?;
134 std::fs::create_dir_all(root.join(META_DIR))?;
135 let salt = crate::encryption::random_salt();
136 std::fs::write(root.join(META_DIR).join(KEYS_FILENAME), salt)?;
137 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
138 Self::create_inner(root, Some(kek))
139 }
140
141 fn create_inner(
142 root: impl AsRef<Path>,
143 kek: Option<Arc<crate::encryption::Kek>>,
144 ) -> Result<Self> {
145 let root = root.as_ref().to_path_buf();
146 std::fs::create_dir_all(&root)?;
147 std::fs::create_dir_all(root.join(TABLES_DIR))?;
148 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
149 let cat = Catalog::empty();
150 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
151 Self::finish_open(root, cat, kek, meta_dek, false)
152 }
153
154 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
156 Self::open_inner(root, None, None)
157 }
158
159 #[cfg(feature = "encryption")]
161 pub fn open_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
162 let root = root.as_ref();
163 let salt_bytes = std::fs::read(root.join(META_DIR).join(KEYS_FILENAME))
164 .map_err(|e| MongrelError::NotFound(format!("encryption salt file: {e}")))?;
165 let mut salt = [0u8; crate::encryption::SALT_LEN];
166 salt.copy_from_slice(&salt_bytes);
167 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
168 Self::open_inner(root, Some(kek), None)
169 }
170
171 #[cfg(feature = "encryption")]
173 pub fn open_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
174 let root = root.as_ref();
175 let salt_path = root.join(META_DIR).join(KEYS_FILENAME);
176 let salt_bytes = std::fs::read(&salt_path).map_err(|e| {
177 MongrelError::NotFound(format!(
178 "encryption salt file {:?}: {e} (database not encrypted, or corrupted)",
179 salt_path
180 ))
181 })?;
182 if salt_bytes.len() != crate::encryption::SALT_LEN {
183 return Err(MongrelError::InvalidArgument(format!(
184 "salt file is {} bytes, expected {}",
185 salt_bytes.len(),
186 crate::encryption::SALT_LEN
187 )));
188 }
189 let mut salt = [0u8; crate::encryption::SALT_LEN];
190 salt.copy_from_slice(&salt_bytes);
191 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
192 Self::open_inner(root, Some(kek), None)
193 }
194
195 fn open_inner(
196 root: impl AsRef<Path>,
197 kek: Option<Arc<crate::encryption::Kek>>,
198 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
199 ) -> Result<Self> {
200 let root = root.as_ref().to_path_buf();
201 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
202 let cat = catalog::read(&root, meta_dek.as_ref())?
203 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
204 Self::finish_open(root, cat, kek, meta_dek, true)
205 }
206
207 fn finish_open(
208 root: PathBuf,
209 cat: Catalog,
210 kek: Option<Arc<crate::encryption::Kek>>,
211 meta_dek: Option<[u8; META_DEK_LEN]>,
212 existing: bool,
213 ) -> Result<Self> {
214 let epoch = Arc::new(EpochAuthority::new(cat.db_epoch));
215 let snapshots = Arc::new(SnapshotRegistry::new());
216 let page_cache = Arc::new(crate::cache::Sharded::new(
217 crate::cache::CACHE_SHARDS,
218 || {
219 crate::cache::PageCache::new(
220 crate::engine::PAGE_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
221 )
222 },
223 ));
224 let decoded_cache = Arc::new(crate::cache::Sharded::new(
225 crate::cache::CACHE_SHARDS,
226 || {
227 crate::cache::DecodedPageCache::new(
228 crate::engine::DECODED_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
229 )
230 },
231 ));
232 let commit_lock = Arc::new(Mutex::new(()));
233 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
234 let shared_wal = Arc::new(Mutex::new(if existing {
235 crate::wal::SharedWal::open(&root, Epoch(cat.db_epoch), wal_dek.clone())?
236 } else {
237 crate::wal::SharedWal::create_with_dek(&root, Epoch(cat.db_epoch), wal_dek.clone())?
238 }));
239 let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));
243 let group = Arc::new(crate::txn::GroupCommit::new(
244 shared_wal.lock().durable_seq(),
245 ));
246 let txn_ids = Arc::new(Mutex::new(1u64));
250
251 let mut cat = cat;
257 if existing {
258 recover_ddl_from_wal(&root, &mut cat, meta_dek.as_ref(), wal_dek.as_ref())?;
259 }
260
261 let mut tables: HashMap<u64, TableHandle> = HashMap::new();
267 for entry in &cat.tables {
268 if !matches!(entry.state, TableState::Live) {
269 continue;
270 }
271 let tdir = root.join(TABLES_DIR).join(entry.table_id.to_string());
272 let ctx = SharedCtx {
273 epoch: Arc::clone(&epoch),
274 page_cache: Arc::clone(&page_cache),
275 decoded_cache: Arc::clone(&decoded_cache),
276 snapshots: Arc::clone(&snapshots),
277 kek: kek.clone(),
278 commit_lock: Arc::clone(&commit_lock),
279 shared: Some(crate::engine::SharedWalCtx {
280 wal: Arc::clone(&shared_wal),
281 group: Arc::clone(&group),
282 poisoned: Arc::clone(&poisoned),
283 txn_ids: Arc::clone(&txn_ids),
284 }),
285 };
286 let t = Table::open_in(&tdir, ctx)?;
287 tables.insert(entry.table_id, Arc::new(Mutex::new(t)));
288 }
289
290 if existing {
296 recover_shared_wal(&root, &tables, &epoch, wal_dek.as_ref())?;
297 sweep_pending_txn_dirs(&root, &cat);
300 }
301
302 if existing {
306 cat.open_generation = cat.open_generation.wrapping_add(1);
307 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
308 }
309 let next_txn_id = (cat.open_generation << 32) | 1;
310 *txn_ids.lock() = next_txn_id;
312
313 Ok(Self {
314 root,
315 catalog: RwLock::new(cat),
316 epoch,
317 snapshots,
318 page_cache,
319 decoded_cache,
320 commit_lock,
321 shared_wal,
322 next_txn_id: txn_ids,
323 tables: RwLock::new(tables),
324 kek,
325 ddl_lock: Mutex::new(()),
326 meta_dek,
327 conflicts: crate::txn::ConflictIndex::new(),
328 active_txns: crate::txn::ActiveTxns::new(),
329 poisoned,
330 group,
331 spill_threshold: std::sync::atomic::AtomicU64::new(64 * 1024 * 1024),
332 active_spills: Arc::new(crate::retention::ActiveSpills::new()),
333 spill_hook: Mutex::new(None),
334 })
335 }
336
337 pub fn visible_epoch(&self) -> Epoch {
339 self.epoch.visible()
340 }
341
342 pub fn catalog_snapshot(&self) -> Catalog {
344 self.catalog.read().clone()
345 }
346
347 pub fn root(&self) -> &Path {
349 &self.root
350 }
351
352 pub fn table_id(&self, name: &str) -> Result<u64> {
355 let cat = self.catalog.read();
356 cat.live(name)
357 .map(|e| e.table_id)
358 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
359 }
360
361 pub fn procedures(&self) -> Vec<StoredProcedure> {
362 self.catalog
363 .read()
364 .procedures
365 .iter()
366 .map(|p| p.procedure.clone())
367 .collect()
368 }
369
370 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
371 self.catalog
372 .read()
373 .procedures
374 .iter()
375 .find(|p| p.procedure.name == name)
376 .map(|p| p.procedure.clone())
377 }
378
379 pub fn create_procedure(&self, mut procedure: StoredProcedure) -> Result<StoredProcedure> {
380 let _g = self.ddl_lock.lock();
381 procedure.validate()?;
382 self.validate_procedure_references(&procedure)?;
383 {
384 let cat = self.catalog.read();
385 if cat
386 .procedures
387 .iter()
388 .any(|p| p.procedure.name == procedure.name)
389 {
390 return Err(MongrelError::InvalidArgument(format!(
391 "procedure {:?} already exists",
392 procedure.name
393 )));
394 }
395 }
396 let commit_lock = Arc::clone(&self.commit_lock);
397 let _c = commit_lock.lock();
398 let epoch = self.epoch.bump_assigned();
399 procedure.created_epoch = epoch.0;
400 procedure.updated_epoch = epoch.0;
401 {
402 let mut cat = self.catalog.write();
403 cat.procedures.push(ProcedureEntry::from(procedure.clone()));
404 cat.db_epoch = epoch.0;
405 }
406 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
407 self.advance_visible(epoch);
408 Ok(procedure)
409 }
410
411 pub fn create_or_replace_procedure(
412 &self,
413 procedure: StoredProcedure,
414 ) -> Result<StoredProcedure> {
415 let _g = self.ddl_lock.lock();
416 procedure.validate()?;
417 self.validate_procedure_references(&procedure)?;
418 let commit_lock = Arc::clone(&self.commit_lock);
419 let _c = commit_lock.lock();
420 let epoch = self.epoch.bump_assigned();
421 let replaced = {
422 let mut cat = self.catalog.write();
423 let next = match cat
424 .procedures
425 .iter()
426 .position(|p| p.procedure.name == procedure.name)
427 {
428 Some(idx) => {
429 let next = cat.procedures[idx]
430 .procedure
431 .replaced(procedure.clone(), epoch.0)?;
432 cat.procedures[idx] = ProcedureEntry::from(next.clone());
433 next
434 }
435 None => {
436 let mut next = procedure;
437 next.created_epoch = epoch.0;
438 next.updated_epoch = epoch.0;
439 cat.procedures.push(ProcedureEntry::from(next.clone()));
440 next
441 }
442 };
443 cat.db_epoch = epoch.0;
444 next
445 };
446 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
447 self.advance_visible(epoch);
448 Ok(replaced)
449 }
450
451 pub fn drop_procedure(&self, name: &str) -> Result<()> {
452 let _g = self.ddl_lock.lock();
453 let commit_lock = Arc::clone(&self.commit_lock);
454 let _c = commit_lock.lock();
455 let epoch = self.epoch.bump_assigned();
456 {
457 let mut cat = self.catalog.write();
458 let before = cat.procedures.len();
459 cat.procedures.retain(|p| p.procedure.name != name);
460 if cat.procedures.len() == before {
461 return Err(MongrelError::NotFound(format!(
462 "procedure {name:?} not found"
463 )));
464 }
465 cat.db_epoch = epoch.0;
466 }
467 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
468 self.advance_visible(epoch);
469 Ok(())
470 }
471
472 pub fn call_procedure(
473 &self,
474 name: &str,
475 args: HashMap<String, crate::Value>,
476 ) -> Result<ProcedureCallResult> {
477 let procedure = self
478 .procedure(name)
479 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
480 let args = bind_procedure_args(&procedure, args)?;
481 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
482 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
483 if has_writes {
484 let mut tx = self.begin();
485 let run = (|| {
486 for step in &procedure.body.steps {
487 let output =
488 self.execute_procedure_step(step, &args, &outputs, Some(&mut tx))?;
489 outputs.insert(step.id().to_string(), output);
490 }
491 eval_return_output(&procedure.body.return_value, &args, &outputs)
492 })();
493 match run {
494 Ok(output) => {
495 let epoch = tx.commit()?.0;
496 Ok(ProcedureCallResult {
497 epoch: Some(epoch),
498 output,
499 })
500 }
501 Err(e) => {
502 tx.rollback();
503 Err(e)
504 }
505 }
506 } else {
507 for step in &procedure.body.steps {
508 let output = self.execute_procedure_step(step, &args, &outputs, None)?;
509 outputs.insert(step.id().to_string(), output);
510 }
511 Ok(ProcedureCallResult {
512 epoch: None,
513 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
514 })
515 }
516 }
517
518 fn execute_procedure_step(
519 &self,
520 step: &ProcedureStep,
521 args: &HashMap<String, crate::Value>,
522 outputs: &HashMap<String, ProcedureCallOutput>,
523 tx: Option<&mut crate::txn::Transaction<'_>>,
524 ) -> Result<ProcedureCallOutput> {
525 match step {
526 ProcedureStep::NativeQuery {
527 table,
528 conditions,
529 projection,
530 limit,
531 ..
532 } => {
533 let mut q = crate::Query::new();
534 for condition in conditions {
535 q = q.and(eval_condition(condition, args, outputs)?);
536 }
537 let handle = self.table(table)?;
538 let mut rows = handle.lock().query(&q)?;
539 if let Some(limit) = limit {
540 rows.truncate(*limit);
541 }
542 let projection = projection.as_ref();
543 Ok(ProcedureCallOutput::Rows(
544 rows.into_iter()
545 .map(|row| ProcedureCallRow {
546 row_id: Some(row.row_id),
547 columns: match projection {
548 Some(ids) => row
549 .columns
550 .into_iter()
551 .filter(|(id, _)| ids.contains(id))
552 .collect(),
553 None => row.columns,
554 },
555 })
556 .collect(),
557 ))
558 }
559 ProcedureStep::Put {
560 table,
561 cells,
562 returning,
563 ..
564 } => {
565 let tx = tx.ok_or_else(|| {
566 MongrelError::InvalidArgument(
567 "write procedure step requires a transaction".into(),
568 )
569 })?;
570 let cells = eval_cells(cells, args, outputs)?;
571 if *returning {
572 let out = tx.put_returning(table, cells)?;
573 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
574 row_id: None,
575 columns: out.row.columns.into_iter().collect(),
576 }))
577 } else {
578 tx.put(table, cells)?;
579 Ok(ProcedureCallOutput::Null)
580 }
581 }
582 ProcedureStep::Upsert {
583 table,
584 cells,
585 update_cells,
586 returning,
587 ..
588 } => {
589 let tx = tx.ok_or_else(|| {
590 MongrelError::InvalidArgument(
591 "write procedure step requires a transaction".into(),
592 )
593 })?;
594 let cells = eval_cells(cells, args, outputs)?;
595 let action = match update_cells {
596 Some(update_cells) => {
597 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
598 }
599 None => crate::UpsertAction::DoNothing,
600 };
601 let out = tx.upsert(table, cells, action)?;
602 if *returning {
603 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
604 row_id: None,
605 columns: out.row.columns.into_iter().collect(),
606 }))
607 } else {
608 Ok(ProcedureCallOutput::Null)
609 }
610 }
611 ProcedureStep::DeleteByPk { table, pk, .. } => {
612 let tx = tx.ok_or_else(|| {
613 MongrelError::InvalidArgument(
614 "write procedure step requires a transaction".into(),
615 )
616 })?;
617 let pk = eval_value(pk, args, outputs)?;
618 let handle = self.table(table)?;
619 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
620 MongrelError::NotFound("procedure delete_by_pk target not found".into())
621 })?;
622 tx.delete(table, row_id)?;
623 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
624 }
625 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
626 "DeleteRows procedure step is not supported by the core executor yet".into(),
627 )),
628 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
629 "SqlQuery procedure step must be executed by mongreldb-query".into(),
630 )),
631 }
632 }
633
634 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
635 let cat = self.catalog.read();
636 for step in &procedure.body.steps {
637 let Some(table_name) = step.table() else {
638 continue;
639 };
640 let schema = &cat
641 .live(table_name)
642 .ok_or_else(|| {
643 MongrelError::InvalidArgument(format!(
644 "procedure {:?} references unknown table {table_name:?}",
645 procedure.name
646 ))
647 })?
648 .schema;
649 match step {
650 ProcedureStep::NativeQuery {
651 conditions,
652 projection,
653 ..
654 } => {
655 for condition in conditions {
656 validate_condition_columns(condition, schema)?;
657 }
658 if let Some(projection) = projection {
659 for id in projection {
660 validate_column_id(*id, schema)?;
661 }
662 }
663 }
664 ProcedureStep::Put { cells, .. } => {
665 for cell in cells {
666 validate_column_id(cell.column_id, schema)?;
667 }
668 }
669 ProcedureStep::Upsert {
670 cells,
671 update_cells,
672 ..
673 } => {
674 for cell in cells {
675 validate_column_id(cell.column_id, schema)?;
676 }
677 if let Some(update_cells) = update_cells {
678 for cell in update_cells {
679 validate_column_id(cell.column_id, schema)?;
680 }
681 }
682 }
683 ProcedureStep::DeleteByPk { .. } => {
684 if schema.primary_key().is_none() {
685 return Err(MongrelError::InvalidArgument(format!(
686 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
687 procedure.name
688 )));
689 }
690 }
691 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
692 }
693 }
694 Ok(())
695 }
696
697 pub fn begin(&self) -> crate::txn::Transaction<'_> {
699 let txn_id = self.alloc_txn_id();
700 let read = Snapshot::at(self.epoch.visible());
701 crate::txn::Transaction::new(self, txn_id, read)
702 }
703
704 pub fn transaction<T>(
706 &self,
707 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
708 ) -> Result<T> {
709 let mut tx = self.begin();
710 match f(&mut tx) {
711 Ok(out) => {
712 tx.commit()?;
713 Ok(out)
714 }
715 Err(e) => {
716 tx.rollback();
717 Err(e)
718 }
719 }
720 }
721
722 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
725 self.active_txns.register(epoch)
726 }
727
728 fn validate_constraints(
743 &self,
744 staging: &mut Vec<(u64, crate::txn::Staged)>,
745 read_epoch: Epoch,
746 ) -> Result<()> {
747 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
748 use crate::memtable::Row;
749 use crate::txn::Staged;
750 use std::collections::HashSet;
751
752 let snapshot = Snapshot::at(read_epoch);
753 let cat = self.catalog.read();
754
755 let live: Vec<(u64, &str, &crate::schema::Schema)> = cat
757 .tables
758 .iter()
759 .filter(|e| matches!(e.state, TableState::Live))
760 .map(|e| (e.table_id, e.name.as_str(), &e.schema))
761 .collect();
762
763 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
765 if !any_constraints {
766 return Ok(());
767 }
768
769 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
771 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
772 if let Some(r) = rows_cache.get(&table_id) {
773 return Ok(r.clone());
774 }
775 let handle = self.table_by_id(table_id)?;
776 let rows = handle.lock().visible_rows(snapshot)?;
777 rows_cache.insert(table_id, rows.clone());
778 Ok(rows)
779 };
780
781 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
786 loop {
787 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
788 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
789 .iter()
790 .filter_map(|(t, op)| match op {
791 Staged::Delete(rid) => Some((*t, *rid)),
792 _ => None,
793 })
794 .collect();
795 for (table_id, rid) in deletes {
796 if !cascaded.insert((table_id, rid.0)) {
797 continue;
798 }
799 let Some(tname) = live
800 .iter()
801 .find(|(t, _, _)| *t == table_id)
802 .map(|(_, n, _)| *n)
803 else {
804 continue;
805 };
806 let parent_handle = self.table_by_id(table_id)?;
807 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
808 continue;
809 };
810 for (child_id, _child_name, child_schema) in &live {
811 for fk in &child_schema.constraints.foreign_keys {
812 if fk.ref_table != tname {
813 continue;
814 }
815 let Some(parent_key) =
816 encode_composite_key(&fk.ref_columns, &parent_row.columns)
817 else {
818 continue;
819 };
820 match fk.on_delete {
821 FkAction::Restrict => continue,
822 FkAction::Cascade => {
823 let child_rows = load_rows(*child_id)?;
824 for cr in &child_rows {
825 if !cascaded.contains(&(*child_id, cr.row_id.0))
826 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
827 == Some(parent_key.as_slice())
828 {
829 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
830 }
831 }
832 }
833 FkAction::SetNull => {
834 let child_rows = load_rows(*child_id)?;
835 for cr in &child_rows {
836 if !cascaded.contains(&(*child_id, cr.row_id.0))
837 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
838 == Some(parent_key.as_slice())
839 {
840 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
843 .columns
844 .iter()
845 .map(|(k, v)| (*k, v.clone()))
846 .collect();
847 for cid in &fk.columns {
848 cells.retain(|(k, _)| k != cid);
849 cells.push((*cid, crate::memtable::Value::Null));
850 }
851 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
852 new_ops.push((*child_id, Staged::Put(cells)));
853 }
854 }
855 }
856 }
857 }
858 }
859 }
860 if new_ops.is_empty() {
861 break;
862 }
863 staging.extend(new_ops);
864 }
865
866 let staged_deletes: HashSet<(u64, u64)> = staging
870 .iter()
871 .filter_map(|(t, op)| match op {
872 Staged::Delete(rid) => Some((*t, rid.0)),
873 _ => None,
874 })
875 .collect();
876
877 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
879
880 for (table_id, op) in staging.iter() {
882 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id).copied()
883 else {
884 continue;
885 };
886 let cells_map: HashMap<u16, crate::memtable::Value>;
887 match op {
888 Staged::Put(cells) => {
889 cells_map = cells.iter().cloned().collect();
890
891 if !schema.constraints.checks.is_empty() {
893 validate_checks(&schema.constraints.checks, &cells_map)?;
894 }
895
896 for uc in &schema.constraints.uniques {
898 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
899 continue; };
901 let marker = (*table_id, uc.id, key.clone());
902 if !seen_unique.insert(marker) {
903 return Err(MongrelError::Conflict(format!(
904 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
905 uc.name
906 )));
907 }
908 let rows = load_rows(*table_id)?;
909 for r in &rows {
910 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
913 continue;
914 }
915 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
916 if theirs == key {
917 return Err(MongrelError::Conflict(format!(
918 "UNIQUE constraint '{}' on table '{tname}' violated",
919 uc.name
920 )));
921 }
922 }
923 }
924 }
925
926 for fk in &schema.constraints.foreign_keys {
928 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
929 continue; };
931 let Some(parent_id) = cat
932 .tables
933 .iter()
934 .find(|t| t.name == fk.ref_table)
935 .map(|t| t.table_id)
936 else {
937 return Err(MongrelError::InvalidArgument(format!(
938 "FOREIGN KEY '{}' references unknown table '{}'",
939 fk.name, fk.ref_table
940 )));
941 };
942 let parent_rows = load_rows(parent_id)?;
943 let mut found = false;
944 for r in &parent_rows {
945 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
946 continue;
947 }
948 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
949 if pkey == child_key {
950 found = true;
951 break;
952 }
953 }
954 }
955 if !found {
956 return Err(MongrelError::Conflict(format!(
957 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
958 fk.name, fk.ref_table
959 )));
960 }
961 }
962 }
963 Staged::Delete(rid) => {
964 let parent_handle = self.table_by_id(*table_id)?;
968 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
969 continue;
970 };
971 for (child_id, child_name, child_schema) in &live {
972 for fk in &child_schema.constraints.foreign_keys {
973 if fk.ref_table != tname || fk.on_delete != FkAction::Restrict {
974 continue;
975 }
976 let Some(parent_key) =
977 encode_composite_key(&fk.ref_columns, &parent_row.columns)
978 else {
979 continue;
980 };
981 let child_rows = load_rows(*child_id)?;
982 for r in &child_rows {
983 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
986 continue;
987 }
988 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
989 if ck == parent_key {
990 return Err(MongrelError::Conflict(format!(
991 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
992 fk.name
993 )));
994 }
995 }
996 }
997 }
998 }
999 }
1000 Staged::Truncate => {
1001 for (child_id, child_name, child_schema) in &live {
1005 for fk in &child_schema.constraints.foreign_keys {
1006 if fk.ref_table != tname {
1007 continue;
1008 }
1009 let child_rows = load_rows(*child_id)?;
1010 if child_rows
1011 .iter()
1012 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
1013 {
1014 return Err(MongrelError::Conflict(format!(
1015 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
1016 fk.name
1017 )));
1018 }
1019 }
1020 }
1021 }
1022 }
1023 }
1024 Ok(())
1025 }
1026
1027 pub(crate) fn commit_transaction(
1034 &self,
1035 txn_id: u64,
1036 read_epoch: Epoch,
1037 mut staging: Vec<(u64, crate::txn::Staged)>,
1038 ) -> Result<Epoch> {
1039 use crate::memtable::Row;
1040 use crate::txn::{Staged, StagedOp, WriteKey};
1041 use crate::wal::Op;
1042 use std::collections::hash_map::DefaultHasher;
1043 use std::hash::{Hash, Hasher};
1044 use std::sync::atomic::Ordering;
1045
1046 if self.poisoned.load(Ordering::Relaxed) {
1047 return Err(MongrelError::Other(
1048 "database poisoned by fsync error".into(),
1049 ));
1050 }
1051
1052 let write_keys = {
1054 let cat = self.catalog.read();
1055 let mut keys: Vec<WriteKey> = Vec::new();
1056 for (table_id, staged) in &staging {
1057 match staged {
1058 Staged::Put(cells) => {
1059 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
1060 for col in &entry.schema.columns {
1061 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
1062 if let Some((_, val)) =
1063 cells.iter().find(|(id, _)| *id == col.id)
1064 {
1065 let mut h = DefaultHasher::new();
1066 val.encode_key().hash(&mut h);
1067 keys.push(WriteKey::Unique {
1068 table_id: *table_id,
1069 index_id: 0,
1070 key_hash: h.finish(),
1071 });
1072 }
1073 }
1074 }
1075 for uc in &entry.schema.constraints.uniques {
1082 if let Some(key_bytes) = crate::constraint::encode_composite_key(
1083 &uc.columns,
1084 &cells.iter().cloned().collect(),
1085 ) {
1086 let mut h = DefaultHasher::new();
1087 key_bytes.hash(&mut h);
1088 keys.push(WriteKey::Unique {
1089 table_id: *table_id,
1090 index_id: uc.id | 0x8000,
1091 key_hash: h.finish(),
1092 });
1093 }
1094 }
1095 }
1096 }
1097 Staged::Delete(rid) => keys.push(WriteKey::Row {
1098 table_id: *table_id,
1099 row_id: rid.0,
1100 }),
1101 Staged::Truncate => keys.push(WriteKey::Table {
1102 table_id: *table_id,
1103 }),
1104 }
1105 }
1106 keys
1107 };
1108
1109 let min_active = self.active_txns.min_read_epoch();
1111 if min_active < u64::MAX {
1112 self.conflicts.prune_below(Epoch(min_active));
1113 }
1114
1115 if self.conflicts.conflicts(&write_keys, read_epoch) {
1119 return Err(MongrelError::Conflict(
1120 "write-write conflict (pre-validate, first-committer-wins)".into(),
1121 ));
1122 }
1123 let pre_validate_version = self.conflicts.version();
1124
1125 {
1130 let tables = self.tables.read();
1131 for (table_id, staged) in &mut staging {
1132 if let Staged::Put(cells) = staged {
1133 if let Some(handle) = tables.get(table_id) {
1134 let mut t = handle.lock();
1135 t.fill_auto_inc(cells)?;
1136 }
1137 }
1138 }
1139 }
1140
1141 self.validate_constraints(&mut staging, read_epoch)?;
1148
1149 let mut spilled: Vec<SpilledRun> = Vec::new();
1153 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
1154 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
1158 {
1159 let mut table_bytes: HashMap<u64, usize> = HashMap::new();
1160 for (table_id, staged) in &staging {
1161 if let Staged::Put(cells) = staged {
1162 *table_bytes.entry(*table_id).or_default() += cells.len() * 16;
1163 }
1164 }
1165 let tables = self.tables.read();
1166 for (&table_id, &bytes) in &table_bytes {
1167 if bytes as u64
1168 <= self
1169 .spill_threshold
1170 .load(std::sync::atomic::Ordering::Relaxed)
1171 {
1172 continue;
1173 }
1174 let Some(handle) = tables.get(&table_id) else {
1175 continue;
1176 };
1177 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
1178 let mut t = handle.lock();
1179 let tdir = t.table_dir().to_path_buf();
1180 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
1181 std::fs::create_dir_all(&txn_dir)?;
1182 let run_id = t.alloc_run_id() as u128;
1183 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
1184
1185 let mut rows: Vec<Row> = Vec::new();
1186 for (tid, staged) in &staging {
1187 if *tid != table_id {
1188 continue;
1189 }
1190 if let Staged::Put(cells) = staged {
1191 t.validate_cells_not_null(cells)?;
1192 let row_id = t.alloc_row_id();
1193 let mut row = Row::new(row_id, Epoch(0));
1194 for (c, v) in cells {
1195 row.columns.insert(*c, v.clone());
1196 }
1197 rows.push(row);
1198 }
1199 }
1200 let schema = t.schema_ref().clone();
1201 let kek = t.kek_ref().cloned();
1202 let specs = t.indexable_column_specs();
1203 drop(t);
1204
1205 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
1206 .uniform_epoch(true);
1207 if let Some(ref kek) = kek {
1208 writer = writer.with_encryption(kek.as_ref(), specs);
1209 }
1210 let header = writer.write(&pending_path, &rows)?;
1211 let row_count = header.row_count;
1212 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
1213 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
1214
1215 spilled.push(SpilledRun {
1216 table_id,
1217 run_id,
1218 pending_path,
1219 rows,
1220 row_count,
1221 min_rid,
1222 max_rid,
1223 });
1224 spilled_tables.insert(table_id);
1225 }
1226 }
1227
1228 if spill_guard.is_some() {
1230 if let Some(hook) = self.spill_hook.lock().as_ref() {
1231 hook();
1232 }
1233 }
1234
1235 let mut prebuilt: Vec<Option<Row>> = Vec::with_capacity(staging.len());
1246 {
1247 let tables = self.tables.read();
1248 for (table_id, staged) in &staging {
1249 match staged {
1250 Staged::Put(cells) if !spilled_tables.contains(table_id) => {
1251 let handle = tables.get(table_id).ok_or_else(|| {
1252 MongrelError::NotFound(format!("table {table_id} not mounted"))
1253 })?;
1254 let mut t = handle.lock();
1255 t.validate_cells_not_null(cells)?;
1256 let row_id = t.alloc_row_id();
1257 drop(t);
1258 let mut row = Row::new(row_id, Epoch(0));
1259 for (c, v) in cells {
1260 row.columns.insert(*c, v.clone());
1261 }
1262 prebuilt.push(Some(row));
1263 }
1264 Staged::Put(_) | Staged::Delete(_) | Staged::Truncate => prebuilt.push(None),
1265 }
1266 }
1267 }
1268
1269 let added_runs: Vec<crate::wal::AddedRun> = spilled
1271 .iter()
1272 .map(|s| crate::wal::AddedRun {
1273 table_id: s.table_id,
1274 run_id: s.run_id,
1275 row_count: s.row_count,
1276 level: 0,
1277 min_row_id: s.min_rid,
1278 max_row_id: s.max_rid,
1279 content_hash: [0u8; 32],
1280 })
1281 .collect();
1282 let (new_epoch, applies, commit_seq) = {
1283 let mut wal = self.shared_wal.lock();
1284
1285 if self.conflicts.version() != pre_validate_version
1290 && self.conflicts.conflicts(&write_keys, read_epoch)
1291 {
1292 drop(wal);
1296 for s in &spilled {
1297 if let Some(parent) = s.pending_path.parent() {
1298 let _ = std::fs::remove_dir_all(parent);
1299 }
1300 }
1301 return Err(MongrelError::Conflict(
1302 "write-write conflict (sequencer delta re-check)".into(),
1303 ));
1304 }
1305
1306 let new_epoch = self.epoch.bump_assigned();
1307 let mut applies: Vec<(u64, Vec<StagedOp>)> = Vec::new();
1308
1309 for (idx, (table_id, staged)) in staging.iter().enumerate() {
1310 if spilled_tables.contains(table_id) && matches!(staged, Staged::Put(_)) {
1313 continue;
1314 }
1315 let mut ops = Vec::new();
1316 match staged {
1317 Staged::Put(_) => {
1318 let mut row = prebuilt[idx].take().expect("prebuilt put row");
1320 row.committed_epoch = new_epoch;
1321 let payload = bincode::serialize(&vec![row.clone()])
1322 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
1323 wal.append(
1324 txn_id,
1325 *table_id,
1326 Op::Put {
1327 table_id: *table_id,
1328 rows: payload,
1329 },
1330 )?;
1331 ops.push(StagedOp::Put(row));
1332 }
1333 Staged::Delete(rid) => {
1334 wal.append(
1335 txn_id,
1336 *table_id,
1337 Op::Delete {
1338 table_id: *table_id,
1339 row_ids: vec![*rid],
1340 },
1341 )?;
1342 ops.push(StagedOp::Delete(*rid));
1343 }
1344 Staged::Truncate => {
1345 wal.append(
1346 txn_id,
1347 *table_id,
1348 Op::TruncateTable {
1349 table_id: *table_id,
1350 },
1351 )?;
1352 ops.push(StagedOp::Truncate);
1353 }
1354 }
1355 applies.push((*table_id, ops));
1356 }
1357
1358 let commit_seq = wal.append_commit(txn_id, new_epoch, &added_runs)?;
1359
1360 self.conflicts.record(&write_keys, new_epoch);
1365 (new_epoch, applies, commit_seq)
1366 };
1367
1368 self.group
1370 .await_durable(&self.shared_wal, commit_seq)
1371 .inspect_err(|_| {
1372 self.poisoned.store(true, Ordering::Relaxed);
1373 })?;
1374
1375 {
1377 let tables = self.tables.read();
1378 for s in &spilled {
1380 if let Some(handle) = tables.get(&s.table_id) {
1381 let mut t = handle.lock();
1382 let dest = t.run_path(s.run_id as u64);
1383 std::fs::rename(&s.pending_path, &dest)?;
1384 if let Some(parent) = s.pending_path.parent() {
1386 let _ = std::fs::remove_dir_all(parent);
1387 }
1388 t.link_run(crate::manifest::RunRef {
1389 run_id: s.run_id,
1390 level: 0,
1391 epoch_created: new_epoch.0,
1392 row_count: s.row_count,
1393 });
1394 t.apply_run_metadata(&s.rows)?;
1399 t.invalidate_pending_cache();
1400 t.persist_manifest(new_epoch)?;
1401 }
1402 }
1403 for (table_id, ops) in applies {
1405 if let Some(handle) = tables.get(&table_id) {
1406 let mut t = handle.lock();
1407 for op in ops {
1408 match op {
1409 StagedOp::Put(row) => t.apply_put_rows(vec![row])?,
1410 StagedOp::Delete(rid) => t.apply_delete(rid, new_epoch),
1411 StagedOp::Truncate => t.apply_truncate(new_epoch)?,
1412 }
1413 }
1414 t.invalidate_pending_cache();
1415 t.persist_manifest(new_epoch)?;
1416 }
1417 }
1418 }
1419
1420 self.advance_visible(new_epoch);
1421 Ok(new_epoch)
1422 }
1423
1424 fn advance_visible(&self, published: Epoch) {
1430 self.epoch.publish_in_order(published);
1431 }
1432
1433 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
1436 let e = self.epoch.visible();
1437 let g = self.snapshots.register(e);
1438 (Snapshot::at(e), g)
1439 }
1440
1441 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
1444 let e = self.epoch.visible();
1445 let g = self.snapshots.register_owned(e);
1446 (Snapshot::at(e), g)
1447 }
1448
1449 pub fn table_names(&self) -> Vec<String> {
1451 self.catalog
1452 .read()
1453 .tables
1454 .iter()
1455 .filter(|t| matches!(t.state, TableState::Live))
1456 .map(|t| t.name.clone())
1457 .collect()
1458 }
1459
1460 pub fn close(&self) -> Result<()> {
1466 for name in self.table_names() {
1467 if let Ok(handle) = self.table(&name) {
1468 if let Err(e) = handle.lock().close() {
1469 eprintln!("[close] flush failed for {name}: {e}");
1470 }
1471 }
1472 }
1473 Ok(())
1474 }
1475
1476 pub fn compact(&self) -> Result<(usize, usize)> {
1482 let mut compacted = 0;
1483 let mut skipped = 0;
1484 for name in self.table_names() {
1485 let Ok(handle) = self.table(&name) else {
1486 continue;
1487 };
1488 {
1489 let mut t = handle.lock();
1490 let before = t.run_count();
1491 if before < 2 {
1492 skipped += 1;
1493 continue;
1494 }
1495 match t.compact() {
1496 Ok(()) => {
1497 let after = t.run_count();
1498 compacted += 1;
1499 eprintln!("[compact] {name}: {before} -> {after} runs");
1500 }
1501 Err(e) => {
1502 eprintln!("[compact] {name}: compaction failed: {e}");
1503 skipped += 1;
1504 }
1505 }
1506 }
1507 }
1508 Ok((compacted, skipped))
1509 }
1510
1511 pub fn compact_table(&self, name: &str) -> Result<bool> {
1514 let handle = self.table(name)?;
1515 let mut t = handle.lock();
1516 let before = t.run_count();
1517 if before < 2 {
1518 return Ok(false);
1519 }
1520 t.compact()?;
1521 Ok(t.run_count() < before)
1522 }
1523
1524 pub fn table(&self, name: &str) -> Result<TableHandle> {
1526 let cat = self.catalog.read();
1527 let entry = cat
1528 .live(name)
1529 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
1530 let id = entry.table_id;
1531 drop(cat);
1532 self.tables
1533 .read()
1534 .get(&id)
1535 .cloned()
1536 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
1537 }
1538
1539 fn table_by_id(&self, id: u64) -> Result<TableHandle> {
1542 self.tables
1543 .read()
1544 .get(&id)
1545 .cloned()
1546 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
1547 }
1548
1549 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
1555 use crate::wal::DdlOp;
1556 use std::sync::atomic::Ordering;
1557
1558 if self.poisoned.load(Ordering::Relaxed) {
1559 return Err(MongrelError::Other(
1560 "database poisoned by fsync error".into(),
1561 ));
1562 }
1563
1564 let _g = self.ddl_lock.lock();
1565 {
1566 let cat = self.catalog.read();
1567 if cat.live(name).is_some() {
1568 return Err(MongrelError::InvalidArgument(format!(
1569 "table {name:?} already exists"
1570 )));
1571 }
1572 }
1573
1574 let commit_lock = Arc::clone(&self.commit_lock);
1577 let _c = commit_lock.lock();
1578 let table_id = {
1579 let mut cat = self.catalog.write();
1580 let id = cat.next_table_id;
1581 cat.next_table_id += 1;
1582 id
1583 };
1584 let epoch = self.epoch.bump_assigned();
1585 let txn_id = self.alloc_txn_id();
1586
1587 let mut schema = schema;
1591 schema.schema_id = table_id;
1592 schema.validate_auto_increment()?;
1599
1600 let schema_json = DdlOp::encode_schema(&schema)?;
1603 let commit_seq = {
1604 let mut wal = self.shared_wal.lock();
1605 wal.append(
1606 txn_id,
1607 table_id,
1608 crate::wal::Op::Ddl(DdlOp::CreateTable {
1609 table_id,
1610 name: name.to_string(),
1611 schema_json,
1612 }),
1613 )?;
1614 wal.append_commit(txn_id, epoch, &[])?
1615 };
1616 self.group
1617 .await_durable(&self.shared_wal, commit_seq)
1618 .inspect_err(|_| {
1619 self.poisoned.store(true, Ordering::Relaxed);
1620 })?;
1621
1622 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
1624 std::fs::create_dir_all(&tdir)?;
1625 let ctx = SharedCtx {
1626 epoch: Arc::clone(&self.epoch),
1627 page_cache: Arc::clone(&self.page_cache),
1628 decoded_cache: Arc::clone(&self.decoded_cache),
1629 snapshots: Arc::clone(&self.snapshots),
1630 kek: self.kek.clone(),
1631 commit_lock: Arc::clone(&self.commit_lock),
1632 shared: Some(crate::engine::SharedWalCtx {
1633 wal: Arc::clone(&self.shared_wal),
1634 group: Arc::clone(&self.group),
1635 poisoned: Arc::clone(&self.poisoned),
1636 txn_ids: Arc::clone(&self.next_txn_id),
1637 }),
1638 };
1639 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
1640
1641 {
1644 let mut cat = self.catalog.write();
1645 cat.tables.push(CatalogEntry {
1646 table_id,
1647 name: name.to_string(),
1648 schema,
1649 state: TableState::Live,
1650 created_epoch: epoch.0,
1651 });
1652 }
1653 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1654 self.tables
1655 .write()
1656 .insert(table_id, Arc::new(Mutex::new(table)));
1657
1658 self.advance_visible(epoch);
1659 Ok(table_id)
1660 }
1661
1662 pub fn drop_table(&self, name: &str) -> Result<()> {
1664 use crate::wal::DdlOp;
1665 use std::sync::atomic::Ordering;
1666
1667 if self.poisoned.load(Ordering::Relaxed) {
1668 return Err(MongrelError::Other(
1669 "database poisoned by fsync error".into(),
1670 ));
1671 }
1672
1673 let _g = self.ddl_lock.lock();
1674 let table_id = {
1675 let cat = self.catalog.read();
1676 cat.live(name)
1677 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
1678 .table_id
1679 };
1680
1681 let commit_lock = Arc::clone(&self.commit_lock);
1682 let _c = commit_lock.lock();
1683 let epoch = self.epoch.bump_assigned();
1684 let txn_id = self.alloc_txn_id();
1685 let commit_seq = {
1686 let mut wal = self.shared_wal.lock();
1687 wal.append(
1688 txn_id,
1689 table_id,
1690 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
1691 )?;
1692 wal.append_commit(txn_id, epoch, &[])?
1693 };
1694 self.group
1695 .await_durable(&self.shared_wal, commit_seq)
1696 .inspect_err(|_| {
1697 self.poisoned.store(true, Ordering::Relaxed);
1698 })?;
1699
1700 {
1701 let mut cat = self.catalog.write();
1702 let entry = cat
1703 .tables
1704 .iter_mut()
1705 .find(|t| t.table_id == table_id)
1706 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
1707 entry.state = TableState::Dropped { at_epoch: epoch.0 };
1708 }
1709 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1710 self.tables.write().remove(&table_id);
1711
1712 self.advance_visible(epoch);
1713 Ok(())
1714 }
1715
1716 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
1725 use crate::wal::DdlOp;
1726 use std::sync::atomic::Ordering;
1727
1728 if self.poisoned.load(Ordering::Relaxed) {
1729 return Err(MongrelError::Other(
1730 "database poisoned by fsync error".into(),
1731 ));
1732 }
1733
1734 if name == new_name {
1737 return Ok(());
1738 }
1739 if new_name.is_empty() {
1740 return Err(MongrelError::InvalidArgument(
1741 "rename_table: new name must not be empty".into(),
1742 ));
1743 }
1744
1745 let _g = self.ddl_lock.lock();
1746 let table_id = {
1747 let cat = self.catalog.read();
1748 let src = cat
1749 .live(name)
1750 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
1751 if cat.live(new_name).is_some() {
1755 return Err(MongrelError::InvalidArgument(format!(
1756 "rename_table: a table named {new_name:?} already exists"
1757 )));
1758 }
1759 src.table_id
1760 };
1761
1762 let commit_lock = Arc::clone(&self.commit_lock);
1763 let _c = commit_lock.lock();
1764 let epoch = self.epoch.bump_assigned();
1765 let txn_id = self.alloc_txn_id();
1766 let commit_seq = {
1767 let mut wal = self.shared_wal.lock();
1768 wal.append(
1769 txn_id,
1770 table_id,
1771 crate::wal::Op::Ddl(DdlOp::RenameTable {
1772 table_id,
1773 new_name: new_name.to_string(),
1774 }),
1775 )?;
1776 wal.append_commit(txn_id, epoch, &[])?
1777 };
1778 self.group
1779 .await_durable(&self.shared_wal, commit_seq)
1780 .inspect_err(|_| {
1781 self.poisoned.store(true, Ordering::Relaxed);
1782 })?;
1783
1784 {
1785 let mut cat = self.catalog.write();
1786 let entry = cat
1787 .tables
1788 .iter_mut()
1789 .find(|t| t.table_id == table_id)
1790 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
1791 entry.name = new_name.to_string();
1792 }
1793 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1794 self.advance_visible(epoch);
1797 Ok(())
1798 }
1799
1800 pub fn alter_column(
1801 &self,
1802 table_name: &str,
1803 column_name: &str,
1804 change: AlterColumn,
1805 ) -> Result<ColumnDef> {
1806 use crate::wal::DdlOp;
1807 use std::sync::atomic::Ordering;
1808
1809 if self.poisoned.load(Ordering::Relaxed) {
1810 return Err(MongrelError::Other(
1811 "database poisoned by fsync error".into(),
1812 ));
1813 }
1814
1815 let _g = self.ddl_lock.lock();
1816 let table_id = {
1817 let cat = self.catalog.read();
1818 cat.live(table_name)
1819 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
1820 .table_id
1821 };
1822 let handle =
1823 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
1824 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
1825 })?;
1826 let mut table = handle.lock();
1827 let column = table.prepare_alter_column(column_name, &change)?;
1828 if table
1829 .schema()
1830 .columns
1831 .iter()
1832 .find(|c| c.id == column.id)
1833 .is_some_and(|c| c == &column)
1834 {
1835 return Ok(column);
1836 }
1837
1838 let commit_lock = Arc::clone(&self.commit_lock);
1839 let _c = commit_lock.lock();
1840 let epoch = self.epoch.bump_assigned();
1841 let txn_id = self.alloc_txn_id();
1842 let column_json = DdlOp::encode_column(&column)?;
1843 let commit_seq = {
1844 let mut wal = self.shared_wal.lock();
1845 wal.append(
1846 txn_id,
1847 table_id,
1848 crate::wal::Op::Ddl(DdlOp::AlterTable {
1849 table_id,
1850 column_json,
1851 }),
1852 )?;
1853 wal.append_commit(txn_id, epoch, &[])?
1854 };
1855 self.group
1856 .await_durable(&self.shared_wal, commit_seq)
1857 .inspect_err(|_| {
1858 self.poisoned.store(true, Ordering::Relaxed);
1859 })?;
1860
1861 table.apply_altered_column(column.clone())?;
1862 let schema = table.schema().clone();
1863 drop(table);
1864
1865 {
1866 let mut cat = self.catalog.write();
1867 let entry = cat
1868 .tables
1869 .iter_mut()
1870 .find(|t| t.table_id == table_id)
1871 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
1872 entry.schema = schema;
1873 }
1874 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1875
1876 self.advance_visible(epoch);
1877 Ok(column)
1878 }
1879
1880 pub fn gc(&self) -> Result<usize> {
1886 let min_active = self.snapshots.min_active(self.epoch.visible()).0;
1887 let mut reclaimed = 0;
1888
1889 let cat = self.catalog.read();
1891 for entry in &cat.tables {
1892 if let TableState::Dropped { at_epoch } = entry.state {
1893 if at_epoch <= min_active {
1894 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
1895 if tdir.exists() {
1896 std::fs::remove_dir_all(&tdir)?;
1897 reclaimed += 1;
1898 }
1899 }
1900 }
1901 }
1902 drop(cat);
1903
1904 let cat = self.catalog.read();
1909 for entry in &cat.tables {
1910 if !matches!(entry.state, TableState::Live) {
1911 continue;
1912 }
1913 let txn_dir = self
1914 .root
1915 .join(TABLES_DIR)
1916 .join(entry.table_id.to_string())
1917 .join("_txn");
1918 if !txn_dir.exists() {
1919 continue;
1920 }
1921 for sub in std::fs::read_dir(&txn_dir)? {
1922 let sub = sub?;
1923 let name = sub.file_name();
1924 let Some(name) = name.to_str() else { continue };
1925 let is_active = name
1927 .parse::<u64>()
1928 .map(|id| self.active_spills.is_active(id))
1929 .unwrap_or(false);
1930 if is_active {
1931 continue;
1932 }
1933 std::fs::remove_dir_all(sub.path())?;
1934 reclaimed += 1;
1935 }
1936 }
1937 drop(cat);
1938
1939 let tables = self.tables.read();
1943 for handle in tables.values() {
1944 reclaimed += handle.lock().reap_retiring(Epoch(min_active))?;
1945 }
1946
1947 let all_durable = self.active_spills.is_idle()
1955 && tables.values().all(|h| {
1956 let g = h.lock();
1957 g.memtable_len() == 0 && g.mutable_run_len() == 0
1958 });
1959 drop(tables);
1960 if all_durable {
1961 reclaimed += self.shared_wal.lock().gc_segments(u64::MAX)?;
1962 }
1963
1964 Ok(reclaimed)
1965 }
1966 fn alloc_txn_id(&self) -> u64 {
1967 let mut g = self.next_txn_id.lock();
1968 let id = *g;
1969 *g = g.wrapping_add(1);
1970 id
1971 }
1972
1973 pub fn set_spill_threshold(&self, bytes: u64) {
1977 self.spill_threshold
1978 .store(bytes, std::sync::atomic::Ordering::Relaxed);
1979 }
1980
1981 #[doc(hidden)]
1985 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
1986 *self.spill_hook.lock() = Some(Box::new(f));
1987 }
1988
1989 #[doc(hidden)]
1993 pub fn __wal_group_sync_count(&self) -> u64 {
1994 self.shared_wal.lock().group_sync_count()
1995 }
1996
1997 #[doc(hidden)]
2000 pub fn __poison(&self) {
2001 self.poisoned
2002 .store(true, std::sync::atomic::Ordering::Relaxed);
2003 }
2004
2005 pub fn check(&self) -> Vec<CheckIssue> {
2018 let mut issues = Vec::new();
2019 let cat = self.catalog.read();
2020 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
2021 for entry in &cat.tables {
2022 if !matches!(entry.state, TableState::Live) {
2023 continue;
2024 }
2025 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
2026 let mut err = |sev: &str, desc: String| {
2027 issues.push(CheckIssue {
2028 table_id: entry.table_id,
2029 table_name: entry.name.clone(),
2030 severity: sev.into(),
2031 description: desc,
2032 });
2033 };
2034 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
2035 Ok(m) => m,
2036 Err(e) => {
2037 err("error", format!("manifest read failed: {e}"));
2038 continue;
2039 }
2040 };
2041 if m.flushed_epoch > m.current_epoch {
2042 err(
2043 "error",
2044 format!(
2045 "flushed_epoch {} exceeds current_epoch {} (impossible)",
2046 m.flushed_epoch, m.current_epoch
2047 ),
2048 );
2049 }
2050
2051 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
2052 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
2053 for rr in &m.runs {
2054 referenced.insert(rr.run_id);
2055 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
2056 if !run_path.exists() {
2057 err("error", format!("missing run file: r-{}.sr", rr.run_id));
2058 continue;
2059 }
2060 match crate::sorted_run::RunReader::open(
2061 &run_path,
2062 entry.schema.clone(),
2063 self.kek.clone(),
2064 ) {
2065 Ok(reader) => {
2066 if reader.row_count() as u64 != rr.row_count {
2067 err(
2068 "error",
2069 format!(
2070 "run r-{} row count mismatch: manifest {} vs run {}",
2071 rr.run_id,
2072 rr.row_count,
2073 reader.row_count()
2074 ),
2075 );
2076 }
2077 }
2078 Err(e) => {
2079 err(
2080 "error",
2081 format!("run r-{} integrity check failed: {e}", rr.run_id),
2082 );
2083 }
2084 }
2085 }
2086
2087 for r in &m.retiring {
2091 referenced.insert(r.run_id);
2092 }
2093
2094 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
2096 for ent in rd.flatten() {
2097 let p = ent.path();
2098 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
2099 continue;
2100 }
2101 let run_id = p
2102 .file_stem()
2103 .and_then(|s| s.to_str())
2104 .and_then(|s| s.strip_prefix("r-"))
2105 .and_then(|s| s.parse::<u128>().ok());
2106 if let Some(id) = run_id {
2107 if !referenced.contains(&id) {
2108 err(
2109 "warning",
2110 format!("orphan run file r-{id}.sr not referenced by the manifest"),
2111 );
2112 }
2113 }
2114 }
2115 }
2116 }
2117
2118 for (seg, msg) in self.shared_wal.lock().verify_segments() {
2125 issues.push(CheckIssue {
2126 table_id: WAL_TABLE_ID,
2127 table_name: "<wal>".into(),
2128 severity: "error".into(),
2129 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
2130 });
2131 }
2132 issues
2133 }
2134
2135 pub fn doctor(&self) -> Result<Vec<u64>> {
2139 let _ddl = self.ddl_lock.lock();
2142 let issues = self.check();
2143 let bad_tables: std::collections::HashSet<u64> = issues
2148 .iter()
2149 .filter(|i| i.severity == "error" && i.table_id != WAL_TABLE_ID)
2150 .map(|i| i.table_id)
2151 .collect();
2152 if bad_tables.is_empty() {
2153 return Ok(Vec::new());
2154 }
2155
2156 let qdir = self.root.join("_quarantine");
2157 std::fs::create_dir_all(&qdir)?;
2158 let mut quarantined = Vec::new();
2159 for &table_id in &bad_tables {
2160 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
2161 if tdir.exists() {
2162 let dest = qdir.join(table_id.to_string());
2163 std::fs::rename(&tdir, &dest)?;
2164 }
2165 {
2166 let mut cat = self.catalog.write();
2167 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
2168 entry.state = TableState::Dropped {
2169 at_epoch: self.epoch.visible().0,
2170 };
2171 }
2172 }
2173 self.tables.write().remove(&table_id);
2175 quarantined.push(table_id);
2176 }
2177 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2178 Ok(quarantined)
2179 }
2180
2181 #[allow(dead_code)]
2183 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
2184 self.kek.as_ref()
2185 }
2186
2187 #[allow(dead_code)]
2189 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
2190 &self.epoch
2191 }
2192
2193 #[allow(dead_code)]
2195 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
2196 &self.snapshots
2197 }
2198}
2199
2200fn recover_shared_wal(
2209 root: &Path,
2210 tables: &HashMap<u64, TableHandle>,
2211 epoch: &EpochAuthority,
2212 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
2213) -> Result<()> {
2214 use crate::memtable::Row;
2215 use crate::rowid::RowId;
2216 use crate::wal::{Op, SharedWal};
2217
2218 let records = SharedWal::replay_with_dek(root, wal_dek)?;
2219
2220 let mut committed: HashMap<u64, u64> = HashMap::new();
2222 let mut spilled_to_link: Vec<(
2223 u64, u64, Vec<crate::wal::AddedRun>,
2226 )> = Vec::new();
2227 for r in &records {
2228 if let Op::TxnCommit {
2229 epoch: ce,
2230 ref added_runs,
2231 } = r.op
2232 {
2233 committed.insert(r.txn_id, ce);
2234 if !added_runs.is_empty() {
2235 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
2236 }
2237 }
2238 }
2239
2240 type TableStage = (Vec<Row>, Vec<(RowId, Epoch)>, Option<Epoch>, Epoch);
2242 let mut stage: HashMap<u64, TableStage> = HashMap::new();
2243 let mut max_epoch = epoch.visible().0;
2244 for r in records {
2245 let Some(&ce) = committed.get(&r.txn_id) else {
2246 continue; };
2248 let commit_epoch = Epoch(ce);
2249 max_epoch = max_epoch.max(ce);
2250 match r.op {
2251 Op::Put { table_id, rows } => {
2252 let skip = tables
2254 .get(&table_id)
2255 .map(|h| h.lock().flushed_epoch() >= ce)
2256 .unwrap_or(true);
2257 if skip {
2258 continue;
2259 }
2260 let rows: Vec<Row> = match bincode::deserialize(&rows) {
2261 Ok(v) => v,
2262 Err(_) => continue,
2263 };
2264 let rows: Vec<Row> = rows
2267 .into_iter()
2268 .map(|mut row| {
2269 row.committed_epoch = commit_epoch;
2270 row
2271 })
2272 .collect();
2273 let entry = stage
2274 .entry(table_id)
2275 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
2276 entry.0.extend(rows);
2277 entry.3 = commit_epoch;
2278 }
2279 Op::Delete { table_id, row_ids } => {
2280 let skip = tables
2281 .get(&table_id)
2282 .map(|h| h.lock().flushed_epoch() >= ce)
2283 .unwrap_or(true);
2284 if skip {
2285 continue;
2286 }
2287 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
2288 let entry = stage
2289 .entry(table_id)
2290 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
2291 entry.1.extend(dels);
2292 entry.3 = commit_epoch;
2293 }
2294 Op::TruncateTable { table_id } => {
2295 let skip = tables
2296 .get(&table_id)
2297 .map(|h| h.lock().flushed_epoch() >= ce)
2298 .unwrap_or(true);
2299 if skip {
2300 continue;
2301 }
2302 stage.insert(
2303 table_id,
2304 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
2305 );
2306 }
2307 _ => {}
2308 }
2309 }
2310 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
2311 let Some(handle) = tables.get(&table_id) else {
2312 continue;
2313 };
2314 let mut t = handle.lock();
2315 if let Some(epoch) = truncate_epoch {
2316 t.apply_truncate(epoch)?;
2317 }
2318 t.recover_apply(rows, deletes)?;
2319 if truncate_epoch.is_some() {
2320 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
2321 t.live_count = rows.len() as u64;
2322 t.persist_manifest(table_epoch)?;
2323 }
2324 }
2325
2326 for (txn_id, ce, added_runs) in &spilled_to_link {
2330 for ar in added_runs {
2331 let Some(handle) = tables.get(&ar.table_id) else {
2332 continue;
2333 };
2334 let mut t = handle.lock();
2335 let dest = t.run_path(ar.run_id as u64);
2336 if !dest.exists() {
2337 let pending = root
2338 .join(TABLES_DIR)
2339 .join(ar.table_id.to_string())
2340 .join("_txn")
2341 .join(txn_id.to_string())
2342 .join(format!("r-{}.sr", ar.run_id));
2343 if pending.exists() {
2344 if let Some(parent) = pending.parent() {
2345 std::fs::rename(&pending, &dest)?;
2346 let _ = std::fs::remove_dir_all(parent);
2347 }
2348 }
2349 }
2350 if t.run_path(ar.run_id as u64).exists() {
2356 t.recover_spilled_run(crate::manifest::RunRef {
2357 run_id: ar.run_id,
2358 level: ar.level,
2359 epoch_created: *ce,
2360 row_count: ar.row_count,
2361 });
2362 }
2363 }
2364 }
2365
2366 epoch.advance_recovered(Epoch(max_epoch));
2367 Ok(())
2368}
2369
2370fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
2371 match condition {
2372 ProcedureCondition::Pk { .. } => {
2373 if schema.primary_key().is_none() {
2374 return Err(MongrelError::InvalidArgument(
2375 "procedure condition Pk references a table without a primary key".into(),
2376 ));
2377 }
2378 }
2379 ProcedureCondition::BitmapEq { column_id, .. }
2380 | ProcedureCondition::BitmapIn { column_id, .. }
2381 | ProcedureCondition::Range { column_id, .. }
2382 | ProcedureCondition::RangeF64 { column_id, .. }
2383 | ProcedureCondition::IsNull { column_id }
2384 | ProcedureCondition::IsNotNull { column_id }
2385 | ProcedureCondition::FmContains { column_id, .. } => {
2386 validate_column_id(*column_id, schema)?;
2387 }
2388 }
2389 Ok(())
2390}
2391
2392fn bind_procedure_args(
2393 procedure: &StoredProcedure,
2394 mut args: HashMap<String, crate::Value>,
2395) -> Result<HashMap<String, crate::Value>> {
2396 let mut out = HashMap::new();
2397 for param in &procedure.params {
2398 let value = match args.remove(¶m.name) {
2399 Some(value) => value,
2400 None => param.default.clone().ok_or_else(|| {
2401 MongrelError::InvalidArgument(format!(
2402 "missing required procedure parameter {:?}",
2403 param.name
2404 ))
2405 })?,
2406 };
2407 if !param.nullable && matches!(value, crate::Value::Null) {
2408 return Err(MongrelError::InvalidArgument(format!(
2409 "procedure parameter {:?} must not be NULL",
2410 param.name
2411 )));
2412 }
2413 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty) {
2414 return Err(MongrelError::InvalidArgument(format!(
2415 "procedure parameter {:?} has wrong type",
2416 param.name
2417 )));
2418 }
2419 out.insert(param.name.clone(), value);
2420 }
2421 if let Some(extra) = args.keys().next() {
2422 return Err(MongrelError::InvalidArgument(format!(
2423 "unknown procedure parameter {extra:?}"
2424 )));
2425 }
2426 Ok(out)
2427}
2428
2429fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
2430 matches!(
2431 (value, ty),
2432 (crate::Value::Bool(_), crate::TypeId::Bool)
2433 | (crate::Value::Int64(_), crate::TypeId::Int8)
2434 | (crate::Value::Int64(_), crate::TypeId::Int16)
2435 | (crate::Value::Int64(_), crate::TypeId::Int32)
2436 | (crate::Value::Int64(_), crate::TypeId::Int64)
2437 | (crate::Value::Int64(_), crate::TypeId::UInt8)
2438 | (crate::Value::Int64(_), crate::TypeId::UInt16)
2439 | (crate::Value::Int64(_), crate::TypeId::UInt32)
2440 | (crate::Value::Int64(_), crate::TypeId::UInt64)
2441 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
2442 | (crate::Value::Int64(_), crate::TypeId::Date32)
2443 | (crate::Value::Float64(_), crate::TypeId::Float32)
2444 | (crate::Value::Float64(_), crate::TypeId::Float64)
2445 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
2446 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
2447 )
2448}
2449
2450fn eval_cells(
2451 cells: &[crate::procedure::ProcedureCell],
2452 args: &HashMap<String, crate::Value>,
2453 outputs: &HashMap<String, ProcedureCallOutput>,
2454) -> Result<Vec<(u16, crate::Value)>> {
2455 cells
2456 .iter()
2457 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
2458 .collect()
2459}
2460
2461fn eval_condition(
2462 condition: &ProcedureCondition,
2463 args: &HashMap<String, crate::Value>,
2464 outputs: &HashMap<String, ProcedureCallOutput>,
2465) -> Result<crate::Condition> {
2466 Ok(match condition {
2467 ProcedureCondition::Pk { value } => {
2468 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
2469 }
2470 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
2471 column_id: *column_id,
2472 value: eval_value(value, args, outputs)?.encode_key(),
2473 },
2474 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
2475 column_id: *column_id,
2476 values: values
2477 .iter()
2478 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
2479 .collect::<Result<Vec<_>>>()?,
2480 },
2481 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
2482 column_id: *column_id,
2483 lo: expect_i64(eval_value(lo, args, outputs)?)?,
2484 hi: expect_i64(eval_value(hi, args, outputs)?)?,
2485 },
2486 ProcedureCondition::RangeF64 {
2487 column_id,
2488 lo,
2489 lo_inclusive,
2490 hi,
2491 hi_inclusive,
2492 } => crate::Condition::RangeF64 {
2493 column_id: *column_id,
2494 lo: expect_f64(eval_value(lo, args, outputs)?)?,
2495 lo_inclusive: *lo_inclusive,
2496 hi: expect_f64(eval_value(hi, args, outputs)?)?,
2497 hi_inclusive: *hi_inclusive,
2498 },
2499 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
2500 column_id: *column_id,
2501 },
2502 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
2503 column_id: *column_id,
2504 },
2505 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
2506 column_id: *column_id,
2507 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
2508 },
2509 })
2510}
2511
2512fn eval_value(
2513 value: &ProcedureValue,
2514 args: &HashMap<String, crate::Value>,
2515 outputs: &HashMap<String, ProcedureCallOutput>,
2516) -> Result<crate::Value> {
2517 match value {
2518 ProcedureValue::Literal(value) => Ok(value.clone()),
2519 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
2520 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
2521 }),
2522 ProcedureValue::StepScalar(id) => match outputs.get(id) {
2523 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
2524 _ => Err(MongrelError::InvalidArgument(format!(
2525 "procedure step {id:?} did not return a scalar"
2526 ))),
2527 },
2528 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
2529 Err(MongrelError::InvalidArgument(
2530 "row-valued procedure reference cannot be used as a scalar".into(),
2531 ))
2532 }
2533 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
2534 "structured procedure value cannot be used as a scalar cell".into(),
2535 )),
2536 }
2537}
2538
2539fn eval_return_output(
2540 value: &ProcedureValue,
2541 args: &HashMap<String, crate::Value>,
2542 outputs: &HashMap<String, ProcedureCallOutput>,
2543) -> Result<ProcedureCallOutput> {
2544 match value {
2545 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
2546 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
2547 args.get(name).cloned().ok_or_else(|| {
2548 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
2549 })?,
2550 )),
2551 ProcedureValue::StepRows(id)
2552 | ProcedureValue::StepRow(id)
2553 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
2554 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
2555 }),
2556 ProcedureValue::Object(fields) => {
2557 let mut out = Vec::with_capacity(fields.len());
2558 for (name, value) in fields {
2559 out.push((name.clone(), eval_return_output(value, args, outputs)?));
2560 }
2561 Ok(ProcedureCallOutput::Object(out))
2562 }
2563 ProcedureValue::Array(values) => {
2564 let mut out = Vec::with_capacity(values.len());
2565 for value in values {
2566 out.push(eval_return_output(value, args, outputs)?);
2567 }
2568 Ok(ProcedureCallOutput::Array(out))
2569 }
2570 }
2571}
2572
2573fn expect_i64(value: crate::Value) -> Result<i64> {
2574 match value {
2575 crate::Value::Int64(value) => Ok(value),
2576 _ => Err(MongrelError::InvalidArgument(
2577 "procedure value must be Int64".into(),
2578 )),
2579 }
2580}
2581
2582fn expect_f64(value: crate::Value) -> Result<f64> {
2583 match value {
2584 crate::Value::Float64(value) => Ok(value),
2585 _ => Err(MongrelError::InvalidArgument(
2586 "procedure value must be Float64".into(),
2587 )),
2588 }
2589}
2590
2591fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
2592 match value {
2593 crate::Value::Bytes(value) => Ok(value),
2594 _ => Err(MongrelError::InvalidArgument(
2595 "procedure value must be Bytes".into(),
2596 )),
2597 }
2598}
2599
2600fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
2601 if schema.columns.iter().any(|c| c.id == column_id) {
2602 Ok(())
2603 } else {
2604 Err(MongrelError::InvalidArgument(format!(
2605 "unknown column id {column_id}"
2606 )))
2607 }
2608}
2609
2610fn recover_ddl_from_wal(
2616 root: &Path,
2617 cat: &mut Catalog,
2618 meta_dek: Option<&[u8; META_DEK_LEN]>,
2619 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
2620) -> Result<()> {
2621 use crate::wal::{DdlOp, Op, SharedWal};
2622
2623 let records = match SharedWal::replay_with_dek(root, wal_dek) {
2624 Ok(r) => r,
2625 Err(_) => return Ok(()),
2626 };
2627
2628 let mut committed: HashMap<u64, u64> = HashMap::new();
2629 for r in &records {
2630 if let Op::TxnCommit { epoch: ce, .. } = r.op {
2631 committed.insert(r.txn_id, ce);
2632 }
2633 }
2634
2635 let mut changed = false;
2636 for r in records {
2637 let Some(&ce) = committed.get(&r.txn_id) else {
2638 continue;
2639 };
2640 match r.op {
2641 Op::Ddl(DdlOp::CreateTable {
2642 table_id,
2643 ref name,
2644 ref schema_json,
2645 }) => {
2646 if cat.tables.iter().any(|t| t.table_id == table_id) {
2647 continue;
2648 }
2649 let schema = DdlOp::decode_schema(schema_json)?;
2650 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
2651 if !tdir.exists() {
2652 std::fs::create_dir_all(tdir.join(crate::engine::WAL_DIR))?;
2653 std::fs::create_dir_all(tdir.join(crate::engine::RUNS_DIR))?;
2654 crate::engine::write_schema(&tdir, &schema)?;
2655 let mut m = crate::manifest::Manifest::new(table_id, schema.schema_id);
2661 crate::manifest::write_atomic(&tdir, &mut m, meta_dek)?;
2662 }
2663 cat.tables.push(CatalogEntry {
2664 table_id,
2665 name: name.clone(),
2666 schema,
2667 state: TableState::Live,
2668 created_epoch: ce,
2669 });
2670 cat.next_table_id = cat.next_table_id.max(table_id + 1);
2671 changed = true;
2672 }
2673 Op::Ddl(DdlOp::DropTable { table_id }) => {
2674 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
2675 if matches!(entry.state, TableState::Live) {
2676 entry.state = TableState::Dropped { at_epoch: ce };
2677 changed = true;
2678 }
2679 }
2680 }
2681 Op::Ddl(DdlOp::RenameTable {
2682 table_id,
2683 ref new_name,
2684 }) => {
2685 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
2686 if entry.name != *new_name {
2687 entry.name = new_name.clone();
2688 changed = true;
2689 }
2690 }
2691 }
2695 Op::Ddl(DdlOp::AlterTable {
2696 table_id,
2697 ref column_json,
2698 }) => {
2699 let column = DdlOp::decode_column(column_json)?;
2700 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
2701 if apply_recovered_column_def(&mut entry.schema, column) {
2702 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
2703 if tdir.exists() {
2704 crate::engine::write_schema(&tdir, &entry.schema)?;
2705 }
2706 changed = true;
2707 }
2708 }
2709 }
2710 _ => {}
2711 }
2712 }
2713
2714 if changed {
2715 catalog::write_atomic(root, cat, meta_dek)?;
2716 }
2717 Ok(())
2718}
2719
2720fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> bool {
2721 match schema.columns.iter_mut().find(|c| c.id == column.id) {
2722 Some(existing) if *existing == column => false,
2723 Some(existing) => {
2724 *existing = column;
2725 schema.schema_id = schema.schema_id.saturating_add(1);
2726 true
2727 }
2728 None => {
2729 schema.columns.push(column);
2730 schema.schema_id = schema.schema_id.saturating_add(1);
2731 true
2732 }
2733 }
2734}
2735
2736fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
2741 for entry in &cat.tables {
2742 let txn_dir = root
2743 .join(TABLES_DIR)
2744 .join(entry.table_id.to_string())
2745 .join("_txn");
2746 if txn_dir.exists() {
2747 let _ = std::fs::remove_dir_all(&txn_dir);
2748 }
2749 }
2750}