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::external_table::ExternalTableEntry;
14use crate::memtable::Value;
15use crate::procedure::{
16 ProcedureCallOutput, ProcedureCallResult, ProcedureCallRow, ProcedureCondition, ProcedureEntry,
17 ProcedureStep, ProcedureValue, StoredProcedure,
18};
19use crate::retention::{OwnedSnapshotGuard, SnapshotGuard, SnapshotRegistry};
20use crate::rowid::RowId;
21use crate::schema::{AlterColumn, ColumnDef, Schema};
22use crate::trigger::{
23 StoredTrigger, TriggerCondition, TriggerConfig, TriggerEntry, TriggerEvent, TriggerExpr,
24 TriggerRaiseAction, TriggerStep, TriggerTarget, TriggerTiming, TriggerValue,
25};
26use parking_lot::{Mutex, RwLock};
27use std::collections::HashMap;
28use std::io::Write;
29use std::path::{Path, PathBuf};
30use std::sync::atomic::{AtomicBool, AtomicU32};
31use std::sync::Arc;
32
33pub const TABLES_DIR: &str = "tables";
34pub const VTAB_DIR: &str = "_vtab";
35pub const META_DIR: &str = "_meta";
36pub const KEYS_FILENAME: &str = "keys";
37
38pub const WAL_TABLE_ID: u64 = u64::MAX;
42pub const EXTERNAL_TABLE_ID: u64 = u64::MAX - 1;
45
46#[derive(Debug, Clone)]
47pub enum ExternalTriggerWrite {
48 Insert {
49 table: String,
50 cells: Vec<(u16, Value)>,
51 },
52 UpdateByPk {
53 table: String,
54 pk: Value,
55 cells: Vec<(u16, Value)>,
56 },
57 DeleteByPk {
58 table: String,
59 pk: Value,
60 },
61}
62
63impl ExternalTriggerWrite {
64 fn table(&self) -> &str {
65 match self {
66 Self::Insert { table, .. }
67 | Self::UpdateByPk { table, .. }
68 | Self::DeleteByPk { table, .. } => table,
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq)]
74pub enum ExternalTriggerBaseWrite {
75 Put {
76 table: String,
77 cells: Vec<(u16, Value)>,
78 },
79 Delete {
80 table: String,
81 row_id: RowId,
82 },
83}
84
85#[derive(Debug, Clone, PartialEq)]
86pub struct ExternalTriggerWriteResult {
87 pub state: Vec<u8>,
88 pub base_writes: Vec<ExternalTriggerBaseWrite>,
89}
90
91impl ExternalTriggerWriteResult {
92 pub fn new(state: Vec<u8>) -> Self {
93 Self {
94 state,
95 base_writes: Vec::new(),
96 }
97 }
98}
99
100pub trait ExternalTriggerBridge {
101 fn apply_trigger_external_write(
102 &self,
103 entry: &ExternalTableEntry,
104 base_state: Vec<u8>,
105 op: ExternalTriggerWrite,
106 ) -> Result<ExternalTriggerWriteResult>;
107}
108
109struct SpilledRun {
111 table_id: u64,
112 run_id: u128,
113 pending_path: PathBuf,
114 rows: Vec<crate::memtable::Row>,
115 row_count: u64,
116 min_rid: u64,
117 max_rid: u64,
118}
119
120#[derive(Debug, Clone)]
121struct TriggerRowImage {
122 columns: HashMap<u16, Value>,
123}
124
125impl TriggerRowImage {
126 fn from_row(row: crate::memtable::Row) -> Self {
127 Self {
128 columns: row.columns,
129 }
130 }
131
132 fn from_cells(cells: &[(u16, Value)]) -> Self {
133 Self {
134 columns: cells.iter().cloned().collect(),
135 }
136 }
137}
138
139#[derive(Debug, Clone)]
140struct WriteEvent {
141 table: String,
142 kind: TriggerEvent,
143 old: Option<TriggerRowImage>,
144 new: Option<TriggerRowImage>,
145 changed_columns: Vec<u16>,
146 op_indices: Vec<usize>,
147 put_idx: Option<usize>,
148 trigger_stack: Vec<String>,
149}
150
151#[derive(Default)]
152struct TriggerExpansion {
153 before: Vec<(u64, crate::txn::Staged)>,
154 before_stacks: Vec<Vec<String>>,
155 before_external: Vec<ExternalTriggerWrite>,
156 after: Vec<(u64, crate::txn::Staged)>,
157 after_stacks: Vec<Vec<String>>,
158 after_external: Vec<ExternalTriggerWrite>,
159 ignored_indices: std::collections::BTreeSet<usize>,
160}
161
162struct TriggerProgramOutput<'a> {
163 added: &'a mut Vec<(u64, crate::txn::Staged)>,
164 added_stacks: &'a mut Vec<Vec<String>>,
165 added_external: &'a mut Vec<ExternalTriggerWrite>,
166 ignored_indices: &'a mut std::collections::BTreeSet<usize>,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170enum TriggerProgramOutcome {
171 Continue,
172 Ignore,
173}
174
175#[derive(Debug, Clone)]
177pub struct CheckIssue {
178 pub table_id: u64,
179 pub table_name: String,
180 pub severity: String,
181 pub description: String,
182}
183
184pub type TableHandle = Arc<Mutex<Table>>;
188
189pub struct Database {
192 root: PathBuf,
193 catalog: RwLock<Catalog>,
194 epoch: Arc<EpochAuthority>,
195 snapshots: Arc<SnapshotRegistry>,
196 page_cache: Arc<crate::cache::Sharded<crate::cache::PageCache>>,
197 decoded_cache: Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>,
198 commit_lock: Arc<Mutex<()>>,
199 shared_wal: Arc<Mutex<crate::wal::SharedWal>>,
204 next_txn_id: Arc<Mutex<u64>>,
208 tables: RwLock<HashMap<u64, TableHandle>>,
209 kek: Option<Arc<crate::encryption::Kek>>,
210 ddl_lock: Mutex<()>,
213 meta_dek: Option<[u8; META_DEK_LEN]>,
214 spill_threshold: std::sync::atomic::AtomicU64,
217 conflicts: crate::txn::ConflictIndex,
220 active_txns: crate::txn::ActiveTxns,
223 poisoned: Arc<std::sync::atomic::AtomicBool>,
226 group: Arc<crate::txn::GroupCommit>,
230 active_spills: Arc<crate::retention::ActiveSpills>,
233 #[doc(hidden)]
237 spill_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
238 trigger_recursive: AtomicBool,
239 trigger_max_depth: AtomicU32,
240 trigger_max_loop_iterations: AtomicU32,
241 _lock: Option<std::fs::File>,
244 notify: tokio::sync::broadcast::Sender<ChangeEvent>,
249 principal: RwLock<Option<crate::auth::Principal>>,
259 auth_state: crate::auth_state::AuthState,
265}
266
267struct EpochGuard<'a> {
280 authority: &'a EpochAuthority,
281 epoch: Epoch,
282 armed: bool,
283}
284
285impl<'a> EpochGuard<'a> {
286 fn new(authority: &'a EpochAuthority, epoch: Epoch) -> Self {
287 Self {
288 authority,
289 epoch,
290 armed: true,
291 }
292 }
293
294 fn disarm(&mut self) {
297 self.armed = false;
298 }
299}
300
301impl Drop for EpochGuard<'_> {
302 fn drop(&mut self) {
303 if self.armed {
304 self.authority.abandon(self.epoch);
305 }
306 }
307}
308
309#[derive(Debug, Clone, serde::Serialize)]
311pub struct ChangeEvent {
312 pub channel: String,
313 pub table: String,
314 pub op: String,
315 pub epoch: u64,
316 pub message: Option<String>,
317}
318
319impl std::fmt::Debug for Database {
326 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327 let cat = self.catalog.read();
328 let principal_guard = self.principal.read();
329 let principal: &str = principal_guard
330 .as_ref()
331 .map(|p| p.username.as_str())
332 .unwrap_or("<none>");
333 f.debug_struct("Database")
334 .field("root", &self.root)
335 .field("db_epoch", &cat.db_epoch)
336 .field("open_generation", &cat.open_generation)
337 .field("tables", &cat.tables.len())
338 .field("visible_epoch", &self.epoch.visible().0)
339 .field("encrypted", &self.kek.is_some())
340 .field("require_auth", &cat.require_auth)
341 .field("principal", &principal)
342 .finish()
343 }
344}
345
346impl Database {
347 pub fn create(root: impl AsRef<Path>) -> Result<Self> {
349 Self::create_inner(root, None)
350 }
351
352 #[cfg(feature = "encryption")]
355 pub fn create_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
356 let root = root.as_ref();
357 std::fs::create_dir_all(root)?;
358 std::fs::create_dir_all(root.join(META_DIR))?;
359 let salt = crate::encryption::random_salt();
360 std::fs::write(root.join(META_DIR).join(KEYS_FILENAME), salt)?;
361 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
362 Self::create_inner(root, Some(kek))
363 }
364
365 #[cfg(feature = "encryption")]
368 pub fn create_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
369 let root = root.as_ref();
370 std::fs::create_dir_all(root)?;
371 std::fs::create_dir_all(root.join(META_DIR))?;
372 let salt = crate::encryption::random_salt();
373 std::fs::write(root.join(META_DIR).join(KEYS_FILENAME), salt)?;
374 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
375 Self::create_inner(root, Some(kek))
376 }
377
378 fn create_inner(
379 root: impl AsRef<Path>,
380 kek: Option<Arc<crate::encryption::Kek>>,
381 ) -> Result<Self> {
382 let root = root.as_ref().to_path_buf();
383 std::fs::create_dir_all(&root)?;
384 std::fs::create_dir_all(root.join(TABLES_DIR))?;
385 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
386 let cat = Catalog::empty();
387 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
388 Self::finish_open(root, cat, kek, meta_dek, false, None)
389 }
390
391 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
393 Self::open_inner(root, None, None)
394 }
395
396 #[cfg(feature = "encryption")]
398 pub fn open_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
399 let root = root.as_ref();
400 let salt_bytes = std::fs::read(root.join(META_DIR).join(KEYS_FILENAME))
401 .map_err(|e| MongrelError::NotFound(format!("encryption salt file: {e}")))?;
402 let mut salt = [0u8; crate::encryption::SALT_LEN];
403 salt.copy_from_slice(&salt_bytes);
404 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
405 Self::open_inner(root, Some(kek), None)
406 }
407
408 #[cfg(feature = "encryption")]
410 pub fn open_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
411 let root = root.as_ref();
412 let salt_path = root.join(META_DIR).join(KEYS_FILENAME);
413 let salt_bytes = std::fs::read(&salt_path).map_err(|e| {
414 MongrelError::NotFound(format!(
415 "encryption salt file {:?}: {e} (database not encrypted, or corrupted)",
416 salt_path
417 ))
418 })?;
419 if salt_bytes.len() != crate::encryption::SALT_LEN {
420 return Err(MongrelError::InvalidArgument(format!(
421 "salt file is {} bytes, expected {}",
422 salt_bytes.len(),
423 crate::encryption::SALT_LEN
424 )));
425 }
426 let mut salt = [0u8; crate::encryption::SALT_LEN];
427 salt.copy_from_slice(&salt_bytes);
428 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
429 Self::open_inner(root, Some(kek), None)
430 }
431
432 pub fn open_with_credentials(
444 root: impl AsRef<Path>,
445 username: &str,
446 password: &str,
447 ) -> Result<Self> {
448 Self::open_inner_with_credentials(root, None, username, password)
449 }
450
451 #[cfg(feature = "encryption")]
454 pub fn open_encrypted_with_credentials(
455 root: impl AsRef<Path>,
456 passphrase: &str,
457 username: &str,
458 password: &str,
459 ) -> Result<Self> {
460 let root = root.as_ref();
461 let salt_bytes = std::fs::read(root.join(META_DIR).join(KEYS_FILENAME))
462 .map_err(|e| MongrelError::NotFound(format!("encryption salt file: {e}")))?;
463 let mut salt = [0u8; crate::encryption::SALT_LEN];
464 salt.copy_from_slice(&salt_bytes);
465 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
466 Self::open_inner_with_credentials(root, Some(kek), username, password)
467 }
468
469 fn open_inner_with_credentials(
476 root: impl AsRef<Path>,
477 kek: Option<Arc<crate::encryption::Kek>>,
478 username: &str,
479 password: &str,
480 ) -> Result<Self> {
481 let root = root.as_ref().to_path_buf();
482 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
483 let cat = catalog::read(&root, meta_dek.as_ref())?
484 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
485
486 if !cat.require_auth {
489 return Err(MongrelError::AuthNotRequired);
490 }
491
492 let user = cat
497 .users
498 .iter()
499 .find(|u| u.username == username)
500 .filter(|u| !u.password_hash.is_empty())
501 .ok_or_else(|| MongrelError::InvalidCredentials {
502 username: username.to_string(),
503 })?;
504 let password_ok = crate::auth::verify_password(password, &user.password_hash)
505 .map_err(MongrelError::Other)?;
506 if !password_ok {
507 return Err(MongrelError::InvalidCredentials {
508 username: username.to_string(),
509 });
510 }
511
512 let principal =
514 Self::resolve_principal_from_catalog(&cat, &user.username).ok_or_else(|| {
515 MongrelError::InvalidCredentials {
516 username: username.to_string(),
517 }
518 })?;
519
520 Self::finish_open(root, cat, kek, meta_dek, true, Some(principal))
521 }
522
523 pub fn create_with_credentials(
533 root: impl AsRef<Path>,
534 admin_username: &str,
535 admin_password: &str,
536 ) -> Result<Self> {
537 Self::create_inner_with_credentials(root, None, admin_username, admin_password)
538 }
539
540 #[cfg(feature = "encryption")]
544 pub fn create_encrypted_with_credentials(
545 root: impl AsRef<Path>,
546 passphrase: &str,
547 admin_username: &str,
548 admin_password: &str,
549 ) -> Result<Self> {
550 let root = root.as_ref();
551 std::fs::create_dir_all(root)?;
552 std::fs::create_dir_all(root.join(META_DIR))?;
553 let salt = crate::encryption::random_salt();
554 std::fs::write(root.join(META_DIR).join(KEYS_FILENAME), salt)?;
555 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
556 Self::create_inner_with_credentials(root, Some(kek), admin_username, admin_password)
557 }
558
559 fn create_inner_with_credentials(
560 root: impl AsRef<Path>,
561 kek: Option<Arc<crate::encryption::Kek>>,
562 admin_username: &str,
563 admin_password: &str,
564 ) -> Result<Self> {
565 let root = root.as_ref().to_path_buf();
566 std::fs::create_dir_all(&root)?;
567 std::fs::create_dir_all(root.join(TABLES_DIR))?;
568 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
569
570 let password_hash =
572 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
573 let mut cat = Catalog::empty();
574 cat.require_auth = true;
575 cat.next_user_id = 1;
576 cat.users.push(crate::auth::UserEntry {
577 id: 1,
578 username: admin_username.to_string(),
579 password_hash,
580 roles: Vec::new(),
581 is_admin: true,
582 created_epoch: 0,
583 });
584 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
585
586 let admin_principal = crate::auth::Principal {
589 username: admin_username.to_string(),
590 is_admin: true,
591 roles: Vec::new(),
592 permissions: Vec::new(),
593 };
594 Self::finish_open(root, cat, kek, meta_dek, false, Some(admin_principal))
595 }
596
597 fn open_inner(
598 root: impl AsRef<Path>,
599 kek: Option<Arc<crate::encryption::Kek>>,
600 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
601 ) -> Result<Self> {
602 let root = root.as_ref().to_path_buf();
603 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
604 let cat = catalog::read(&root, meta_dek.as_ref())?
605 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
606 Self::finish_open(root, cat, kek, meta_dek, true, None)
607 }
608
609 fn finish_open(
610 root: PathBuf,
611 cat: Catalog,
612 kek: Option<Arc<crate::encryption::Kek>>,
613 meta_dek: Option<[u8; META_DEK_LEN]>,
614 existing: bool,
615 principal: Option<crate::auth::Principal>,
616 ) -> Result<Self> {
617 std::fs::create_dir_all(root.join("_meta")).ok();
623 let lock_path = root.join("_meta").join(".lock");
624 let canonical = lock_path.canonicalize().unwrap_or(lock_path.clone());
625 let lock_file = {
626 static LOCKED_PATHS: std::sync::OnceLock<
627 std::sync::Mutex<std::collections::HashSet<PathBuf>>,
628 > = std::sync::OnceLock::new();
629 let locked = LOCKED_PATHS
630 .get_or_init(|| std::sync::Mutex::new(std::collections::HashSet::new()));
631 let mut guard = locked.lock().unwrap();
632 if guard.contains(&canonical) {
633 None
635 } else {
636 let f = std::fs::OpenOptions::new()
637 .create(true)
638 .truncate(false)
639 .write(true)
640 .open(&lock_path)?;
641 use fs2::FileExt;
642 f.try_lock_exclusive().map_err(|e| {
643 MongrelError::Io(std::io::Error::other(format!(
644 "database at {} is locked by another process: {e}",
645 root.display()
646 )))
647 })?;
648 guard.insert(canonical.clone());
649 Some(f)
650 }
651 };
652
653 let epoch = Arc::new(EpochAuthority::new(cat.db_epoch));
654 let snapshots = Arc::new(SnapshotRegistry::new());
655 let page_cache = Arc::new(crate::cache::Sharded::new(
656 crate::cache::CACHE_SHARDS,
657 || {
658 crate::cache::PageCache::new(
659 crate::engine::PAGE_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
660 )
661 },
662 ));
663 let decoded_cache = Arc::new(crate::cache::Sharded::new(
664 crate::cache::CACHE_SHARDS,
665 || {
666 crate::cache::DecodedPageCache::new(
667 crate::engine::DECODED_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
668 )
669 },
670 ));
671 let commit_lock = Arc::new(Mutex::new(()));
672 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
673 let shared_wal = Arc::new(Mutex::new(if existing {
674 crate::wal::SharedWal::open(&root, Epoch(cat.db_epoch), wal_dek.clone())?
675 } else {
676 crate::wal::SharedWal::create_with_dek(&root, Epoch(cat.db_epoch), wal_dek.clone())?
677 }));
678 let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));
682 let group = Arc::new(crate::txn::GroupCommit::new(
683 shared_wal.lock().durable_seq(),
684 ));
685 let txn_ids = Arc::new(Mutex::new(1u64));
689
690 let mut cat = cat;
696 if existing {
697 recover_ddl_from_wal(&root, &mut cat, meta_dek.as_ref(), wal_dek.as_ref())?;
698 }
699
700 let auth_state = crate::auth_state::AuthState::new(cat.require_auth, principal.clone());
705 let auth_checker: Option<Arc<dyn crate::auth_state::TableAuthChecker>> = Some(Arc::new(
706 crate::auth_state::DefaultTableAuthChecker::new(auth_state.clone()),
707 ));
708
709 let mut tables: HashMap<u64, TableHandle> = HashMap::new();
715 for entry in &cat.tables {
716 if !matches!(entry.state, TableState::Live) {
717 continue;
718 }
719 let tdir = root.join(TABLES_DIR).join(entry.table_id.to_string());
720 let ctx = SharedCtx {
721 epoch: Arc::clone(&epoch),
722 page_cache: Arc::clone(&page_cache),
723 decoded_cache: Arc::clone(&decoded_cache),
724 snapshots: Arc::clone(&snapshots),
725 kek: kek.clone(),
726 commit_lock: Arc::clone(&commit_lock),
727 shared: Some(crate::engine::SharedWalCtx {
728 wal: Arc::clone(&shared_wal),
729 group: Arc::clone(&group),
730 poisoned: Arc::clone(&poisoned),
731 txn_ids: Arc::clone(&txn_ids),
732 }),
733 table_name: Some(entry.name.clone()),
734 auth: auth_checker.clone(),
735 };
736 let t = Table::open_in(&tdir, ctx)?;
737 tables.insert(entry.table_id, Arc::new(Mutex::new(t)));
738 }
739
740 if existing {
746 recover_shared_wal(&root, &tables, &epoch, wal_dek.as_ref())?;
747 sweep_pending_txn_dirs(&root, &cat);
750 }
751
752 if existing {
756 cat.open_generation = cat.open_generation.wrapping_add(1);
757 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
758 }
759 let next_txn_id = (cat.open_generation << 32) | 1;
760 *txn_ids.lock() = next_txn_id;
762
763 if existing && cat.require_auth && principal.is_none() {
771 return Err(MongrelError::AuthRequired);
772 }
773
774 Ok(Self {
775 root,
776 catalog: RwLock::new(cat),
777 epoch,
778 snapshots,
779 page_cache,
780 decoded_cache,
781 commit_lock,
782 shared_wal,
783 next_txn_id: txn_ids,
784 tables: RwLock::new(tables),
785 kek,
786 ddl_lock: Mutex::new(()),
787 meta_dek,
788 conflicts: crate::txn::ConflictIndex::new(),
789 active_txns: crate::txn::ActiveTxns::new(),
790 poisoned,
791 group,
792 spill_threshold: std::sync::atomic::AtomicU64::new(64 * 1024 * 1024),
793 active_spills: Arc::new(crate::retention::ActiveSpills::new()),
794 spill_hook: Mutex::new(None),
795 trigger_recursive: AtomicBool::new(TriggerConfig::default().recursive_triggers),
796 trigger_max_depth: AtomicU32::new(TriggerConfig::default().max_depth),
797 trigger_max_loop_iterations: AtomicU32::new(
798 TriggerConfig::default().max_loop_iterations,
799 ),
800 _lock: lock_file,
801 notify: {
802 let (tx, _rx) = tokio::sync::broadcast::channel(256);
803 tx
804 },
805 principal: RwLock::new(principal),
806 auth_state,
807 })
808 }
809
810 pub fn visible_epoch(&self) -> Epoch {
812 self.epoch.visible()
813 }
814
815 pub fn catalog_snapshot(&self) -> Catalog {
817 self.catalog.read().clone()
818 }
819
820 pub fn root(&self) -> &Path {
822 &self.root
823 }
824
825 pub fn table_id(&self, name: &str) -> Result<u64> {
828 let cat = self.catalog.read();
829 cat.live(name)
830 .map(|e| e.table_id)
831 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
832 }
833
834 pub fn procedures(&self) -> Vec<StoredProcedure> {
835 self.catalog
836 .read()
837 .procedures
838 .iter()
839 .map(|p| p.procedure.clone())
840 .collect()
841 }
842
843 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
844 self.catalog
845 .read()
846 .procedures
847 .iter()
848 .find(|p| p.procedure.name == name)
849 .map(|p| p.procedure.clone())
850 }
851
852 pub fn create_procedure(&self, mut procedure: StoredProcedure) -> Result<StoredProcedure> {
853 self.require(&crate::auth::Permission::Ddl)?;
854 let _g = self.ddl_lock.lock();
855 procedure.validate()?;
856 self.validate_procedure_references(&procedure)?;
857 {
858 let cat = self.catalog.read();
859 if cat
860 .procedures
861 .iter()
862 .any(|p| p.procedure.name == procedure.name)
863 {
864 return Err(MongrelError::InvalidArgument(format!(
865 "procedure {:?} already exists",
866 procedure.name
867 )));
868 }
869 }
870 let commit_lock = Arc::clone(&self.commit_lock);
871 let _c = commit_lock.lock();
872 let epoch = self.epoch.bump_assigned();
873 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
874 procedure.created_epoch = epoch.0;
875 procedure.updated_epoch = epoch.0;
876 {
877 let mut cat = self.catalog.write();
878 cat.procedures.push(ProcedureEntry::from(procedure.clone()));
879 cat.db_epoch = epoch.0;
880 }
881 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
882 self.epoch.publish_in_order(epoch);
883 _epoch_guard.disarm();
884 Ok(procedure)
885 }
886
887 pub fn create_or_replace_procedure(
888 &self,
889 procedure: StoredProcedure,
890 ) -> Result<StoredProcedure> {
891 let _g = self.ddl_lock.lock();
892 procedure.validate()?;
893 self.validate_procedure_references(&procedure)?;
894 let commit_lock = Arc::clone(&self.commit_lock);
895 let _c = commit_lock.lock();
896 let epoch = self.epoch.bump_assigned();
897 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
898 let replaced = {
899 let mut cat = self.catalog.write();
900 let next = match cat
901 .procedures
902 .iter()
903 .position(|p| p.procedure.name == procedure.name)
904 {
905 Some(idx) => {
906 let next = cat.procedures[idx]
907 .procedure
908 .replaced(procedure.clone(), epoch.0)?;
909 cat.procedures[idx] = ProcedureEntry::from(next.clone());
910 next
911 }
912 None => {
913 let mut next = procedure;
914 next.created_epoch = epoch.0;
915 next.updated_epoch = epoch.0;
916 cat.procedures.push(ProcedureEntry::from(next.clone()));
917 next
918 }
919 };
920 cat.db_epoch = epoch.0;
921 next
922 };
923 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
924 self.epoch.publish_in_order(epoch);
925 _epoch_guard.disarm();
926 Ok(replaced)
927 }
928
929 pub fn drop_procedure(&self, name: &str) -> Result<()> {
930 self.require(&crate::auth::Permission::Ddl)?;
931 let _g = self.ddl_lock.lock();
932 let commit_lock = Arc::clone(&self.commit_lock);
933 let _c = commit_lock.lock();
934 let epoch = self.epoch.bump_assigned();
935 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
936 {
937 let mut cat = self.catalog.write();
938 let before = cat.procedures.len();
939 cat.procedures.retain(|p| p.procedure.name != name);
940 if cat.procedures.len() == before {
941 return Err(MongrelError::NotFound(format!(
942 "procedure {name:?} not found"
943 )));
944 }
945 cat.db_epoch = epoch.0;
946 }
947 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
948 self.epoch.publish_in_order(epoch);
949 _epoch_guard.disarm();
950 Ok(())
951 }
952
953 pub fn users(&self) -> Vec<crate::auth::UserEntry> {
958 self.catalog.read().users.clone()
959 }
960
961 pub fn roles(&self) -> Vec<crate::auth::RoleEntry> {
963 self.catalog.read().roles.clone()
964 }
965
966 pub fn create_user(&self, username: &str, password: &str) -> Result<crate::auth::UserEntry> {
968 self.require(&crate::auth::Permission::Admin)?;
969 let hash = crate::auth::hash_password(password).map_err(MongrelError::Other)?;
970 let epoch = self.epoch.bump_assigned();
971 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
972 let id = {
973 let mut cat = self.catalog.write();
974 if cat.users.iter().any(|u| u.username == username) {
975 return Err(MongrelError::InvalidArgument(format!(
976 "user {username:?} already exists"
977 )));
978 }
979 cat.next_user_id += 1;
980 let id = cat.next_user_id;
981 let entry = crate::auth::UserEntry {
982 id,
983 username: username.into(),
984 password_hash: hash,
985 roles: Vec::new(),
986 is_admin: false,
987 created_epoch: epoch.0,
988 };
989 cat.users.push(entry.clone());
990 cat.db_epoch = epoch.0;
991 entry
992 };
993 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
994 self.epoch.publish_in_order(epoch);
995 _epoch_guard.disarm();
996 Ok(id)
997 }
998
999 pub fn drop_user(&self, username: &str) -> Result<()> {
1001 self.require(&crate::auth::Permission::Admin)?;
1002 let epoch = self.epoch.bump_assigned();
1003 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
1004 {
1005 let mut cat = self.catalog.write();
1006 let before = cat.users.len();
1007 cat.users.retain(|u| u.username != username);
1008 if cat.users.len() == before {
1009 return Err(MongrelError::NotFound(format!(
1010 "user {username:?} not found"
1011 )));
1012 }
1013 cat.db_epoch = epoch.0;
1014 }
1015 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1016 self.epoch.publish_in_order(epoch);
1017 _epoch_guard.disarm();
1018 Ok(())
1019 }
1020
1021 pub fn alter_user_password(&self, username: &str, new_password: &str) -> Result<()> {
1023 self.require(&crate::auth::Permission::Admin)?;
1024 let hash = crate::auth::hash_password(new_password).map_err(MongrelError::Other)?;
1025 let epoch = self.epoch.bump_assigned();
1026 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
1027 {
1028 let mut cat = self.catalog.write();
1029 let user = cat
1030 .users
1031 .iter_mut()
1032 .find(|u| u.username == username)
1033 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
1034 user.password_hash = hash;
1035 cat.db_epoch = epoch.0;
1036 }
1037 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1038 self.epoch.publish_in_order(epoch);
1039 _epoch_guard.disarm();
1040 Ok(())
1041 }
1042
1043 pub fn verify_user(
1046 &self,
1047 username: &str,
1048 password: &str,
1049 ) -> Result<Option<crate::auth::UserEntry>> {
1050 let cat = self.catalog.read();
1051 let Some(user) = cat.users.iter().find(|u| u.username == username) else {
1052 return Ok(None);
1053 };
1054 if user.password_hash.is_empty() {
1055 return Ok(None);
1056 }
1057 let ok = crate::auth::verify_password(password, &user.password_hash)
1058 .map_err(MongrelError::Other)?;
1059 if ok {
1060 Ok(Some(user.clone()))
1061 } else {
1062 Ok(None)
1063 }
1064 }
1065
1066 pub fn set_user_admin(&self, username: &str, is_admin: bool) -> Result<()> {
1068 self.require(&crate::auth::Permission::Admin)?;
1069 let epoch = self.epoch.bump_assigned();
1070 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
1071 {
1072 let mut cat = self.catalog.write();
1073 let user = cat
1074 .users
1075 .iter_mut()
1076 .find(|u| u.username == username)
1077 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
1078 user.is_admin = is_admin;
1079 cat.db_epoch = epoch.0;
1080 }
1081 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1082 self.epoch.publish_in_order(epoch);
1083 _epoch_guard.disarm();
1084 Ok(())
1085 }
1086
1087 pub fn create_role(&self, name: &str) -> Result<crate::auth::RoleEntry> {
1089 self.require(&crate::auth::Permission::Admin)?;
1090 let epoch = self.epoch.bump_assigned();
1091 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
1092 let entry = {
1093 let mut cat = self.catalog.write();
1094 if cat.roles.iter().any(|r| r.name == name) {
1095 return Err(MongrelError::InvalidArgument(format!(
1096 "role {name:?} already exists"
1097 )));
1098 }
1099 let entry = crate::auth::RoleEntry {
1100 name: name.into(),
1101 permissions: Vec::new(),
1102 created_epoch: epoch.0,
1103 };
1104 cat.roles.push(entry.clone());
1105 cat.db_epoch = epoch.0;
1106 entry
1107 };
1108 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1109 self.epoch.publish_in_order(epoch);
1110 _epoch_guard.disarm();
1111 Ok(entry)
1112 }
1113
1114 pub fn drop_role(&self, name: &str) -> Result<()> {
1116 self.require(&crate::auth::Permission::Admin)?;
1117 let epoch = self.epoch.bump_assigned();
1118 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
1119 {
1120 let mut cat = self.catalog.write();
1121 let before = cat.roles.len();
1122 cat.roles.retain(|r| r.name != name);
1123 if cat.roles.len() == before {
1124 return Err(MongrelError::NotFound(format!("role {name:?} not found")));
1125 }
1126 for user in &mut cat.users {
1128 user.roles.retain(|r| r != name);
1129 }
1130 cat.db_epoch = epoch.0;
1131 }
1132 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1133 self.epoch.publish_in_order(epoch);
1134 _epoch_guard.disarm();
1135 Ok(())
1136 }
1137
1138 pub fn grant_role(&self, username: &str, role_name: &str) -> Result<()> {
1140 self.require(&crate::auth::Permission::Admin)?;
1141 let epoch = self.epoch.bump_assigned();
1142 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
1143 {
1144 let mut cat = self.catalog.write();
1145 if !cat.roles.iter().any(|r| r.name == role_name) {
1146 return Err(MongrelError::NotFound(format!(
1147 "role {role_name:?} not found"
1148 )));
1149 }
1150 let user = cat
1151 .users
1152 .iter_mut()
1153 .find(|u| u.username == username)
1154 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
1155 if !user.roles.contains(&role_name.to_string()) {
1156 user.roles.push(role_name.into());
1157 }
1158 cat.db_epoch = epoch.0;
1159 }
1160 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1161 self.epoch.publish_in_order(epoch);
1162 _epoch_guard.disarm();
1163 Ok(())
1164 }
1165
1166 pub fn revoke_role(&self, username: &str, role_name: &str) -> Result<()> {
1168 self.require(&crate::auth::Permission::Admin)?;
1169 let epoch = self.epoch.bump_assigned();
1170 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
1171 {
1172 let mut cat = self.catalog.write();
1173 let user = cat
1174 .users
1175 .iter_mut()
1176 .find(|u| u.username == username)
1177 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
1178 user.roles.retain(|r| r != role_name);
1179 cat.db_epoch = epoch.0;
1180 }
1181 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1182 self.epoch.publish_in_order(epoch);
1183 _epoch_guard.disarm();
1184 Ok(())
1185 }
1186
1187 pub fn grant_permission(
1189 &self,
1190 role_name: &str,
1191 permission: crate::auth::Permission,
1192 ) -> Result<()> {
1193 self.require(&crate::auth::Permission::Admin)?;
1194 let epoch = self.epoch.bump_assigned();
1195 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
1196 {
1197 let mut cat = self.catalog.write();
1198 let role = cat
1199 .roles
1200 .iter_mut()
1201 .find(|r| r.name == role_name)
1202 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
1203 if !role.permissions.contains(&permission) {
1204 role.permissions.push(permission);
1205 }
1206 cat.db_epoch = epoch.0;
1207 }
1208 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1209 self.epoch.publish_in_order(epoch);
1210 _epoch_guard.disarm();
1211 Ok(())
1212 }
1213
1214 pub fn revoke_permission(
1216 &self,
1217 role_name: &str,
1218 permission: crate::auth::Permission,
1219 ) -> Result<()> {
1220 self.require(&crate::auth::Permission::Admin)?;
1221 let epoch = self.epoch.bump_assigned();
1222 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
1223 {
1224 let mut cat = self.catalog.write();
1225 let role = cat
1226 .roles
1227 .iter_mut()
1228 .find(|r| r.name == role_name)
1229 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
1230 role.permissions.retain(|p| p != &permission);
1231 cat.db_epoch = epoch.0;
1232 }
1233 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1234 self.epoch.publish_in_order(epoch);
1235 _epoch_guard.disarm();
1236 Ok(())
1237 }
1238
1239 pub fn resolve_principal(&self, username: &str) -> Option<crate::auth::Principal> {
1242 let cat = self.catalog.read();
1243 Self::resolve_principal_from_catalog(&cat, username)
1244 }
1245
1246 fn resolve_principal_from_catalog(
1251 cat: &Catalog,
1252 username: &str,
1253 ) -> Option<crate::auth::Principal> {
1254 let user = cat.users.iter().find(|u| u.username == username)?;
1255 let mut permissions = Vec::new();
1256 for role_name in &user.roles {
1257 if let Some(role) = cat.roles.iter().find(|r| &r.name == role_name) {
1258 permissions.extend(role.permissions.iter().cloned());
1259 }
1260 }
1261 Some(crate::auth::Principal {
1262 username: user.username.clone(),
1263 is_admin: user.is_admin,
1264 roles: user.roles.clone(),
1265 permissions,
1266 })
1267 }
1268
1269 pub fn check_permission(&self, username: &str, permission: &crate::auth::Permission) -> bool {
1271 match self.resolve_principal(username) {
1272 Some(p) => p.has_permission(permission),
1273 None => false,
1274 }
1275 }
1276
1277 pub fn require_auth_enabled(&self) -> bool {
1281 self.catalog.read().require_auth
1282 }
1283
1284 pub fn principal(&self) -> Option<crate::auth::Principal> {
1288 self.principal.read().clone()
1289 }
1290
1291 fn table_auth_checker(&self) -> Option<Arc<dyn crate::auth_state::TableAuthChecker>> {
1297 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
1298 self.auth_state.clone(),
1299 )))
1300 }
1301
1302 pub fn refresh_principal(&self) -> Result<()> {
1315 let username = match self.principal.read().clone() {
1316 Some(p) => p.username,
1317 None => return Ok(()),
1318 };
1319 let cat = catalog::read(&self.root, self.meta_dek.as_ref())?
1323 .ok_or_else(|| MongrelError::NotFound("catalog vanished during refresh".into()))?;
1324 *self.catalog.write() = cat.clone();
1327 match Self::resolve_principal_from_catalog(&cat, &username) {
1328 Some(p) => {
1329 *self.principal.write() = Some(p.clone());
1330 self.auth_state.set_principal(Some(p));
1334 Ok(())
1335 }
1336 None => Err(MongrelError::InvalidCredentials { username }),
1337 }
1338 }
1339
1340 pub fn enable_auth(&self, admin_username: &str, admin_password: &str) -> Result<()> {
1352 let password_hash =
1353 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
1354 let epoch = self.epoch.bump_assigned();
1355 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
1356 {
1357 let mut cat = self.catalog.write();
1358 if cat.require_auth {
1359 return Err(MongrelError::InvalidArgument(
1360 "database already has require_auth enabled".into(),
1361 ));
1362 }
1363 if cat.users.iter().any(|u| u.username == admin_username) {
1366 return Err(MongrelError::InvalidArgument(format!(
1367 "user {admin_username:?} already exists"
1368 )));
1369 }
1370 cat.next_user_id = cat.next_user_id.max(1);
1371 let id = cat.next_user_id;
1372 cat.next_user_id += 1;
1373 cat.users.push(crate::auth::UserEntry {
1374 id,
1375 username: admin_username.to_string(),
1376 password_hash,
1377 roles: Vec::new(),
1378 is_admin: true,
1379 created_epoch: epoch.0,
1380 });
1381 cat.require_auth = true;
1382 cat.db_epoch = epoch.0;
1383 }
1384 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1385 *self.principal.write() = Some(crate::auth::Principal {
1388 username: admin_username.to_string(),
1389 is_admin: true,
1390 roles: Vec::new(),
1391 permissions: Vec::new(),
1392 });
1393 self.auth_state.set_require_auth(true);
1394 self.epoch.publish_in_order(epoch);
1395 _epoch_guard.disarm();
1396 Ok(())
1397 }
1398
1399 pub fn disable_auth(&self) -> Result<()> {
1416 let epoch = self.epoch.bump_assigned();
1417 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
1418 {
1419 let mut cat = self.catalog.write();
1420 if !cat.require_auth {
1421 return Err(MongrelError::InvalidArgument(
1422 "database does not have require_auth enabled".into(),
1423 ));
1424 }
1425 cat.require_auth = false;
1426 cat.db_epoch = epoch.0;
1427 }
1428 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1429 *self.principal.write() = None;
1431 self.auth_state.set_require_auth(false);
1433 self.epoch.publish_in_order(epoch);
1434 _epoch_guard.disarm();
1435 Ok(())
1436 }
1437
1438 pub fn require(&self, perm: &crate::auth::Permission) -> Result<()> {
1445 if !self.catalog.read().require_auth {
1446 return Ok(());
1447 }
1448 let guard = self.principal.read();
1449 let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
1450 if p.has_permission(perm) {
1451 Ok(())
1452 } else {
1453 Err(MongrelError::PermissionDenied {
1454 required: perm.clone(),
1455 principal: p.username.clone(),
1456 })
1457 }
1458 }
1459
1460 pub fn require_table(
1465 &self,
1466 table: &str,
1467 perm: crate::auth_state::RequiredPermission,
1468 ) -> Result<()> {
1469 self.require(&perm.into_permission(table))
1470 }
1471
1472 pub fn triggers(&self) -> Vec<StoredTrigger> {
1473 self.catalog
1474 .read()
1475 .triggers
1476 .iter()
1477 .map(|t| t.trigger.clone())
1478 .collect()
1479 }
1480
1481 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
1482 self.catalog
1483 .read()
1484 .triggers
1485 .iter()
1486 .find(|t| t.trigger.name == name)
1487 .map(|t| t.trigger.clone())
1488 }
1489
1490 pub fn create_trigger(&self, mut trigger: StoredTrigger) -> Result<StoredTrigger> {
1491 self.require(&crate::auth::Permission::Ddl)?;
1492 let _g = self.ddl_lock.lock();
1493 trigger.validate()?;
1494 self.validate_trigger_references(&trigger)?;
1495 {
1496 let cat = self.catalog.read();
1497 if cat.triggers.iter().any(|t| t.trigger.name == trigger.name) {
1498 return Err(MongrelError::InvalidArgument(format!(
1499 "trigger {:?} already exists",
1500 trigger.name
1501 )));
1502 }
1503 }
1504 let commit_lock = Arc::clone(&self.commit_lock);
1505 let _c = commit_lock.lock();
1506 let epoch = self.epoch.bump_assigned();
1507 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
1508 trigger.created_epoch = epoch.0;
1509 trigger.updated_epoch = epoch.0;
1510 {
1511 let mut cat = self.catalog.write();
1512 cat.triggers.push(TriggerEntry::from(trigger.clone()));
1513 cat.db_epoch = epoch.0;
1514 }
1515 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1516 self.epoch.publish_in_order(epoch);
1517 _epoch_guard.disarm();
1518 Ok(trigger)
1519 }
1520
1521 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
1522 let _g = self.ddl_lock.lock();
1523 trigger.validate()?;
1524 self.validate_trigger_references(&trigger)?;
1525 let commit_lock = Arc::clone(&self.commit_lock);
1526 let _c = commit_lock.lock();
1527 let epoch = self.epoch.bump_assigned();
1528 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
1529 let replaced = {
1530 let mut cat = self.catalog.write();
1531 let next = match cat
1532 .triggers
1533 .iter()
1534 .position(|t| t.trigger.name == trigger.name)
1535 {
1536 Some(idx) => {
1537 let next = cat.triggers[idx]
1538 .trigger
1539 .replaced(trigger.clone(), epoch.0)?;
1540 cat.triggers[idx] = TriggerEntry::from(next.clone());
1541 next
1542 }
1543 None => {
1544 let mut next = trigger;
1545 next.created_epoch = epoch.0;
1546 next.updated_epoch = epoch.0;
1547 cat.triggers.push(TriggerEntry::from(next.clone()));
1548 next
1549 }
1550 };
1551 cat.db_epoch = epoch.0;
1552 next
1553 };
1554 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1555 self.epoch.publish_in_order(epoch);
1556 _epoch_guard.disarm();
1557 Ok(replaced)
1558 }
1559
1560 pub fn drop_trigger(&self, name: &str) -> Result<()> {
1561 self.require(&crate::auth::Permission::Ddl)?;
1562 let _g = self.ddl_lock.lock();
1563 let commit_lock = Arc::clone(&self.commit_lock);
1564 let _c = commit_lock.lock();
1565 let epoch = self.epoch.bump_assigned();
1566 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
1567 {
1568 let mut cat = self.catalog.write();
1569 let before = cat.triggers.len();
1570 cat.triggers.retain(|t| t.trigger.name != name);
1571 if cat.triggers.len() == before {
1572 return Err(MongrelError::NotFound(format!(
1573 "trigger {name:?} not found"
1574 )));
1575 }
1576 cat.db_epoch = epoch.0;
1577 }
1578 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1579 self.epoch.publish_in_order(epoch);
1580 _epoch_guard.disarm();
1581 Ok(())
1582 }
1583
1584 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
1585 self.catalog.read().external_tables.clone()
1586 }
1587
1588 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
1589 self.catalog
1590 .read()
1591 .external_tables
1592 .iter()
1593 .find(|t| t.name == name)
1594 .cloned()
1595 }
1596
1597 pub fn create_external_table(
1598 &self,
1599 mut entry: ExternalTableEntry,
1600 ) -> Result<ExternalTableEntry> {
1601 self.require(&crate::auth::Permission::Ddl)?;
1602 let _g = self.ddl_lock.lock();
1603 entry.validate()?;
1604 {
1605 let cat = self.catalog.read();
1606 if cat.live(&entry.name).is_some()
1607 || cat.external_tables.iter().any(|t| t.name == entry.name)
1608 {
1609 return Err(MongrelError::InvalidArgument(format!(
1610 "table {:?} already exists",
1611 entry.name
1612 )));
1613 }
1614 }
1615 let commit_lock = Arc::clone(&self.commit_lock);
1616 let _c = commit_lock.lock();
1617 let epoch = self.epoch.bump_assigned();
1618 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
1619 entry.created_epoch = epoch.0;
1620 {
1621 let mut cat = self.catalog.write();
1622 cat.external_tables.push(entry.clone());
1623 cat.db_epoch = epoch.0;
1624 }
1625 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1626 self.epoch.publish_in_order(epoch);
1627 _epoch_guard.disarm();
1628 Ok(entry)
1629 }
1630
1631 pub fn drop_external_table(&self, name: &str) -> Result<()> {
1632 self.require(&crate::auth::Permission::Ddl)?;
1633 let _g = self.ddl_lock.lock();
1634 let commit_lock = Arc::clone(&self.commit_lock);
1635 let _c = commit_lock.lock();
1636 let epoch = self.epoch.bump_assigned();
1637 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
1638 {
1639 let mut cat = self.catalog.write();
1640 let before = cat.external_tables.len();
1641 cat.external_tables.retain(|t| t.name != name);
1642 if cat.external_tables.len() == before {
1643 return Err(MongrelError::NotFound(format!(
1644 "external table {name:?} not found"
1645 )));
1646 }
1647 cat.db_epoch = epoch.0;
1648 }
1649 let state_dir = self.root.join(VTAB_DIR).join(name);
1650 if state_dir.exists() {
1651 std::fs::remove_dir_all(state_dir)?;
1652 }
1653 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1654 self.epoch.publish_in_order(epoch);
1655 _epoch_guard.disarm();
1656 Ok(())
1657 }
1658
1659 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
1660 let txn_id = self.alloc_txn_id();
1661 self.commit_transaction_with_external_states(
1662 txn_id,
1663 self.epoch.visible(),
1664 Vec::new(),
1665 vec![(name.to_string(), state.to_vec())],
1666 None,
1667 )
1668 }
1669
1670 pub fn trigger_config(&self) -> TriggerConfig {
1671 use std::sync::atomic::Ordering;
1672 TriggerConfig {
1673 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
1674 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
1675 max_loop_iterations: self.trigger_max_loop_iterations.load(Ordering::Relaxed),
1676 }
1677 }
1678
1679 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
1680 use std::sync::atomic::Ordering;
1681 if config.max_depth == 0 {
1682 return Err(MongrelError::InvalidArgument(
1683 "trigger max_depth must be greater than 0".into(),
1684 ));
1685 }
1686 self.trigger_recursive
1687 .store(config.recursive_triggers, Ordering::Relaxed);
1688 self.trigger_max_depth
1689 .store(config.max_depth, Ordering::Relaxed);
1690 self.trigger_max_loop_iterations
1691 .store(config.max_loop_iterations, Ordering::Relaxed);
1692 Ok(())
1693 }
1694
1695 pub fn set_recursive_triggers(&self, recursive: bool) {
1696 use std::sync::atomic::Ordering;
1697 self.trigger_recursive.store(recursive, Ordering::Relaxed);
1698 }
1699
1700 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent> {
1703 self.notify.subscribe()
1704 }
1705
1706 pub fn notify(&self, channel: &str, message: Option<String>) {
1709 let _ = self.notify.send(ChangeEvent {
1710 channel: channel.to_string(),
1711 table: String::new(),
1712 op: "notify".into(),
1713 epoch: self.epoch.visible().0,
1714 message,
1715 });
1716 }
1717
1718 pub fn call_procedure(
1719 &self,
1720 name: &str,
1721 args: HashMap<String, crate::Value>,
1722 ) -> Result<ProcedureCallResult> {
1723 self.require(&crate::auth::Permission::All)?;
1727 let procedure = self
1728 .procedure(name)
1729 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
1730 let args = bind_procedure_args(&procedure, args)?;
1731 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
1732 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
1733 if has_writes {
1734 let mut tx = self.begin();
1735 let run = (|| {
1736 for step in &procedure.body.steps {
1737 let output =
1738 self.execute_procedure_step(step, &args, &outputs, Some(&mut tx))?;
1739 outputs.insert(step.id().to_string(), output);
1740 }
1741 eval_return_output(&procedure.body.return_value, &args, &outputs)
1742 })();
1743 match run {
1744 Ok(output) => {
1745 let epoch = tx.commit()?.0;
1746 Ok(ProcedureCallResult {
1747 epoch: Some(epoch),
1748 output,
1749 })
1750 }
1751 Err(e) => {
1752 tx.rollback();
1753 Err(e)
1754 }
1755 }
1756 } else {
1757 for step in &procedure.body.steps {
1758 let output = self.execute_procedure_step(step, &args, &outputs, None)?;
1759 outputs.insert(step.id().to_string(), output);
1760 }
1761 Ok(ProcedureCallResult {
1762 epoch: None,
1763 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
1764 })
1765 }
1766 }
1767
1768 fn execute_procedure_step(
1769 &self,
1770 step: &ProcedureStep,
1771 args: &HashMap<String, crate::Value>,
1772 outputs: &HashMap<String, ProcedureCallOutput>,
1773 tx: Option<&mut crate::txn::Transaction<'_>>,
1774 ) -> Result<ProcedureCallOutput> {
1775 match step {
1776 ProcedureStep::NativeQuery {
1777 table,
1778 conditions,
1779 projection,
1780 limit,
1781 ..
1782 } => {
1783 let mut q = crate::Query::new();
1784 for condition in conditions {
1785 q = q.and(eval_condition(condition, args, outputs)?);
1786 }
1787 let handle = self.table(table)?;
1788 let mut rows = handle.lock().query(&q)?;
1789 if let Some(limit) = limit {
1790 rows.truncate(*limit);
1791 }
1792 let projection = projection.as_ref();
1793 Ok(ProcedureCallOutput::Rows(
1794 rows.into_iter()
1795 .map(|row| ProcedureCallRow {
1796 row_id: Some(row.row_id),
1797 columns: match projection {
1798 Some(ids) => row
1799 .columns
1800 .into_iter()
1801 .filter(|(id, _)| ids.contains(id))
1802 .collect(),
1803 None => row.columns,
1804 },
1805 })
1806 .collect(),
1807 ))
1808 }
1809 ProcedureStep::Put {
1810 table,
1811 cells,
1812 returning,
1813 ..
1814 } => {
1815 let tx = tx.ok_or_else(|| {
1816 MongrelError::InvalidArgument(
1817 "write procedure step requires a transaction".into(),
1818 )
1819 })?;
1820 let cells = eval_cells(cells, args, outputs)?;
1821 if *returning {
1822 let out = tx.put_returning(table, cells)?;
1823 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
1824 row_id: None,
1825 columns: out.row.columns.into_iter().collect(),
1826 }))
1827 } else {
1828 tx.put(table, cells)?;
1829 Ok(ProcedureCallOutput::Null)
1830 }
1831 }
1832 ProcedureStep::Upsert {
1833 table,
1834 cells,
1835 update_cells,
1836 returning,
1837 ..
1838 } => {
1839 let tx = tx.ok_or_else(|| {
1840 MongrelError::InvalidArgument(
1841 "write procedure step requires a transaction".into(),
1842 )
1843 })?;
1844 let cells = eval_cells(cells, args, outputs)?;
1845 let action = match update_cells {
1846 Some(update_cells) => {
1847 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
1848 }
1849 None => crate::UpsertAction::DoNothing,
1850 };
1851 let out = tx.upsert(table, cells, action)?;
1852 if *returning {
1853 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
1854 row_id: None,
1855 columns: out.row.columns.into_iter().collect(),
1856 }))
1857 } else {
1858 Ok(ProcedureCallOutput::Null)
1859 }
1860 }
1861 ProcedureStep::DeleteByPk { table, pk, .. } => {
1862 let tx = tx.ok_or_else(|| {
1863 MongrelError::InvalidArgument(
1864 "write procedure step requires a transaction".into(),
1865 )
1866 })?;
1867 let pk = eval_value(pk, args, outputs)?;
1868 let handle = self.table(table)?;
1869 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
1870 MongrelError::NotFound("procedure delete_by_pk target not found".into())
1871 })?;
1872 tx.delete(table, row_id)?;
1873 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
1874 }
1875 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
1876 "DeleteRows procedure step is not supported by the core executor yet".into(),
1877 )),
1878 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
1879 "SqlQuery procedure step must be executed by mongreldb-query".into(),
1880 )),
1881 }
1882 }
1883
1884 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
1885 let cat = self.catalog.read();
1886 for step in &procedure.body.steps {
1887 let Some(table_name) = step.table() else {
1888 continue;
1889 };
1890 let schema = &cat
1891 .live(table_name)
1892 .ok_or_else(|| {
1893 MongrelError::InvalidArgument(format!(
1894 "procedure {:?} references unknown table {table_name:?}",
1895 procedure.name
1896 ))
1897 })?
1898 .schema;
1899 match step {
1900 ProcedureStep::NativeQuery {
1901 conditions,
1902 projection,
1903 ..
1904 } => {
1905 for condition in conditions {
1906 validate_condition_columns(condition, schema)?;
1907 }
1908 if let Some(projection) = projection {
1909 for id in projection {
1910 validate_column_id(*id, schema)?;
1911 }
1912 }
1913 }
1914 ProcedureStep::Put { cells, .. } => {
1915 for cell in cells {
1916 validate_column_id(cell.column_id, schema)?;
1917 }
1918 }
1919 ProcedureStep::Upsert {
1920 cells,
1921 update_cells,
1922 ..
1923 } => {
1924 for cell in cells {
1925 validate_column_id(cell.column_id, schema)?;
1926 }
1927 if let Some(update_cells) = update_cells {
1928 for cell in update_cells {
1929 validate_column_id(cell.column_id, schema)?;
1930 }
1931 }
1932 }
1933 ProcedureStep::DeleteByPk { .. } => {
1934 if schema.primary_key().is_none() {
1935 return Err(MongrelError::InvalidArgument(format!(
1936 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
1937 procedure.name
1938 )));
1939 }
1940 }
1941 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
1942 }
1943 }
1944 Ok(())
1945 }
1946
1947 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
1948 let cat = self.catalog.read();
1949 let target_schema = match &trigger.target {
1950 TriggerTarget::Table(target_name) => cat
1951 .live(target_name)
1952 .ok_or_else(|| {
1953 MongrelError::InvalidArgument(format!(
1954 "trigger {:?} references unknown target table {target_name:?}",
1955 trigger.name
1956 ))
1957 })?
1958 .schema
1959 .clone(),
1960 TriggerTarget::View(_) => Schema {
1961 columns: trigger.target_columns.clone(),
1962 ..Schema::default()
1963 },
1964 };
1965 for col in &trigger.update_of {
1966 if target_schema.column(col).is_none() {
1967 return Err(MongrelError::InvalidArgument(format!(
1968 "trigger {:?} UPDATE OF references unknown column {col:?}",
1969 trigger.name
1970 )));
1971 }
1972 }
1973 if let Some(expr) = &trigger.when {
1974 validate_trigger_expr(expr, &target_schema, trigger.event)?;
1975 }
1976 let mut select_schemas: HashMap<String, &Schema> = HashMap::new();
1977 for step in &trigger.program.steps {
1978 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
1979 {
1980 return Err(MongrelError::InvalidArgument(
1981 "SetNew trigger steps are only valid in BEFORE triggers".into(),
1982 ));
1983 }
1984 validate_trigger_step(
1985 step,
1986 &cat,
1987 &target_schema,
1988 trigger.event,
1989 &mut select_schemas,
1990 )?;
1991 }
1992 Ok(())
1993 }
1994
1995 pub fn begin(&self) -> crate::txn::Transaction<'_> {
1997 self.begin_with_isolation(crate::txn::IsolationLevel::default())
1998 }
1999
2000 pub fn begin_with_isolation(
2002 &self,
2003 level: crate::txn::IsolationLevel,
2004 ) -> crate::txn::Transaction<'_> {
2005 let txn_id = self.alloc_txn_id();
2006 let epoch = match level {
2007 crate::txn::IsolationLevel::ReadCommitted => self.epoch.visible(),
2008 _ => self.epoch.visible(),
2009 };
2010 let read = Snapshot::at(epoch);
2011 crate::txn::Transaction::new(self, txn_id, read)
2012 }
2013
2014 pub fn begin_with_external_trigger_bridge<'a>(
2017 &'a self,
2018 bridge: &'a dyn ExternalTriggerBridge,
2019 ) -> crate::txn::Transaction<'a> {
2020 let txn_id = self.alloc_txn_id();
2021 let read = Snapshot::at(self.epoch.visible());
2022 crate::txn::Transaction::new(self, txn_id, read).with_external_trigger_bridge(bridge)
2023 }
2024
2025 pub fn transaction<T>(
2027 &self,
2028 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
2029 ) -> Result<T> {
2030 let mut tx = self.begin();
2031 match f(&mut tx) {
2032 Ok(out) => {
2033 tx.commit()?;
2034 Ok(out)
2035 }
2036 Err(e) => {
2037 tx.rollback();
2038 Err(e)
2039 }
2040 }
2041 }
2042
2043 pub fn transaction_with_external_trigger_bridge<'a, T>(
2046 &'a self,
2047 bridge: &'a dyn ExternalTriggerBridge,
2048 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
2049 ) -> Result<T> {
2050 let mut tx = self.begin_with_external_trigger_bridge(bridge);
2051 match f(&mut tx) {
2052 Ok(out) => {
2053 tx.commit()?;
2054 Ok(out)
2055 }
2056 Err(e) => {
2057 tx.rollback();
2058 Err(e)
2059 }
2060 }
2061 }
2062
2063 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
2066 self.active_txns.register(epoch)
2067 }
2068
2069 fn fill_auto_increment_for_staging(
2070 &self,
2071 staging: &mut [(u64, crate::txn::Staged)],
2072 ) -> Result<()> {
2073 let tables = self.tables.read();
2074 for (table_id, staged) in staging {
2075 if let crate::txn::Staged::Put(cells) = staged {
2076 if let Some(handle) = tables.get(table_id) {
2077 let mut t = handle.lock();
2078 t.fill_auto_inc(cells)?;
2079 }
2080 }
2081 }
2082 Ok(())
2083 }
2084
2085 fn expand_table_triggers(
2086 &self,
2087 staging: &mut Vec<(u64, crate::txn::Staged)>,
2088 read_epoch: Epoch,
2089 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
2090 external_states: &mut Vec<(String, Vec<u8>)>,
2091 ) -> Result<()> {
2092 let mut external_writes = Vec::new();
2093 let config = self.trigger_config();
2094 if config.recursive_triggers {
2095 let chunk = std::mem::take(staging);
2096 let stacks = vec![Vec::new(); chunk.len()];
2097 *staging = self.expand_trigger_chunk(
2098 chunk,
2099 stacks,
2100 read_epoch,
2101 0,
2102 config.max_depth,
2103 &mut external_writes,
2104 &config,
2105 )?;
2106 self.apply_external_trigger_writes(
2107 external_writes,
2108 external_trigger_bridge,
2109 external_states,
2110 staging,
2111 )?;
2112 return Ok(());
2113 }
2114
2115 let mut expansion = self.expand_table_triggers_once(staging, read_epoch, None, &config)?;
2116 if !expansion.before.is_empty() {
2117 let mut final_staging = expansion.before;
2118 final_staging.extend(filter_ignored_staging(
2119 std::mem::take(staging),
2120 &expansion.ignored_indices,
2121 ));
2122 *staging = final_staging;
2123 } else if !expansion.ignored_indices.is_empty() {
2124 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
2125 }
2126 staging.append(&mut expansion.after);
2127 external_writes.append(&mut expansion.before_external);
2128 external_writes.append(&mut expansion.after_external);
2129 self.apply_external_trigger_writes(
2130 external_writes,
2131 external_trigger_bridge,
2132 external_states,
2133 staging,
2134 )?;
2135 Ok(())
2136 }
2137
2138 #[allow(clippy::too_many_arguments)]
2139 fn expand_trigger_chunk(
2140 &self,
2141 mut chunk: Vec<(u64, crate::txn::Staged)>,
2142 stacks: Vec<Vec<String>>,
2143 read_epoch: Epoch,
2144 depth: u32,
2145 max_depth: u32,
2146 external_writes: &mut Vec<ExternalTriggerWrite>,
2147 config: &TriggerConfig,
2148 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
2149 if chunk.is_empty() {
2150 return Ok(Vec::new());
2151 }
2152 self.fill_auto_increment_for_staging(&mut chunk)?;
2153 let expansion =
2154 self.expand_table_triggers_once(&mut chunk, read_epoch, Some(&stacks), config)?;
2155 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
2156 let stack = expansion
2157 .before_stacks
2158 .first()
2159 .or_else(|| expansion.after_stacks.first())
2160 .cloned()
2161 .unwrap_or_default();
2162 return Err(MongrelError::Conflict(format!(
2163 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
2164 Self::format_trigger_stack(&stack)
2165 )));
2166 }
2167
2168 let mut out = Vec::new();
2169 external_writes.extend(expansion.before_external);
2170 out.extend(self.expand_trigger_chunk(
2171 expansion.before,
2172 expansion.before_stacks,
2173 read_epoch,
2174 depth + 1,
2175 max_depth,
2176 external_writes,
2177 config,
2178 )?);
2179 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
2180 external_writes.extend(expansion.after_external);
2181 out.extend(self.expand_trigger_chunk(
2182 expansion.after,
2183 expansion.after_stacks,
2184 read_epoch,
2185 depth + 1,
2186 max_depth,
2187 external_writes,
2188 config,
2189 )?);
2190 Ok(out)
2191 }
2192
2193 fn apply_external_trigger_writes(
2194 &self,
2195 writes: Vec<ExternalTriggerWrite>,
2196 bridge: Option<&dyn ExternalTriggerBridge>,
2197 external_states: &mut Vec<(String, Vec<u8>)>,
2198 staging: &mut Vec<(u64, crate::txn::Staged)>,
2199 ) -> Result<()> {
2200 if writes.is_empty() {
2201 return Ok(());
2202 }
2203 let bridge = bridge.ok_or_else(|| {
2204 MongrelError::InvalidArgument(
2205 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
2206 )
2207 })?;
2208 for write in writes {
2209 let table = write.table().to_string();
2210 let entry = self.external_table(&table).ok_or_else(|| {
2211 MongrelError::NotFound(format!("external table {table:?} not found"))
2212 })?;
2213 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
2214 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
2215 external_states.push((table, result.state));
2216 for base_write in result.base_writes {
2217 match base_write {
2218 ExternalTriggerBaseWrite::Put { table, cells } => {
2219 let table_id = self.table_id(&table)?;
2220 staging.push((table_id, crate::txn::Staged::Put(cells)));
2221 }
2222 ExternalTriggerBaseWrite::Delete { table, row_id } => {
2223 let table_id = self.table_id(&table)?;
2224 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
2225 }
2226 }
2227 }
2228 }
2229 dedup_external_states_in_place(external_states);
2230 Ok(())
2231 }
2232
2233 fn expand_table_triggers_once(
2234 &self,
2235 staging: &mut Vec<(u64, crate::txn::Staged)>,
2236 read_epoch: Epoch,
2237 trigger_stacks: Option<&[Vec<String>]>,
2238 config: &TriggerConfig,
2239 ) -> Result<TriggerExpansion> {
2240 let triggers: Vec<StoredTrigger> = self
2241 .catalog
2242 .read()
2243 .triggers
2244 .iter()
2245 .filter(|entry| {
2246 entry.trigger.enabled
2247 && matches!(
2248 entry.trigger.timing,
2249 TriggerTiming::Before | TriggerTiming::After
2250 )
2251 && matches!(entry.trigger.target, TriggerTarget::Table(_))
2252 })
2253 .map(|entry| entry.trigger.clone())
2254 .collect();
2255 if triggers.is_empty() || staging.is_empty() {
2256 return Ok(TriggerExpansion::default());
2257 }
2258
2259 let before_triggers = triggers
2260 .iter()
2261 .filter(|trigger| trigger.timing == TriggerTiming::Before)
2262 .cloned()
2263 .collect::<Vec<_>>();
2264 let after_triggers = triggers
2265 .iter()
2266 .filter(|trigger| trigger.timing == TriggerTiming::After)
2267 .cloned()
2268 .collect::<Vec<_>>();
2269
2270 let mut before_added = Vec::new();
2271 let mut before_stacks = Vec::new();
2272 let mut before_external = Vec::new();
2273 let mut ignored_indices = std::collections::BTreeSet::new();
2274 if !before_triggers.is_empty() {
2275 let before_events =
2276 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks)?;
2277 let mut out = TriggerProgramOutput {
2278 added: &mut before_added,
2279 added_stacks: &mut before_stacks,
2280 added_external: &mut before_external,
2281 ignored_indices: &mut ignored_indices,
2282 };
2283 self.execute_triggers_for_events(
2284 &before_triggers,
2285 &before_events,
2286 Some(staging),
2287 &mut out,
2288 config,
2289 read_epoch,
2290 )?;
2291 }
2292
2293 let after_events = if after_triggers.is_empty() {
2294 Vec::new()
2295 } else {
2296 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks)?
2297 .into_iter()
2298 .filter(|event| {
2299 !event
2300 .op_indices
2301 .iter()
2302 .any(|idx| ignored_indices.contains(idx))
2303 })
2304 .collect()
2305 };
2306
2307 let mut after_added = Vec::new();
2308 let mut after_stacks = Vec::new();
2309 let mut after_external = Vec::new();
2310 let mut out = TriggerProgramOutput {
2311 added: &mut after_added,
2312 added_stacks: &mut after_stacks,
2313 added_external: &mut after_external,
2314 ignored_indices: &mut ignored_indices,
2315 };
2316 self.execute_triggers_for_events(
2317 &after_triggers,
2318 &after_events,
2319 None,
2320 &mut out,
2321 config,
2322 read_epoch,
2323 )?;
2324 Ok(TriggerExpansion {
2325 before: before_added,
2326 before_stacks,
2327 before_external,
2328 after: after_added,
2329 after_stacks,
2330 after_external,
2331 ignored_indices,
2332 })
2333 }
2334
2335 fn execute_triggers_for_events(
2336 &self,
2337 triggers: &[StoredTrigger],
2338 events: &[WriteEvent],
2339 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
2340 out: &mut TriggerProgramOutput<'_>,
2341 config: &TriggerConfig,
2342 read_epoch: Epoch,
2343 ) -> Result<()> {
2344 for event in events {
2345 for trigger in triggers {
2346 if event
2347 .op_indices
2348 .iter()
2349 .any(|idx| out.ignored_indices.contains(idx))
2350 {
2351 break;
2352 }
2353 let matches = {
2354 let cat = self.catalog.read();
2355 trigger_matches_event(trigger, event, &cat)?
2356 };
2357 if !matches {
2358 continue;
2359 }
2360 if let Some(when) = &trigger.when {
2361 if !eval_trigger_expr(when, event)? {
2362 continue;
2363 }
2364 }
2365 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
2366 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
2367 return Err(MongrelError::Conflict(format!(
2368 "trigger recursion cycle detected; trigger stack: {}",
2369 Self::format_trigger_stack(&trigger_stack)
2370 )));
2371 }
2372 let outcome = match staging.as_mut() {
2373 Some(staging) => self.execute_trigger_program(
2374 trigger,
2375 event,
2376 Some(&mut **staging),
2377 out,
2378 &trigger_stack,
2379 config,
2380 read_epoch,
2381 )?,
2382 None => self.execute_trigger_program(
2383 trigger,
2384 event,
2385 None,
2386 out,
2387 &trigger_stack,
2388 config,
2389 read_epoch,
2390 )?,
2391 };
2392 if outcome == TriggerProgramOutcome::Ignore {
2393 out.ignored_indices.extend(event.op_indices.iter().copied());
2394 break;
2395 }
2396 }
2397 }
2398 Ok(())
2399 }
2400
2401 fn trigger_events_for_staging(
2402 &self,
2403 staging: &[(u64, crate::txn::Staged)],
2404 read_epoch: Epoch,
2405 trigger_stacks: Option<&[Vec<String>]>,
2406 ) -> Result<Vec<WriteEvent>> {
2407 use crate::txn::Staged;
2408 use std::collections::{HashMap, VecDeque};
2409
2410 let snapshot = Snapshot::at(read_epoch);
2411 let cat = self.catalog.read();
2412 let mut table_names = HashMap::new();
2413 let mut table_schemas = HashMap::new();
2414 for entry in cat
2415 .tables
2416 .iter()
2417 .filter(|entry| matches!(entry.state, TableState::Live))
2418 {
2419 table_names.insert(entry.table_id, entry.name.clone());
2420 table_schemas.insert(entry.table_id, entry.schema.clone());
2421 }
2422 drop(cat);
2423
2424 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
2425 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
2426 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
2427
2428 for (idx, (table_id, staged)) in staging.iter().enumerate() {
2429 let Some(schema) = table_schemas.get(table_id) else {
2430 continue;
2431 };
2432 let Some(pk) = schema.primary_key() else {
2433 continue;
2434 };
2435 match staged {
2436 Staged::Delete(row_id) => {
2437 let handle = self.table_by_id(*table_id)?;
2438 let Some(row) = handle.lock().get(*row_id, snapshot) else {
2439 continue;
2440 };
2441 let Some(pk_value) = row.columns.get(&pk.id) else {
2442 continue;
2443 };
2444 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
2445 delete_by_key
2446 .entry((*table_id, pk_value.encode_key()))
2447 .or_default()
2448 .push_back(idx);
2449 }
2450 Staged::Put(cells) => {
2451 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
2452 put_by_key
2453 .entry((*table_id, value.encode_key()))
2454 .or_default()
2455 .push_back(idx);
2456 }
2457 }
2458 Staged::Truncate => {}
2459 }
2460 }
2461
2462 let mut paired_delete = std::collections::HashSet::new();
2463 let mut paired_put = std::collections::HashSet::new();
2464 let mut events = Vec::new();
2465
2466 for (key, deletes) in delete_by_key.iter_mut() {
2467 let Some(puts) = put_by_key.get_mut(key) else {
2468 continue;
2469 };
2470 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
2471 paired_delete.insert(delete_idx);
2472 paired_put.insert(put_idx);
2473 let (table_id, _) = &staging[put_idx];
2474 let Some(table_name) = table_names.get(table_id).cloned() else {
2475 continue;
2476 };
2477 let old = old_rows.get(&delete_idx).cloned();
2478 let new = match &staging[put_idx].1 {
2479 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
2480 _ => None,
2481 };
2482 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
2483 events.push(WriteEvent {
2484 table: table_name,
2485 kind: TriggerEvent::Update,
2486 old,
2487 new,
2488 changed_columns,
2489 op_indices: vec![delete_idx, put_idx],
2490 put_idx: Some(put_idx),
2491 trigger_stack: Self::trigger_stack_for_indices(
2492 trigger_stacks,
2493 &[delete_idx, put_idx],
2494 ),
2495 });
2496 }
2497 }
2498
2499 for (idx, (table_id, staged)) in staging.iter().enumerate() {
2500 let Some(table_name) = table_names.get(table_id).cloned() else {
2501 continue;
2502 };
2503 match staged {
2504 Staged::Put(cells) if !paired_put.contains(&idx) => {
2505 let new = Some(TriggerRowImage::from_cells(cells));
2506 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
2507 events.push(WriteEvent {
2508 table: table_name,
2509 kind: TriggerEvent::Insert,
2510 old: None,
2511 new,
2512 changed_columns,
2513 op_indices: vec![idx],
2514 put_idx: Some(idx),
2515 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
2516 });
2517 }
2518 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
2519 let old = match old_rows.get(&idx).cloned() {
2520 Some(old) => Some(old),
2521 None => {
2522 let handle = self.table_by_id(*table_id)?;
2523 let row = handle.lock().get(*row_id, snapshot);
2524 row.map(TriggerRowImage::from_row)
2525 }
2526 };
2527 let Some(old) = old else {
2528 continue;
2529 };
2530 let changed_columns = old.columns.keys().copied().collect();
2531 events.push(WriteEvent {
2532 table: table_name,
2533 kind: TriggerEvent::Delete,
2534 old: Some(old),
2535 new: None,
2536 changed_columns,
2537 op_indices: vec![idx],
2538 put_idx: None,
2539 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
2540 });
2541 }
2542 Staged::Truncate => {}
2543 _ => {}
2544 }
2545 }
2546
2547 Ok(events)
2548 }
2549
2550 #[allow(clippy::too_many_arguments)]
2551 fn execute_trigger_program(
2552 &self,
2553 trigger: &StoredTrigger,
2554 event: &WriteEvent,
2555 staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
2556 out: &mut TriggerProgramOutput<'_>,
2557 trigger_stack: &[String],
2558 config: &TriggerConfig,
2559 read_epoch: Epoch,
2560 ) -> Result<TriggerProgramOutcome> {
2561 let mut event = event.clone();
2562 let mut select_results: HashMap<String, Vec<TriggerRowImage>> = HashMap::new();
2563 self.execute_trigger_steps(
2564 trigger,
2565 &trigger.program.steps,
2566 &mut event,
2567 staging,
2568 out,
2569 trigger_stack,
2570 config,
2571 &mut select_results,
2572 0,
2573 None,
2574 read_epoch,
2575 )
2576 }
2577
2578 #[allow(clippy::too_many_arguments)]
2579 fn execute_trigger_steps(
2580 &self,
2581 trigger: &StoredTrigger,
2582 steps: &[TriggerStep],
2583 event: &mut WriteEvent,
2584 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
2585 out: &mut TriggerProgramOutput<'_>,
2586 trigger_stack: &[String],
2587 config: &TriggerConfig,
2588 select_results: &mut HashMap<String, Vec<TriggerRowImage>>,
2589 depth: u32,
2590 selected: Option<&TriggerRowImage>,
2591 read_epoch: Epoch,
2592 ) -> Result<TriggerProgramOutcome> {
2593 let _ = depth;
2594 for step in steps {
2595 match step {
2596 TriggerStep::SetNew { cells } => {
2597 if trigger.timing != TriggerTiming::Before {
2598 return Err(MongrelError::InvalidArgument(
2599 "SetNew trigger step is only valid in BEFORE triggers".into(),
2600 ));
2601 }
2602 let put_idx = event.put_idx.ok_or_else(|| {
2603 MongrelError::InvalidArgument(
2604 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
2605 )
2606 })?;
2607 let staging = staging.as_deref_mut().ok_or_else(|| {
2608 MongrelError::InvalidArgument(
2609 "SetNew trigger step requires mutable trigger staging".into(),
2610 )
2611 })?;
2612 let Some((_, crate::txn::Staged::Put(row_cells))) = staging.get_mut(put_idx)
2613 else {
2614 return Err(MongrelError::InvalidArgument(
2615 "SetNew trigger step target row is not mutable".into(),
2616 ));
2617 };
2618 for (column_id, value) in eval_trigger_cells(cells, event, selected)? {
2619 row_cells.retain(|(id, _)| *id != column_id);
2620 row_cells.push((column_id, value.clone()));
2621 if let Some(new) = &mut event.new {
2622 new.columns.insert(column_id, value);
2623 }
2624 }
2625 row_cells.sort_by_key(|(id, _)| *id);
2626 }
2627 TriggerStep::Insert { table, cells } => {
2628 let cells = eval_trigger_cells(cells, event, selected)?;
2629 if let Ok(table_id) = self.table_id(table) {
2630 out.added.push((table_id, crate::txn::Staged::Put(cells)));
2631 out.added_stacks.push(trigger_stack.to_vec());
2632 } else if self.external_table(table).is_some() {
2633 out.added_external.push(ExternalTriggerWrite::Insert {
2634 table: table.clone(),
2635 cells,
2636 });
2637 } else {
2638 return Err(MongrelError::NotFound(format!(
2639 "trigger {:?} insert target {table:?} not found",
2640 trigger.name
2641 )));
2642 }
2643 }
2644 TriggerStep::UpdateByPk { table, pk, cells } => {
2645 let pk = eval_trigger_value(pk, event, selected)?;
2646 let cells = eval_trigger_cells(cells, event, selected)?;
2647 if self.external_table(table).is_some() {
2648 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
2649 table: table.clone(),
2650 pk,
2651 cells,
2652 });
2653 } else {
2654 let row_id = self
2655 .table(table)?
2656 .lock()
2657 .lookup_pk(&pk.encode_key())
2658 .ok_or_else(|| {
2659 MongrelError::NotFound(format!(
2660 "trigger {:?} update target not found",
2661 trigger.name
2662 ))
2663 })?;
2664 let handle = self.table(table)?;
2665 let snapshot = Snapshot::at(self.epoch.visible());
2666 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
2667 MongrelError::NotFound(format!(
2668 "trigger {:?} update target not visible",
2669 trigger.name
2670 ))
2671 })?;
2672 let mut merged = old.columns;
2673 for (column_id, value) in cells {
2674 merged.insert(column_id, value);
2675 }
2676 out.added
2677 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
2678 out.added_stacks.push(trigger_stack.to_vec());
2679 out.added.push((
2680 self.table_id(table)?,
2681 crate::txn::Staged::Put(merged.into_iter().collect()),
2682 ));
2683 out.added_stacks.push(trigger_stack.to_vec());
2684 }
2685 }
2686 TriggerStep::DeleteByPk { table, pk } => {
2687 let pk = eval_trigger_value(pk, event, selected)?;
2688 if self.external_table(table).is_some() {
2689 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
2690 table: table.clone(),
2691 pk,
2692 });
2693 } else {
2694 let row_id = self
2695 .table(table)?
2696 .lock()
2697 .lookup_pk(&pk.encode_key())
2698 .ok_or_else(|| {
2699 MongrelError::NotFound(format!(
2700 "trigger {:?} delete target not found",
2701 trigger.name
2702 ))
2703 })?;
2704 out.added
2705 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
2706 out.added_stacks.push(trigger_stack.to_vec());
2707 }
2708 }
2709 TriggerStep::Select {
2710 id,
2711 table,
2712 conditions,
2713 } => {
2714 let schema = self.table(table)?.lock().schema().clone();
2715 let snapshot = Snapshot::at(read_epoch);
2716 let rows = self.table(table)?.lock().visible_rows(snapshot)?;
2717 let mut matched = Vec::new();
2718 for row in rows {
2719 let image = TriggerRowImage::from_row(row);
2720 let passes = conditions
2721 .iter()
2722 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
2723 .collect::<Result<Vec<_>>>()?
2724 .into_iter()
2725 .all(|b| b);
2726 if passes {
2727 matched.push(image);
2728 }
2729 }
2730 if let Some(pk) = schema.primary_key() {
2731 matched.sort_by(|a, b| {
2732 let av = a.columns.get(&pk.id).unwrap_or(&Value::Null);
2733 let bv = b.columns.get(&pk.id).unwrap_or(&Value::Null);
2734 value_order(av, bv).unwrap_or(std::cmp::Ordering::Equal)
2735 });
2736 }
2737 select_results.insert(id.clone(), matched);
2738 }
2739 TriggerStep::Foreach { id, steps } => {
2740 let rows = select_results.get(id).ok_or_else(|| {
2741 MongrelError::InvalidArgument(format!(
2742 "trigger {:?} foreach references unknown select id {id:?}",
2743 trigger.name
2744 ))
2745 })?;
2746 if rows.len() > config.max_loop_iterations as usize {
2747 return Err(MongrelError::InvalidArgument(format!(
2748 "trigger {:?} foreach exceeded max_loop_iterations ({})",
2749 trigger.name, config.max_loop_iterations
2750 )));
2751 }
2752 for row in rows.clone() {
2753 let result = self.execute_trigger_steps(
2754 trigger,
2755 steps,
2756 event,
2757 staging.as_deref_mut(),
2758 out,
2759 trigger_stack,
2760 config,
2761 select_results,
2762 depth + 1,
2763 Some(&row),
2764 read_epoch,
2765 )?;
2766 if result == TriggerProgramOutcome::Ignore {
2767 return Ok(TriggerProgramOutcome::Ignore);
2768 }
2769 }
2770 }
2771 TriggerStep::DeleteWhere { table, conditions } => {
2772 let schema = self.table(table)?.lock().schema().clone();
2773 let snapshot = Snapshot::at(read_epoch);
2774 let rows = self.table(table)?.lock().visible_rows(snapshot)?;
2775 let table_id = self.table_id(table)?;
2776 let mut to_delete = Vec::new();
2777 for row in rows {
2778 let image = TriggerRowImage::from_row(row.clone());
2779 let passes = conditions
2780 .iter()
2781 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
2782 .collect::<Result<Vec<_>>>()?
2783 .into_iter()
2784 .all(|b| b);
2785 if passes {
2786 to_delete.push((table_id, row.row_id));
2787 }
2788 }
2789 for (table_id, row_id) in to_delete {
2790 out.added
2791 .push((table_id, crate::txn::Staged::Delete(row_id)));
2792 out.added_stacks.push(trigger_stack.to_vec());
2793 }
2794 }
2795 TriggerStep::UpdateWhere {
2796 table,
2797 conditions,
2798 cells,
2799 } => {
2800 let schema = self.table(table)?.lock().schema().clone();
2801 let snapshot = Snapshot::at(read_epoch);
2802 let rows = self.table(table)?.lock().visible_rows(snapshot)?;
2803 let table_id = self.table_id(table)?;
2804 let mut to_update = Vec::new();
2805 for row in rows {
2806 let image = TriggerRowImage::from_row(row.clone());
2807 let passes = conditions
2808 .iter()
2809 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
2810 .collect::<Result<Vec<_>>>()?
2811 .into_iter()
2812 .all(|b| b);
2813 if passes {
2814 let new_cells = cells
2815 .iter()
2816 .map(|cell| {
2817 Ok((
2818 cell.column_id,
2819 eval_trigger_value(&cell.value, event, Some(&image))?,
2820 ))
2821 })
2822 .collect::<Result<Vec<_>>>()?;
2823 let mut merged = row.columns.clone();
2824 for (column_id, value) in new_cells {
2825 merged.insert(column_id, value);
2826 }
2827 to_update.push((table_id, row.row_id, merged));
2828 }
2829 }
2830 for (table_id, row_id, merged) in to_update {
2831 out.added
2832 .push((table_id, crate::txn::Staged::Delete(row_id)));
2833 out.added_stacks.push(trigger_stack.to_vec());
2834 out.added.push((
2835 table_id,
2836 crate::txn::Staged::Put(merged.into_iter().collect()),
2837 ));
2838 out.added_stacks.push(trigger_stack.to_vec());
2839 }
2840 }
2841 TriggerStep::Raise { action, message } => match action {
2842 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
2843 TriggerRaiseAction::Abort
2844 | TriggerRaiseAction::Fail
2845 | TriggerRaiseAction::Rollback => {
2846 let message = eval_trigger_value(message, event, selected)?;
2847 return Err(MongrelError::Conflict(format!(
2848 "trigger {:?} raised: {}; trigger stack: {}",
2849 trigger.name,
2850 trigger_message(message),
2851 Self::format_trigger_stack(trigger_stack)
2852 )));
2853 }
2854 },
2855 }
2856 }
2857 Ok(TriggerProgramOutcome::Continue)
2858 }
2859
2860 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
2861 let Some(stacks) = stacks else {
2862 return Vec::new();
2863 };
2864 let mut out = Vec::new();
2865 for idx in indices {
2866 let Some(stack) = stacks.get(*idx) else {
2867 continue;
2868 };
2869 for name in stack {
2870 if !out.iter().any(|existing| existing == name) {
2871 out.push(name.clone());
2872 }
2873 }
2874 }
2875 out
2876 }
2877
2878 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
2879 let mut out = stack.to_vec();
2880 out.push(trigger_name.to_string());
2881 out
2882 }
2883
2884 fn format_trigger_stack(stack: &[String]) -> String {
2885 if stack.is_empty() {
2886 "<root>".into()
2887 } else {
2888 stack.join(" -> ")
2889 }
2890 }
2891
2892 fn validate_constraints(
2907 &self,
2908 staging: &mut Vec<(u64, crate::txn::Staged)>,
2909 read_epoch: Epoch,
2910 ) -> Result<()> {
2911 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
2912 use crate::memtable::Row;
2913 use crate::txn::Staged;
2914 use std::collections::HashSet;
2915
2916 let snapshot = Snapshot::at(read_epoch);
2917 let cat = self.catalog.read();
2918
2919 let live: Vec<(u64, &str, &crate::schema::Schema)> = cat
2921 .tables
2922 .iter()
2923 .filter(|e| matches!(e.state, TableState::Live))
2924 .map(|e| (e.table_id, e.name.as_str(), &e.schema))
2925 .collect();
2926
2927 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
2929 if !any_constraints {
2930 return Ok(());
2931 }
2932
2933 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
2935 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
2936 if let Some(r) = rows_cache.get(&table_id) {
2937 return Ok(r.clone());
2938 }
2939 let handle = self.table_by_id(table_id)?;
2940 let rows = handle.lock().visible_rows(snapshot)?;
2941 rows_cache.insert(table_id, rows.clone());
2942 Ok(rows)
2943 };
2944
2945 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
2950 loop {
2951 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
2952 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
2953 .iter()
2954 .filter_map(|(t, op)| match op {
2955 Staged::Delete(rid) => Some((*t, *rid)),
2956 _ => None,
2957 })
2958 .collect();
2959 for (table_id, rid) in deletes {
2960 if !cascaded.insert((table_id, rid.0)) {
2961 continue;
2962 }
2963 let Some(tname) = live
2964 .iter()
2965 .find(|(t, _, _)| *t == table_id)
2966 .map(|(_, n, _)| *n)
2967 else {
2968 continue;
2969 };
2970 let parent_handle = self.table_by_id(table_id)?;
2971 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
2972 continue;
2973 };
2974 for (child_id, _child_name, child_schema) in &live {
2975 for fk in &child_schema.constraints.foreign_keys {
2976 if fk.ref_table != tname {
2977 continue;
2978 }
2979 let Some(parent_key) =
2980 encode_composite_key(&fk.ref_columns, &parent_row.columns)
2981 else {
2982 continue;
2983 };
2984 match fk.on_delete {
2985 FkAction::Restrict => continue,
2986 FkAction::Cascade => {
2987 let child_rows = load_rows(*child_id)?;
2988 for cr in &child_rows {
2989 if !cascaded.contains(&(*child_id, cr.row_id.0))
2990 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
2991 == Some(parent_key.as_slice())
2992 {
2993 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
2994 }
2995 }
2996 }
2997 FkAction::SetNull => {
2998 let child_rows = load_rows(*child_id)?;
2999 for cr in &child_rows {
3000 if !cascaded.contains(&(*child_id, cr.row_id.0))
3001 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
3002 == Some(parent_key.as_slice())
3003 {
3004 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
3007 .columns
3008 .iter()
3009 .map(|(k, v)| (*k, v.clone()))
3010 .collect();
3011 for cid in &fk.columns {
3012 cells.retain(|(k, _)| k != cid);
3013 cells.push((*cid, crate::memtable::Value::Null));
3014 }
3015 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
3016 new_ops.push((*child_id, Staged::Put(cells)));
3017 }
3018 }
3019 }
3020 }
3021 }
3022 }
3023 }
3024 if new_ops.is_empty() {
3025 break;
3026 }
3027 staging.extend(new_ops);
3028 }
3029
3030 let staged_deletes: HashSet<(u64, u64)> = staging
3034 .iter()
3035 .filter_map(|(t, op)| match op {
3036 Staged::Delete(rid) => Some((*t, rid.0)),
3037 _ => None,
3038 })
3039 .collect();
3040
3041 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
3043
3044 for (table_id, op) in staging.iter() {
3046 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id).copied()
3047 else {
3048 continue;
3049 };
3050 let cells_map: HashMap<u16, crate::memtable::Value>;
3051 match op {
3052 Staged::Put(cells) => {
3053 cells_map = cells.iter().cloned().collect();
3054
3055 if !schema.constraints.checks.is_empty() {
3057 validate_checks(&schema.constraints.checks, &cells_map)?;
3058 }
3059
3060 for uc in &schema.constraints.uniques {
3062 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
3063 continue; };
3065 let marker = (*table_id, uc.id, key.clone());
3066 if !seen_unique.insert(marker) {
3067 return Err(MongrelError::Conflict(format!(
3068 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
3069 uc.name
3070 )));
3071 }
3072 let rows = load_rows(*table_id)?;
3073 for r in &rows {
3074 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
3077 continue;
3078 }
3079 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
3080 if theirs == key {
3081 return Err(MongrelError::Conflict(format!(
3082 "UNIQUE constraint '{}' on table '{tname}' violated",
3083 uc.name
3084 )));
3085 }
3086 }
3087 }
3088 }
3089
3090 for fk in &schema.constraints.foreign_keys {
3092 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
3093 continue; };
3095 let Some(parent_id) = cat
3096 .tables
3097 .iter()
3098 .find(|t| t.name == fk.ref_table)
3099 .map(|t| t.table_id)
3100 else {
3101 return Err(MongrelError::InvalidArgument(format!(
3102 "FOREIGN KEY '{}' references unknown table '{}'",
3103 fk.name, fk.ref_table
3104 )));
3105 };
3106 let parent_rows = load_rows(parent_id)?;
3107 let mut found = false;
3108 for r in &parent_rows {
3109 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
3110 continue;
3111 }
3112 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
3113 if pkey == child_key {
3114 found = true;
3115 break;
3116 }
3117 }
3118 }
3119 if !found {
3120 return Err(MongrelError::Conflict(format!(
3121 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
3122 fk.name, fk.ref_table
3123 )));
3124 }
3125 }
3126 }
3127 Staged::Delete(rid) => {
3128 let parent_handle = self.table_by_id(*table_id)?;
3132 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
3133 continue;
3134 };
3135 for (child_id, child_name, child_schema) in &live {
3136 for fk in &child_schema.constraints.foreign_keys {
3137 if fk.ref_table != tname || fk.on_delete != FkAction::Restrict {
3138 continue;
3139 }
3140 let Some(parent_key) =
3141 encode_composite_key(&fk.ref_columns, &parent_row.columns)
3142 else {
3143 continue;
3144 };
3145 let child_rows = load_rows(*child_id)?;
3146 for r in &child_rows {
3147 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
3150 continue;
3151 }
3152 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
3153 if ck == parent_key {
3154 return Err(MongrelError::Conflict(format!(
3155 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
3156 fk.name
3157 )));
3158 }
3159 }
3160 }
3161 }
3162 }
3163 }
3164 Staged::Truncate => {
3165 for (child_id, child_name, child_schema) in &live {
3169 for fk in &child_schema.constraints.foreign_keys {
3170 if fk.ref_table != tname {
3171 continue;
3172 }
3173 let child_rows = load_rows(*child_id)?;
3174 if child_rows
3175 .iter()
3176 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
3177 {
3178 return Err(MongrelError::Conflict(format!(
3179 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
3180 fk.name
3181 )));
3182 }
3183 }
3184 }
3185 }
3186 }
3187 }
3188 Ok(())
3189 }
3190
3191 pub(crate) fn commit_transaction_with_external_states(
3198 &self,
3199 txn_id: u64,
3200 read_epoch: Epoch,
3201 mut staging: Vec<(u64, crate::txn::Staged)>,
3202 external_states: Vec<(String, Vec<u8>)>,
3203 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
3204 ) -> Result<Epoch> {
3205 use crate::memtable::Row;
3206 use crate::txn::{Staged, StagedOp, WriteKey};
3207 use crate::wal::Op;
3208 use std::collections::hash_map::DefaultHasher;
3209 use std::hash::{Hash, Hasher};
3210 use std::sync::atomic::Ordering;
3211
3212 if self.poisoned.load(Ordering::Relaxed) {
3213 return Err(MongrelError::Other(
3214 "database poisoned by fsync error".into(),
3215 ));
3216 }
3217 let mut external_states = dedup_external_states(external_states);
3218 if !external_states.is_empty() {
3219 let cat = self.catalog.read();
3220 for (name, _) in &external_states {
3221 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
3222 return Err(MongrelError::NotFound(format!(
3223 "external table {name:?} not found"
3224 )));
3225 }
3226 }
3227 }
3228
3229 self.fill_auto_increment_for_staging(&mut staging)?;
3232 self.expand_table_triggers(
3233 &mut staging,
3234 read_epoch,
3235 external_trigger_bridge,
3236 &mut external_states,
3237 )?;
3238 self.fill_auto_increment_for_staging(&mut staging)?;
3239 external_states = dedup_external_states(external_states);
3240
3241 self.validate_constraints(&mut staging, read_epoch)?;
3246
3247 let write_keys = {
3248 let cat = self.catalog.read();
3249 let mut keys: Vec<WriteKey> = Vec::new();
3250 for (table_id, staged) in &staging {
3251 match staged {
3252 Staged::Put(cells) => {
3253 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
3254 for col in &entry.schema.columns {
3255 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
3256 if let Some((_, val)) =
3257 cells.iter().find(|(id, _)| *id == col.id)
3258 {
3259 let mut h = DefaultHasher::new();
3260 val.encode_key().hash(&mut h);
3261 keys.push(WriteKey::Unique {
3262 table_id: *table_id,
3263 index_id: 0,
3264 key_hash: h.finish(),
3265 });
3266 }
3267 }
3268 }
3269 for uc in &entry.schema.constraints.uniques {
3276 if let Some(key_bytes) = crate::constraint::encode_composite_key(
3277 &uc.columns,
3278 &cells.iter().cloned().collect(),
3279 ) {
3280 let mut h = DefaultHasher::new();
3281 key_bytes.hash(&mut h);
3282 keys.push(WriteKey::Unique {
3283 table_id: *table_id,
3284 index_id: uc.id | 0x8000,
3285 key_hash: h.finish(),
3286 });
3287 }
3288 }
3289 }
3290 }
3291 Staged::Delete(rid) => keys.push(WriteKey::Row {
3292 table_id: *table_id,
3293 row_id: rid.0,
3294 }),
3295 Staged::Truncate => keys.push(WriteKey::Table {
3296 table_id: *table_id,
3297 }),
3298 }
3299 }
3300 for (name, _) in &external_states {
3301 let mut h = DefaultHasher::new();
3302 name.hash(&mut h);
3303 keys.push(WriteKey::Unique {
3304 table_id: EXTERNAL_TABLE_ID,
3305 index_id: 0,
3306 key_hash: h.finish(),
3307 });
3308 }
3309 keys
3310 };
3311
3312 let min_active = self.active_txns.min_read_epoch();
3314 if min_active < u64::MAX {
3315 self.conflicts.prune_below(Epoch(min_active));
3316 }
3317
3318 if self.conflicts.conflicts(&write_keys, read_epoch) {
3322 return Err(MongrelError::Conflict(
3323 "write-write conflict (pre-validate, first-committer-wins)".into(),
3324 ));
3325 }
3326 let pre_validate_version = self.conflicts.version();
3327
3328 let mut spilled: Vec<SpilledRun> = Vec::new();
3332 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
3333 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
3337 {
3338 let mut table_bytes: HashMap<u64, usize> = HashMap::new();
3339 for (table_id, staged) in &staging {
3340 if let Staged::Put(cells) = staged {
3341 *table_bytes.entry(*table_id).or_default() += cells.len() * 16;
3342 }
3343 }
3344 let tables = self.tables.read();
3345 for (&table_id, &bytes) in &table_bytes {
3346 if bytes as u64
3347 <= self
3348 .spill_threshold
3349 .load(std::sync::atomic::Ordering::Relaxed)
3350 {
3351 continue;
3352 }
3353 let Some(handle) = tables.get(&table_id) else {
3354 continue;
3355 };
3356 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
3357 let mut t = handle.lock();
3358 let tdir = t.table_dir().to_path_buf();
3359 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
3360 std::fs::create_dir_all(&txn_dir)?;
3361 let run_id = t.alloc_run_id() as u128;
3362 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
3363
3364 let mut rows: Vec<Row> = Vec::new();
3365 for (tid, staged) in &staging {
3366 if *tid != table_id {
3367 continue;
3368 }
3369 if let Staged::Put(cells) = staged {
3370 t.validate_cells_not_null(cells)?;
3371 let row_id = t.alloc_row_id();
3372 let mut row = Row::new(row_id, Epoch(0));
3373 for (c, v) in cells {
3374 row.columns.insert(*c, v.clone());
3375 }
3376 rows.push(row);
3377 }
3378 }
3379 let schema = t.schema_ref().clone();
3380 let kek = t.kek_ref().cloned();
3381 let specs = t.indexable_column_specs();
3382 drop(t);
3383
3384 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
3385 .uniform_epoch(true);
3386 if let Some(ref kek) = kek {
3387 writer = writer.with_encryption(kek.as_ref(), specs);
3388 }
3389 let header = writer.write(&pending_path, &rows)?;
3390 let row_count = header.row_count;
3391 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
3392 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
3393
3394 spilled.push(SpilledRun {
3395 table_id,
3396 run_id,
3397 pending_path,
3398 rows,
3399 row_count,
3400 min_rid,
3401 max_rid,
3402 });
3403 spilled_tables.insert(table_id);
3404 }
3405 }
3406
3407 if spill_guard.is_some() {
3409 if let Some(hook) = self.spill_hook.lock().as_ref() {
3410 hook();
3411 }
3412 }
3413
3414 let mut prebuilt: Vec<Option<Row>> = Vec::with_capacity(staging.len());
3425 {
3426 let tables = self.tables.read();
3427 for (table_id, staged) in &staging {
3428 match staged {
3429 Staged::Put(cells) if !spilled_tables.contains(table_id) => {
3430 let handle = tables.get(table_id).ok_or_else(|| {
3431 MongrelError::NotFound(format!("table {table_id} not mounted"))
3432 })?;
3433 let mut t = handle.lock();
3434 t.validate_cells_not_null(cells)?;
3435 let row_id = t.alloc_row_id();
3436 drop(t);
3437 let mut row = Row::new(row_id, Epoch(0));
3438 for (c, v) in cells {
3439 row.columns.insert(*c, v.clone());
3440 }
3441 prebuilt.push(Some(row));
3442 }
3443 Staged::Put(_) | Staged::Delete(_) | Staged::Truncate => prebuilt.push(None),
3444 }
3445 }
3446 }
3447
3448 let mut prepared_external = Vec::with_capacity(external_states.len());
3449 for (name, state) in &external_states {
3450 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
3451 prepared_external.push((name.clone(), state.clone(), pending));
3452 }
3453
3454 let added_runs: Vec<crate::wal::AddedRun> = spilled
3456 .iter()
3457 .map(|s| crate::wal::AddedRun {
3458 table_id: s.table_id,
3459 run_id: s.run_id,
3460 row_count: s.row_count,
3461 level: 0,
3462 min_row_id: s.min_rid,
3463 max_row_id: s.max_rid,
3464 content_hash: [0u8; 32],
3465 })
3466 .collect();
3467 let (new_epoch, mut _epoch_guard, applies, commit_seq) = {
3468 let mut wal = self.shared_wal.lock();
3469
3470 if self.conflicts.version() != pre_validate_version
3475 && self.conflicts.conflicts(&write_keys, read_epoch)
3476 {
3477 drop(wal);
3481 for s in &spilled {
3482 if let Some(parent) = s.pending_path.parent() {
3483 let _ = std::fs::remove_dir_all(parent);
3484 }
3485 }
3486 for (_, _, pending) in &prepared_external {
3487 let _ = std::fs::remove_file(pending);
3488 }
3489 return Err(MongrelError::Conflict(
3490 "write-write conflict (sequencer delta re-check)".into(),
3491 ));
3492 }
3493
3494 let new_epoch = self.epoch.bump_assigned();
3495 let _epoch_guard = EpochGuard::new(self.epoch.as_ref(), new_epoch);
3496 let mut applies: Vec<(u64, Vec<StagedOp>)> = Vec::new();
3497
3498 for (idx, (table_id, staged)) in staging.iter().enumerate() {
3499 if spilled_tables.contains(table_id) && matches!(staged, Staged::Put(_)) {
3502 continue;
3503 }
3504 let mut ops = Vec::new();
3505 match staged {
3506 Staged::Put(_) => {
3507 let mut row = prebuilt[idx].take().expect("prebuilt put row");
3509 row.committed_epoch = new_epoch;
3510 let payload = bincode::serialize(&vec![row.clone()])
3511 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
3512 wal.append(
3513 txn_id,
3514 *table_id,
3515 Op::Put {
3516 table_id: *table_id,
3517 rows: payload,
3518 },
3519 )?;
3520 ops.push(StagedOp::Put(row));
3521 }
3522 Staged::Delete(rid) => {
3523 wal.append(
3524 txn_id,
3525 *table_id,
3526 Op::Delete {
3527 table_id: *table_id,
3528 row_ids: vec![*rid],
3529 },
3530 )?;
3531 ops.push(StagedOp::Delete(*rid));
3532 }
3533 Staged::Truncate => {
3534 wal.append(
3535 txn_id,
3536 *table_id,
3537 Op::TruncateTable {
3538 table_id: *table_id,
3539 },
3540 )?;
3541 ops.push(StagedOp::Truncate);
3542 }
3543 }
3544 applies.push((*table_id, ops));
3545 }
3546
3547 for (name, state, _) in &prepared_external {
3548 wal.append(
3549 txn_id,
3550 EXTERNAL_TABLE_ID,
3551 Op::ExternalTableState {
3552 name: name.clone(),
3553 state: state.clone(),
3554 },
3555 )?;
3556 }
3557
3558 let commit_seq = wal.append_commit(txn_id, new_epoch, &added_runs)?;
3559
3560 self.conflicts.record(&write_keys, new_epoch);
3565 (new_epoch, _epoch_guard, applies, commit_seq)
3566 };
3567
3568 self.group
3570 .await_durable(&self.shared_wal, commit_seq)
3571 .inspect_err(|_| {
3572 self.poisoned.store(true, Ordering::Relaxed);
3573 })?;
3574
3575 {
3577 let tables = self.tables.read();
3578 for s in &spilled {
3580 if let Some(handle) = tables.get(&s.table_id) {
3581 let mut t = handle.lock();
3582 let dest = t.run_path(s.run_id as u64);
3583 std::fs::rename(&s.pending_path, &dest)?;
3584 if let Some(parent) = s.pending_path.parent() {
3586 let _ = std::fs::remove_dir_all(parent);
3587 }
3588 t.link_run(crate::manifest::RunRef {
3589 run_id: s.run_id,
3590 level: 0,
3591 epoch_created: new_epoch.0,
3592 row_count: s.row_count,
3593 });
3594 t.apply_run_metadata(&s.rows)?;
3599 t.invalidate_pending_cache();
3600 t.persist_manifest(new_epoch)?;
3601 }
3602 }
3603 for (table_id, ops) in applies {
3605 if let Some(handle) = tables.get(&table_id) {
3606 let mut t = handle.lock();
3607 for op in ops {
3608 match op {
3609 StagedOp::Put(row) => t.apply_put_rows(vec![row])?,
3610 StagedOp::Delete(rid) => t.apply_delete(rid, new_epoch),
3611 StagedOp::Truncate => t.apply_truncate(new_epoch)?,
3612 }
3613 }
3614 t.invalidate_pending_cache();
3615 t.persist_manifest(new_epoch)?;
3616 }
3617 }
3618 }
3619 for (name, _, pending) in &prepared_external {
3620 publish_external_state_file(&self.root, name, pending)?;
3621 }
3622
3623 self.epoch.publish_in_order(new_epoch);
3624 _epoch_guard.disarm();
3625 Ok(new_epoch)
3626 }
3627
3628 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
3631 let e = self.epoch.visible();
3632 let g = self.snapshots.register(e);
3633 (Snapshot::at(e), g)
3634 }
3635
3636 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
3639 let e = self.epoch.visible();
3640 let g = self.snapshots.register_owned(e);
3641 (Snapshot::at(e), g)
3642 }
3643
3644 pub fn table_names(&self) -> Vec<String> {
3646 self.catalog
3647 .read()
3648 .tables
3649 .iter()
3650 .filter(|t| matches!(t.state, TableState::Live))
3651 .map(|t| t.name.clone())
3652 .collect()
3653 }
3654
3655 pub fn close(&self) -> Result<()> {
3661 for name in self.table_names() {
3662 if let Ok(handle) = self.table(&name) {
3663 if let Err(e) = handle.lock().close() {
3664 eprintln!("[close] flush failed for {name}: {e}");
3665 }
3666 }
3667 }
3668 Ok(())
3669 }
3670
3671 pub fn compact(&self) -> Result<(usize, usize)> {
3677 self.require(&crate::auth::Permission::Ddl)?;
3678 let mut compacted = 0;
3679 let mut skipped = 0;
3680 for name in self.table_names() {
3681 let Ok(handle) = self.table(&name) else {
3682 continue;
3683 };
3684 {
3685 let mut t = handle.lock();
3686 let before = t.run_count();
3687 if before < 2 {
3688 skipped += 1;
3689 continue;
3690 }
3691 match t.compact() {
3692 Ok(()) => {
3693 let after = t.run_count();
3694 compacted += 1;
3695 eprintln!("[compact] {name}: {before} -> {after} runs");
3696 }
3697 Err(e) => {
3698 eprintln!("[compact] {name}: compaction failed: {e}");
3699 skipped += 1;
3700 }
3701 }
3702 }
3703 }
3704 Ok((compacted, skipped))
3705 }
3706
3707 pub fn compact_table(&self, name: &str) -> Result<bool> {
3710 self.require(&crate::auth::Permission::Ddl)?;
3711 let handle = self.table(name)?;
3712 let mut t = handle.lock();
3713 let before = t.run_count();
3714 if before < 2 {
3715 return Ok(false);
3716 }
3717 t.compact()?;
3718 Ok(t.run_count() < before)
3719 }
3720
3721 pub fn table(&self, name: &str) -> Result<TableHandle> {
3723 let cat = self.catalog.read();
3724 let entry = cat
3725 .live(name)
3726 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
3727 let id = entry.table_id;
3728 drop(cat);
3729 self.tables
3730 .read()
3731 .get(&id)
3732 .cloned()
3733 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
3734 }
3735
3736 fn table_by_id(&self, id: u64) -> Result<TableHandle> {
3739 self.tables
3740 .read()
3741 .get(&id)
3742 .cloned()
3743 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
3744 }
3745
3746 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
3752 use crate::wal::DdlOp;
3753 use std::sync::atomic::Ordering;
3754
3755 self.require(&crate::auth::Permission::Ddl)?;
3756 if self.poisoned.load(Ordering::Relaxed) {
3757 return Err(MongrelError::Other(
3758 "database poisoned by fsync error".into(),
3759 ));
3760 }
3761
3762 let _g = self.ddl_lock.lock();
3763 {
3764 let cat = self.catalog.read();
3765 if cat.live(name).is_some() {
3766 return Err(MongrelError::InvalidArgument(format!(
3767 "table {name:?} already exists"
3768 )));
3769 }
3770 }
3771
3772 let commit_lock = Arc::clone(&self.commit_lock);
3775 let _c = commit_lock.lock();
3776 let table_id = {
3777 let mut cat = self.catalog.write();
3778 let id = cat.next_table_id;
3779 cat.next_table_id += 1;
3780 id
3781 };
3782 let epoch = self.epoch.bump_assigned();
3783 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3784 let txn_id = self.alloc_txn_id();
3785
3786 let mut schema = schema;
3790 schema.schema_id = table_id;
3791 schema.validate_auto_increment()?;
3798
3799 let schema_json = DdlOp::encode_schema(&schema)?;
3802 let commit_seq = {
3803 let mut wal = self.shared_wal.lock();
3804 wal.append(
3805 txn_id,
3806 table_id,
3807 crate::wal::Op::Ddl(DdlOp::CreateTable {
3808 table_id,
3809 name: name.to_string(),
3810 schema_json,
3811 }),
3812 )?;
3813 wal.append_commit(txn_id, epoch, &[])?
3814 };
3815 self.group
3816 .await_durable(&self.shared_wal, commit_seq)
3817 .inspect_err(|_| {
3818 self.poisoned.store(true, Ordering::Relaxed);
3819 })?;
3820
3821 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
3823 std::fs::create_dir_all(&tdir)?;
3824 let ctx = SharedCtx {
3825 epoch: Arc::clone(&self.epoch),
3826 page_cache: Arc::clone(&self.page_cache),
3827 decoded_cache: Arc::clone(&self.decoded_cache),
3828 snapshots: Arc::clone(&self.snapshots),
3829 kek: self.kek.clone(),
3830 commit_lock: Arc::clone(&self.commit_lock),
3831 shared: Some(crate::engine::SharedWalCtx {
3832 wal: Arc::clone(&self.shared_wal),
3833 group: Arc::clone(&self.group),
3834 poisoned: Arc::clone(&self.poisoned),
3835 txn_ids: Arc::clone(&self.next_txn_id),
3836 }),
3837 table_name: Some(name.to_string()),
3838 auth: self.table_auth_checker(),
3839 };
3840 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
3841
3842 {
3845 let mut cat = self.catalog.write();
3846 cat.tables.push(CatalogEntry {
3847 table_id,
3848 name: name.to_string(),
3849 schema,
3850 state: TableState::Live,
3851 created_epoch: epoch.0,
3852 });
3853 }
3854 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3855 self.tables
3856 .write()
3857 .insert(table_id, Arc::new(Mutex::new(table)));
3858
3859 self.epoch.publish_in_order(epoch);
3860 _epoch_guard.disarm();
3861 Ok(table_id)
3862 }
3863
3864 pub fn drop_table(&self, name: &str) -> Result<()> {
3866 use crate::wal::DdlOp;
3867 use std::sync::atomic::Ordering;
3868
3869 self.require(&crate::auth::Permission::Ddl)?;
3870 if self.poisoned.load(Ordering::Relaxed) {
3871 return Err(MongrelError::Other(
3872 "database poisoned by fsync error".into(),
3873 ));
3874 }
3875
3876 let _g = self.ddl_lock.lock();
3877 let table_id = {
3878 let cat = self.catalog.read();
3879 cat.live(name)
3880 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
3881 .table_id
3882 };
3883
3884 let commit_lock = Arc::clone(&self.commit_lock);
3885 let _c = commit_lock.lock();
3886 let epoch = self.epoch.bump_assigned();
3887 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3888 let txn_id = self.alloc_txn_id();
3889 let commit_seq = {
3890 let mut wal = self.shared_wal.lock();
3891 wal.append(
3892 txn_id,
3893 table_id,
3894 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
3895 )?;
3896 wal.append_commit(txn_id, epoch, &[])?
3897 };
3898 self.group
3899 .await_durable(&self.shared_wal, commit_seq)
3900 .inspect_err(|_| {
3901 self.poisoned.store(true, Ordering::Relaxed);
3902 })?;
3903
3904 {
3905 let mut cat = self.catalog.write();
3906 let entry = cat
3907 .tables
3908 .iter_mut()
3909 .find(|t| t.table_id == table_id)
3910 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
3911 entry.state = TableState::Dropped { at_epoch: epoch.0 };
3912 cat.triggers.retain(|trigger| {
3913 !matches!(
3914 &trigger.trigger.target,
3915 TriggerTarget::Table(target) if target == name
3916 )
3917 });
3918 }
3919 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3920 self.tables.write().remove(&table_id);
3921
3922 self.epoch.publish_in_order(epoch);
3923 _epoch_guard.disarm();
3924 Ok(())
3925 }
3926
3927 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
3936 use crate::wal::DdlOp;
3937 use std::sync::atomic::Ordering;
3938
3939 self.require(&crate::auth::Permission::Ddl)?;
3940 if self.poisoned.load(Ordering::Relaxed) {
3941 return Err(MongrelError::Other(
3942 "database poisoned by fsync error".into(),
3943 ));
3944 }
3945
3946 if name == new_name {
3949 return Ok(());
3950 }
3951 if new_name.is_empty() {
3952 return Err(MongrelError::InvalidArgument(
3953 "rename_table: new name must not be empty".into(),
3954 ));
3955 }
3956
3957 let _g = self.ddl_lock.lock();
3958 let table_id = {
3959 let cat = self.catalog.read();
3960 let src = cat
3961 .live(name)
3962 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
3963 if cat.live(new_name).is_some() {
3967 return Err(MongrelError::InvalidArgument(format!(
3968 "rename_table: a table named {new_name:?} already exists"
3969 )));
3970 }
3971 src.table_id
3972 };
3973
3974 let commit_lock = Arc::clone(&self.commit_lock);
3975 let _c = commit_lock.lock();
3976 let epoch = self.epoch.bump_assigned();
3977 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3978 let txn_id = self.alloc_txn_id();
3979 let commit_seq = {
3980 let mut wal = self.shared_wal.lock();
3981 wal.append(
3982 txn_id,
3983 table_id,
3984 crate::wal::Op::Ddl(DdlOp::RenameTable {
3985 table_id,
3986 new_name: new_name.to_string(),
3987 }),
3988 )?;
3989 wal.append_commit(txn_id, epoch, &[])?
3990 };
3991 self.group
3992 .await_durable(&self.shared_wal, commit_seq)
3993 .inspect_err(|_| {
3994 self.poisoned.store(true, Ordering::Relaxed);
3995 })?;
3996
3997 {
3998 let mut cat = self.catalog.write();
3999 let entry = cat
4000 .tables
4001 .iter_mut()
4002 .find(|t| t.table_id == table_id)
4003 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
4004 entry.name = new_name.to_string();
4005 for trigger in &mut cat.triggers {
4006 if matches!(
4007 &trigger.trigger.target,
4008 TriggerTarget::Table(target) if target == name
4009 ) {
4010 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
4011 }
4012 }
4013 }
4014 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
4015 self.epoch.publish_in_order(epoch);
4018 _epoch_guard.disarm();
4019 Ok(())
4020 }
4021
4022 pub fn alter_column(
4023 &self,
4024 table_name: &str,
4025 column_name: &str,
4026 change: AlterColumn,
4027 ) -> Result<ColumnDef> {
4028 use crate::wal::DdlOp;
4029 use std::sync::atomic::Ordering;
4030
4031 self.require(&crate::auth::Permission::Ddl)?;
4032 if self.poisoned.load(Ordering::Relaxed) {
4033 return Err(MongrelError::Other(
4034 "database poisoned by fsync error".into(),
4035 ));
4036 }
4037
4038 let _g = self.ddl_lock.lock();
4039 let table_id = {
4040 let cat = self.catalog.read();
4041 cat.live(table_name)
4042 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
4043 .table_id
4044 };
4045 let handle =
4046 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
4047 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
4048 })?;
4049 let mut table = handle.lock();
4050 let column = table.prepare_alter_column(column_name, &change)?;
4051 let renamed_column = (column.name != column_name).then(|| column.name.clone());
4052 if table
4053 .schema()
4054 .columns
4055 .iter()
4056 .find(|c| c.id == column.id)
4057 .is_some_and(|c| c == &column)
4058 {
4059 return Ok(column);
4060 }
4061
4062 let commit_lock = Arc::clone(&self.commit_lock);
4063 let _c = commit_lock.lock();
4064 let epoch = self.epoch.bump_assigned();
4065 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
4066 let txn_id = self.alloc_txn_id();
4067 let column_json = DdlOp::encode_column(&column)?;
4068 let commit_seq = {
4069 let mut wal = self.shared_wal.lock();
4070 wal.append(
4071 txn_id,
4072 table_id,
4073 crate::wal::Op::Ddl(DdlOp::AlterTable {
4074 table_id,
4075 column_json,
4076 }),
4077 )?;
4078 wal.append_commit(txn_id, epoch, &[])?
4079 };
4080 self.group
4081 .await_durable(&self.shared_wal, commit_seq)
4082 .inspect_err(|_| {
4083 self.poisoned.store(true, Ordering::Relaxed);
4084 })?;
4085
4086 table.apply_altered_column(column.clone())?;
4087 let schema = table.schema().clone();
4088 drop(table);
4089
4090 {
4091 let mut cat = self.catalog.write();
4092 let entry = cat
4093 .tables
4094 .iter_mut()
4095 .find(|t| t.table_id == table_id)
4096 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
4097 entry.schema = schema;
4098 if let Some(new_column_name) = renamed_column {
4099 for trigger in &mut cat.triggers {
4100 if matches!(
4101 &trigger.trigger.target,
4102 TriggerTarget::Table(target) if target == table_name
4103 ) {
4104 trigger.trigger = trigger.trigger.renamed_update_column(
4105 column_name,
4106 new_column_name.clone(),
4107 epoch.0,
4108 )?;
4109 }
4110 }
4111 }
4112 }
4113 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
4114
4115 self.epoch.publish_in_order(epoch);
4116 _epoch_guard.disarm();
4117 Ok(column)
4118 }
4119
4120 pub fn gc(&self) -> Result<usize> {
4126 self.require(&crate::auth::Permission::Ddl)?;
4127 let min_active = self.snapshots.min_active(self.epoch.visible()).0;
4128 let mut reclaimed = 0;
4129
4130 let cat = self.catalog.read();
4132 for entry in &cat.tables {
4133 if let TableState::Dropped { at_epoch } = entry.state {
4134 if at_epoch <= min_active {
4135 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
4136 if tdir.exists() {
4137 std::fs::remove_dir_all(&tdir)?;
4138 reclaimed += 1;
4139 }
4140 }
4141 }
4142 }
4143 drop(cat);
4144
4145 let cat = self.catalog.read();
4150 for entry in &cat.tables {
4151 if !matches!(entry.state, TableState::Live) {
4152 continue;
4153 }
4154 let txn_dir = self
4155 .root
4156 .join(TABLES_DIR)
4157 .join(entry.table_id.to_string())
4158 .join("_txn");
4159 if !txn_dir.exists() {
4160 continue;
4161 }
4162 for sub in std::fs::read_dir(&txn_dir)? {
4163 let sub = sub?;
4164 let name = sub.file_name();
4165 let Some(name) = name.to_str() else { continue };
4166 let is_active = name
4168 .parse::<u64>()
4169 .map(|id| self.active_spills.is_active(id))
4170 .unwrap_or(false);
4171 if is_active {
4172 continue;
4173 }
4174 std::fs::remove_dir_all(sub.path())?;
4175 reclaimed += 1;
4176 }
4177 }
4178 drop(cat);
4179
4180 let external_names = {
4181 let cat = self.catalog.read();
4182 cat.external_tables
4183 .iter()
4184 .map(|entry| entry.name.clone())
4185 .collect::<std::collections::HashSet<_>>()
4186 };
4187 let vtab_dir = self.root.join(VTAB_DIR);
4188 if vtab_dir.exists() {
4189 for entry in std::fs::read_dir(&vtab_dir)? {
4190 let entry = entry?;
4191 let name = entry.file_name();
4192 let Some(name) = name.to_str() else { continue };
4193 if external_names.contains(name) {
4194 continue;
4195 }
4196 let path = entry.path();
4197 if path.is_dir() {
4198 std::fs::remove_dir_all(path)?;
4199 } else {
4200 std::fs::remove_file(path)?;
4201 }
4202 reclaimed += 1;
4203 }
4204 }
4205
4206 let tables = self.tables.read();
4210 for handle in tables.values() {
4211 reclaimed += handle.lock().reap_retiring(Epoch(min_active))?;
4212 }
4213
4214 let all_durable = self.active_spills.is_idle()
4222 && tables.values().all(|h| {
4223 let g = h.lock();
4224 g.memtable_len() == 0 && g.mutable_run_len() == 0
4225 });
4226 drop(tables);
4227 if all_durable {
4228 reclaimed += self.shared_wal.lock().gc_segments(u64::MAX)?;
4229 }
4230
4231 Ok(reclaimed)
4232 }
4233
4234 pub fn checkpoint(&self) -> Result<()> {
4252 self.close()?;
4254
4255 let _ = self.compact()?;
4257
4258 self.gc()?;
4261
4262 let next_segment_no = self.catalog.read().next_segment_no;
4265 {
4266 let mut wal = self.shared_wal.lock();
4267 wal.rotate(next_segment_no)?;
4268 wal.gc_segments(u64::MAX)?;
4270 }
4271
4272 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
4274
4275 let tables = self.tables.read();
4278 let visible = self.epoch.visible();
4279 for handle in tables.values() {
4280 handle.lock().persist_manifest(visible)?;
4281 }
4282
4283 Ok(())
4284 }
4285 fn alloc_txn_id(&self) -> u64 {
4286 let mut g = self.next_txn_id.lock();
4287 let id = *g;
4288 *g = g.wrapping_add(1);
4289 id
4290 }
4291
4292 pub fn set_spill_threshold(&self, bytes: u64) {
4296 self.spill_threshold
4297 .store(bytes, std::sync::atomic::Ordering::Relaxed);
4298 }
4299
4300 #[doc(hidden)]
4304 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
4305 *self.spill_hook.lock() = Some(Box::new(f));
4306 }
4307
4308 #[doc(hidden)]
4312 pub fn __wal_group_sync_count(&self) -> u64 {
4313 self.shared_wal.lock().group_sync_count()
4314 }
4315
4316 #[doc(hidden)]
4319 pub fn __poison(&self) {
4320 self.poisoned
4321 .store(true, std::sync::atomic::Ordering::Relaxed);
4322 }
4323
4324 pub fn check(&self) -> Vec<CheckIssue> {
4337 let mut issues = Vec::new();
4338 let cat = self.catalog.read();
4339 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
4340 for entry in &cat.tables {
4341 if !matches!(entry.state, TableState::Live) {
4342 continue;
4343 }
4344 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
4345 let mut err = |sev: &str, desc: String| {
4346 issues.push(CheckIssue {
4347 table_id: entry.table_id,
4348 table_name: entry.name.clone(),
4349 severity: sev.into(),
4350 description: desc,
4351 });
4352 };
4353 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
4354 Ok(m) => m,
4355 Err(e) => {
4356 err("error", format!("manifest read failed: {e}"));
4357 continue;
4358 }
4359 };
4360 if m.flushed_epoch > m.current_epoch {
4361 err(
4362 "error",
4363 format!(
4364 "flushed_epoch {} exceeds current_epoch {} (impossible)",
4365 m.flushed_epoch, m.current_epoch
4366 ),
4367 );
4368 }
4369
4370 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
4371 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
4372 for rr in &m.runs {
4373 referenced.insert(rr.run_id);
4374 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
4375 if !run_path.exists() {
4376 err("error", format!("missing run file: r-{}.sr", rr.run_id));
4377 continue;
4378 }
4379 match crate::sorted_run::RunReader::open(
4380 &run_path,
4381 entry.schema.clone(),
4382 self.kek.clone(),
4383 ) {
4384 Ok(reader) => {
4385 if reader.row_count() as u64 != rr.row_count {
4386 err(
4387 "error",
4388 format!(
4389 "run r-{} row count mismatch: manifest {} vs run {}",
4390 rr.run_id,
4391 rr.row_count,
4392 reader.row_count()
4393 ),
4394 );
4395 }
4396 }
4397 Err(e) => {
4398 err(
4399 "error",
4400 format!("run r-{} integrity check failed: {e}", rr.run_id),
4401 );
4402 }
4403 }
4404 }
4405
4406 for r in &m.retiring {
4410 referenced.insert(r.run_id);
4411 }
4412
4413 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
4415 for ent in rd.flatten() {
4416 let p = ent.path();
4417 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
4418 continue;
4419 }
4420 let run_id = p
4421 .file_stem()
4422 .and_then(|s| s.to_str())
4423 .and_then(|s| s.strip_prefix("r-"))
4424 .and_then(|s| s.parse::<u128>().ok());
4425 if let Some(id) = run_id {
4426 if !referenced.contains(&id) {
4427 err(
4428 "warning",
4429 format!("orphan run file r-{id}.sr not referenced by the manifest"),
4430 );
4431 }
4432 }
4433 }
4434 }
4435 }
4436
4437 let external_names = cat
4438 .external_tables
4439 .iter()
4440 .map(|entry| entry.name.clone())
4441 .collect::<std::collections::HashSet<_>>();
4442 let vtab_dir = self.root.join(VTAB_DIR);
4443 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
4444 for entry in entries.flatten() {
4445 let name = entry.file_name();
4446 let Some(name) = name.to_str() else { continue };
4447 if !external_names.contains(name) {
4448 issues.push(CheckIssue {
4449 table_id: EXTERNAL_TABLE_ID,
4450 table_name: name.to_string(),
4451 severity: "warning".into(),
4452 description: format!(
4453 "orphan external table state entry {:?} not referenced by the catalog",
4454 entry.path()
4455 ),
4456 });
4457 }
4458 }
4459 }
4460
4461 for (seg, msg) in self.shared_wal.lock().verify_segments() {
4468 issues.push(CheckIssue {
4469 table_id: WAL_TABLE_ID,
4470 table_name: "<wal>".into(),
4471 severity: "error".into(),
4472 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
4473 });
4474 }
4475 issues
4476 }
4477
4478 pub fn doctor(&self) -> Result<Vec<u64>> {
4482 let _ddl = self.ddl_lock.lock();
4485 let issues = self.check();
4486 let bad_tables: std::collections::HashSet<u64> = issues
4491 .iter()
4492 .filter(|i| {
4493 i.severity == "error"
4494 && i.table_id != WAL_TABLE_ID
4495 && i.table_id != EXTERNAL_TABLE_ID
4496 })
4497 .map(|i| i.table_id)
4498 .collect();
4499 if bad_tables.is_empty() {
4500 return Ok(Vec::new());
4501 }
4502
4503 let qdir = self.root.join("_quarantine");
4504 std::fs::create_dir_all(&qdir)?;
4505 let mut quarantined = Vec::new();
4506 for &table_id in &bad_tables {
4507 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
4508 if tdir.exists() {
4509 let dest = qdir.join(table_id.to_string());
4510 std::fs::rename(&tdir, &dest)?;
4511 }
4512 {
4513 let mut cat = self.catalog.write();
4514 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
4515 entry.state = TableState::Dropped {
4516 at_epoch: self.epoch.visible().0,
4517 };
4518 }
4519 }
4520 self.tables.write().remove(&table_id);
4522 quarantined.push(table_id);
4523 }
4524 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
4525 Ok(quarantined)
4526 }
4527
4528 #[allow(dead_code)]
4530 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
4531 self.kek.as_ref()
4532 }
4533
4534 #[allow(dead_code)]
4536 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
4537 &self.epoch
4538 }
4539
4540 #[allow(dead_code)]
4542 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
4543 &self.snapshots
4544 }
4545}
4546
4547fn external_state_dir(root: &Path, name: &str) -> PathBuf {
4548 root.join(VTAB_DIR).join(name)
4549}
4550
4551fn filter_ignored_staging(
4552 staging: Vec<(u64, crate::txn::Staged)>,
4553 ignored_indices: &std::collections::BTreeSet<usize>,
4554) -> Vec<(u64, crate::txn::Staged)> {
4555 if ignored_indices.is_empty() {
4556 return staging;
4557 }
4558 staging
4559 .into_iter()
4560 .enumerate()
4561 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
4562 .collect()
4563}
4564
4565fn external_state_file(root: &Path, name: &str) -> PathBuf {
4566 external_state_dir(root, name).join("state.json")
4567}
4568
4569fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
4570 let path = external_state_file(root, name);
4571 match std::fs::read(path) {
4572 Ok(bytes) => Ok(bytes),
4573 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
4574 Err(e) => Err(e.into()),
4575 }
4576}
4577
4578fn current_external_state_bytes(
4579 root: &Path,
4580 external_states: &[(String, Vec<u8>)],
4581 name: &str,
4582) -> Result<Vec<u8>> {
4583 for (table, state) in external_states.iter().rev() {
4584 if table == name {
4585 return Ok(state.clone());
4586 }
4587 }
4588 read_external_state_file(root, name)
4589}
4590
4591fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
4592 let mut out = external_states;
4593 dedup_external_states_in_place(&mut out);
4594 out
4595}
4596
4597fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
4598 let mut seen = std::collections::HashSet::new();
4599 let mut out = Vec::with_capacity(external_states.len());
4600 for (name, state) in std::mem::take(external_states).into_iter().rev() {
4601 if seen.insert(name.clone()) {
4602 out.push((name, state));
4603 }
4604 }
4605 out.reverse();
4606 *external_states = out;
4607}
4608
4609fn prepare_external_state_file(
4610 root: &Path,
4611 name: &str,
4612 state: &[u8],
4613 txn_id: u64,
4614) -> Result<PathBuf> {
4615 let dir = external_state_dir(root, name);
4616 std::fs::create_dir_all(&dir)?;
4617 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
4618 {
4619 let mut file = std::fs::File::create(&pending)?;
4620 file.write_all(state)?;
4621 file.sync_all()?;
4622 }
4623 Ok(pending)
4624}
4625
4626fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
4627 let path = external_state_file(root, name);
4628 std::fs::rename(pending, &path)?;
4629 if let Ok(dir) = std::fs::File::open(external_state_dir(root, name)) {
4630 let _ = dir.sync_all();
4631 }
4632 Ok(())
4633}
4634
4635fn write_external_state_file(root: &Path, name: &str, state: &[u8]) -> Result<()> {
4636 let pending = prepare_external_state_file(root, name, state, 0)?;
4637 publish_external_state_file(root, name, &pending)
4638}
4639
4640fn recover_shared_wal(
4649 root: &Path,
4650 tables: &HashMap<u64, TableHandle>,
4651 epoch: &EpochAuthority,
4652 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
4653) -> Result<()> {
4654 use crate::memtable::Row;
4655 use crate::rowid::RowId;
4656 use crate::wal::{Op, SharedWal};
4657
4658 let records = SharedWal::replay_with_dek(root, wal_dek)?;
4659
4660 let mut committed: HashMap<u64, u64> = HashMap::new();
4662 let mut spilled_to_link: Vec<(
4663 u64, u64, Vec<crate::wal::AddedRun>,
4666 )> = Vec::new();
4667 for r in &records {
4668 if let Op::TxnCommit {
4669 epoch: ce,
4670 ref added_runs,
4671 } = r.op
4672 {
4673 committed.insert(r.txn_id, ce);
4674 if !added_runs.is_empty() {
4675 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
4676 }
4677 }
4678 }
4679
4680 type TableStage = (Vec<Row>, Vec<(RowId, Epoch)>, Option<Epoch>, Epoch);
4682 let mut stage: HashMap<u64, TableStage> = HashMap::new();
4683 let mut max_epoch = epoch.visible().0;
4684 for r in records {
4685 let Some(&ce) = committed.get(&r.txn_id) else {
4686 continue; };
4688 let commit_epoch = Epoch(ce);
4689 max_epoch = max_epoch.max(ce);
4690 match r.op {
4691 Op::Put { table_id, rows } => {
4692 let skip = tables
4694 .get(&table_id)
4695 .map(|h| h.lock().flushed_epoch() >= ce)
4696 .unwrap_or(true);
4697 if skip {
4698 continue;
4699 }
4700 let rows: Vec<Row> = match bincode::deserialize(&rows) {
4701 Ok(v) => v,
4702 Err(_) => continue,
4703 };
4704 let rows: Vec<Row> = rows
4707 .into_iter()
4708 .map(|mut row| {
4709 row.committed_epoch = commit_epoch;
4710 row
4711 })
4712 .collect();
4713 let entry = stage
4714 .entry(table_id)
4715 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
4716 entry.0.extend(rows);
4717 entry.3 = commit_epoch;
4718 }
4719 Op::Delete { table_id, row_ids } => {
4720 let skip = tables
4721 .get(&table_id)
4722 .map(|h| h.lock().flushed_epoch() >= ce)
4723 .unwrap_or(true);
4724 if skip {
4725 continue;
4726 }
4727 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
4728 let entry = stage
4729 .entry(table_id)
4730 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
4731 entry.1.extend(dels);
4732 entry.3 = commit_epoch;
4733 }
4734 Op::TruncateTable { table_id } => {
4735 let skip = tables
4736 .get(&table_id)
4737 .map(|h| h.lock().flushed_epoch() >= ce)
4738 .unwrap_or(true);
4739 if skip {
4740 continue;
4741 }
4742 stage.insert(
4743 table_id,
4744 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
4745 );
4746 }
4747 Op::ExternalTableState { name, state } => {
4748 write_external_state_file(root, &name, &state)?;
4749 }
4750 Op::Flush { .. } | Op::TxnCommit { .. } | Op::TxnAbort | Op::Ddl(_) => {}
4751 }
4752 }
4753 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
4754 let Some(handle) = tables.get(&table_id) else {
4755 continue;
4756 };
4757 let mut t = handle.lock();
4758 if let Some(epoch) = truncate_epoch {
4759 t.apply_truncate(epoch)?;
4760 }
4761 t.recover_apply(rows, deletes)?;
4762 if truncate_epoch.is_some() {
4763 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
4764 t.live_count = rows.len() as u64;
4765 t.persist_manifest(table_epoch)?;
4766 }
4767 }
4768
4769 for (txn_id, ce, added_runs) in &spilled_to_link {
4773 for ar in added_runs {
4774 let Some(handle) = tables.get(&ar.table_id) else {
4775 continue;
4776 };
4777 let mut t = handle.lock();
4778 let dest = t.run_path(ar.run_id as u64);
4779 if !dest.exists() {
4780 let pending = root
4781 .join(TABLES_DIR)
4782 .join(ar.table_id.to_string())
4783 .join("_txn")
4784 .join(txn_id.to_string())
4785 .join(format!("r-{}.sr", ar.run_id));
4786 if pending.exists() {
4787 if let Some(parent) = pending.parent() {
4788 std::fs::rename(&pending, &dest)?;
4789 let _ = std::fs::remove_dir_all(parent);
4790 }
4791 }
4792 }
4793 if t.run_path(ar.run_id as u64).exists() {
4799 t.recover_spilled_run(crate::manifest::RunRef {
4800 run_id: ar.run_id,
4801 level: ar.level,
4802 epoch_created: *ce,
4803 row_count: ar.row_count,
4804 });
4805 }
4806 }
4807 }
4808
4809 epoch.advance_recovered(Epoch(max_epoch));
4810 Ok(())
4811}
4812
4813fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
4814 match condition {
4815 ProcedureCondition::Pk { .. } => {
4816 if schema.primary_key().is_none() {
4817 return Err(MongrelError::InvalidArgument(
4818 "procedure condition Pk references a table without a primary key".into(),
4819 ));
4820 }
4821 }
4822 ProcedureCondition::BitmapEq { column_id, .. }
4823 | ProcedureCondition::BitmapIn { column_id, .. }
4824 | ProcedureCondition::Range { column_id, .. }
4825 | ProcedureCondition::RangeF64 { column_id, .. }
4826 | ProcedureCondition::IsNull { column_id }
4827 | ProcedureCondition::IsNotNull { column_id }
4828 | ProcedureCondition::FmContains { column_id, .. } => {
4829 validate_column_id(*column_id, schema)?;
4830 }
4831 }
4832 Ok(())
4833}
4834
4835fn bind_procedure_args(
4836 procedure: &StoredProcedure,
4837 mut args: HashMap<String, crate::Value>,
4838) -> Result<HashMap<String, crate::Value>> {
4839 let mut out = HashMap::new();
4840 for param in &procedure.params {
4841 let value = match args.remove(¶m.name) {
4842 Some(value) => value,
4843 None => param.default.clone().ok_or_else(|| {
4844 MongrelError::InvalidArgument(format!(
4845 "missing required procedure parameter {:?}",
4846 param.name
4847 ))
4848 })?,
4849 };
4850 if !param.nullable && matches!(value, crate::Value::Null) {
4851 return Err(MongrelError::InvalidArgument(format!(
4852 "procedure parameter {:?} must not be NULL",
4853 param.name
4854 )));
4855 }
4856 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty.clone()) {
4857 return Err(MongrelError::InvalidArgument(format!(
4858 "procedure parameter {:?} has wrong type",
4859 param.name
4860 )));
4861 }
4862 out.insert(param.name.clone(), value);
4863 }
4864 if let Some(extra) = args.keys().next() {
4865 return Err(MongrelError::InvalidArgument(format!(
4866 "unknown procedure parameter {extra:?}"
4867 )));
4868 }
4869 Ok(out)
4870}
4871
4872fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
4873 matches!(
4874 (value, ty),
4875 (crate::Value::Bool(_), crate::TypeId::Bool)
4876 | (crate::Value::Int64(_), crate::TypeId::Int8)
4877 | (crate::Value::Int64(_), crate::TypeId::Int16)
4878 | (crate::Value::Int64(_), crate::TypeId::Int32)
4879 | (crate::Value::Int64(_), crate::TypeId::Int64)
4880 | (crate::Value::Int64(_), crate::TypeId::UInt8)
4881 | (crate::Value::Int64(_), crate::TypeId::UInt16)
4882 | (crate::Value::Int64(_), crate::TypeId::UInt32)
4883 | (crate::Value::Int64(_), crate::TypeId::UInt64)
4884 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
4885 | (crate::Value::Int64(_), crate::TypeId::Date32)
4886 | (crate::Value::Float64(_), crate::TypeId::Float32)
4887 | (crate::Value::Float64(_), crate::TypeId::Float64)
4888 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
4889 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
4890 )
4891}
4892
4893fn eval_cells(
4894 cells: &[crate::procedure::ProcedureCell],
4895 args: &HashMap<String, crate::Value>,
4896 outputs: &HashMap<String, ProcedureCallOutput>,
4897) -> Result<Vec<(u16, crate::Value)>> {
4898 cells
4899 .iter()
4900 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
4901 .collect()
4902}
4903
4904fn eval_condition(
4905 condition: &ProcedureCondition,
4906 args: &HashMap<String, crate::Value>,
4907 outputs: &HashMap<String, ProcedureCallOutput>,
4908) -> Result<crate::Condition> {
4909 Ok(match condition {
4910 ProcedureCondition::Pk { value } => {
4911 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
4912 }
4913 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
4914 column_id: *column_id,
4915 value: eval_value(value, args, outputs)?.encode_key(),
4916 },
4917 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
4918 column_id: *column_id,
4919 values: values
4920 .iter()
4921 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
4922 .collect::<Result<Vec<_>>>()?,
4923 },
4924 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
4925 column_id: *column_id,
4926 lo: expect_i64(eval_value(lo, args, outputs)?)?,
4927 hi: expect_i64(eval_value(hi, args, outputs)?)?,
4928 },
4929 ProcedureCondition::RangeF64 {
4930 column_id,
4931 lo,
4932 lo_inclusive,
4933 hi,
4934 hi_inclusive,
4935 } => crate::Condition::RangeF64 {
4936 column_id: *column_id,
4937 lo: expect_f64(eval_value(lo, args, outputs)?)?,
4938 lo_inclusive: *lo_inclusive,
4939 hi: expect_f64(eval_value(hi, args, outputs)?)?,
4940 hi_inclusive: *hi_inclusive,
4941 },
4942 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
4943 column_id: *column_id,
4944 },
4945 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
4946 column_id: *column_id,
4947 },
4948 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
4949 column_id: *column_id,
4950 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
4951 },
4952 })
4953}
4954
4955fn eval_value(
4956 value: &ProcedureValue,
4957 args: &HashMap<String, crate::Value>,
4958 outputs: &HashMap<String, ProcedureCallOutput>,
4959) -> Result<crate::Value> {
4960 match value {
4961 ProcedureValue::Literal(value) => Ok(value.clone()),
4962 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
4963 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
4964 }),
4965 ProcedureValue::StepScalar(id) => match outputs.get(id) {
4966 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
4967 _ => Err(MongrelError::InvalidArgument(format!(
4968 "procedure step {id:?} did not return a scalar"
4969 ))),
4970 },
4971 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
4972 Err(MongrelError::InvalidArgument(
4973 "row-valued procedure reference cannot be used as a scalar".into(),
4974 ))
4975 }
4976 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
4977 "structured procedure value cannot be used as a scalar cell".into(),
4978 )),
4979 }
4980}
4981
4982fn eval_return_output(
4983 value: &ProcedureValue,
4984 args: &HashMap<String, crate::Value>,
4985 outputs: &HashMap<String, ProcedureCallOutput>,
4986) -> Result<ProcedureCallOutput> {
4987 match value {
4988 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
4989 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
4990 args.get(name).cloned().ok_or_else(|| {
4991 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
4992 })?,
4993 )),
4994 ProcedureValue::StepRows(id)
4995 | ProcedureValue::StepRow(id)
4996 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
4997 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
4998 }),
4999 ProcedureValue::Object(fields) => {
5000 let mut out = Vec::with_capacity(fields.len());
5001 for (name, value) in fields {
5002 out.push((name.clone(), eval_return_output(value, args, outputs)?));
5003 }
5004 Ok(ProcedureCallOutput::Object(out))
5005 }
5006 ProcedureValue::Array(values) => {
5007 let mut out = Vec::with_capacity(values.len());
5008 for value in values {
5009 out.push(eval_return_output(value, args, outputs)?);
5010 }
5011 Ok(ProcedureCallOutput::Array(out))
5012 }
5013 }
5014}
5015
5016fn expect_i64(value: crate::Value) -> Result<i64> {
5017 match value {
5018 crate::Value::Int64(value) => Ok(value),
5019 _ => Err(MongrelError::InvalidArgument(
5020 "procedure value must be Int64".into(),
5021 )),
5022 }
5023}
5024
5025fn expect_f64(value: crate::Value) -> Result<f64> {
5026 match value {
5027 crate::Value::Float64(value) => Ok(value),
5028 _ => Err(MongrelError::InvalidArgument(
5029 "procedure value must be Float64".into(),
5030 )),
5031 }
5032}
5033
5034fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
5035 match value {
5036 crate::Value::Bytes(value) => Ok(value),
5037 _ => Err(MongrelError::InvalidArgument(
5038 "procedure value must be Bytes".into(),
5039 )),
5040 }
5041}
5042
5043fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
5044 if schema.columns.iter().any(|c| c.id == column_id) {
5045 Ok(())
5046 } else {
5047 Err(MongrelError::InvalidArgument(format!(
5048 "unknown column id {column_id}"
5049 )))
5050 }
5051}
5052
5053fn trigger_matches_event(
5054 trigger: &StoredTrigger,
5055 event: &WriteEvent,
5056 cat: &Catalog,
5057) -> Result<bool> {
5058 if trigger.event != event.kind {
5059 return Ok(false);
5060 }
5061 let TriggerTarget::Table(target) = &trigger.target else {
5062 return Ok(false);
5063 };
5064 if target != &event.table {
5065 return Ok(false);
5066 }
5067 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
5068 let schema = &cat
5069 .live(target)
5070 .ok_or_else(|| {
5071 MongrelError::InvalidArgument(format!(
5072 "trigger {:?} references unknown table {target:?}",
5073 trigger.name
5074 ))
5075 })?
5076 .schema;
5077 let mut watched = Vec::with_capacity(trigger.update_of.len());
5078 for name in &trigger.update_of {
5079 let col = schema.column(name).ok_or_else(|| {
5080 MongrelError::InvalidArgument(format!(
5081 "trigger {:?} references unknown UPDATE OF column {name:?}",
5082 trigger.name
5083 ))
5084 })?;
5085 watched.push(col.id);
5086 }
5087 if !event
5088 .changed_columns
5089 .iter()
5090 .any(|column_id| watched.contains(column_id))
5091 {
5092 return Ok(false);
5093 }
5094 }
5095 Ok(true)
5096}
5097
5098fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
5099 let mut ids = std::collections::BTreeSet::new();
5100 if let Some(old) = old {
5101 ids.extend(old.columns.keys().copied());
5102 }
5103 if let Some(new) = new {
5104 ids.extend(new.columns.keys().copied());
5105 }
5106 ids.into_iter()
5107 .filter(|id| {
5108 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
5109 })
5110 .collect()
5111}
5112
5113fn eval_trigger_cells(
5114 cells: &[crate::trigger::TriggerCell],
5115 event: &WriteEvent,
5116 selected: Option<&TriggerRowImage>,
5117) -> Result<Vec<(u16, Value)>> {
5118 cells
5119 .iter()
5120 .map(|cell| {
5121 Ok((
5122 cell.column_id,
5123 eval_trigger_value(&cell.value, event, selected)?,
5124 ))
5125 })
5126 .collect()
5127}
5128
5129fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
5130 match expr {
5131 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
5132 Value::Bool(value) => Ok(value),
5133 Value::Null => Ok(false),
5134 other => Err(MongrelError::InvalidArgument(format!(
5135 "trigger WHEN value must be boolean, got {other:?}"
5136 ))),
5137 },
5138 TriggerExpr::Eq { left, right } => Ok(values_equal(
5139 &eval_trigger_value(left, event, None)?,
5140 &eval_trigger_value(right, event, None)?,
5141 )),
5142 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
5143 &eval_trigger_value(left, event, None)?,
5144 &eval_trigger_value(right, event, None)?,
5145 )),
5146 TriggerExpr::Lt { left, right } => match value_order(
5147 &eval_trigger_value(left, event, None)?,
5148 &eval_trigger_value(right, event, None)?,
5149 ) {
5150 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
5151 None => Ok(false),
5152 },
5153 TriggerExpr::Lte { left, right } => match value_order(
5154 &eval_trigger_value(left, event, None)?,
5155 &eval_trigger_value(right, event, None)?,
5156 ) {
5157 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
5158 None => Ok(false),
5159 },
5160 TriggerExpr::Gt { left, right } => match value_order(
5161 &eval_trigger_value(left, event, None)?,
5162 &eval_trigger_value(right, event, None)?,
5163 ) {
5164 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
5165 None => Ok(false),
5166 },
5167 TriggerExpr::Gte { left, right } => match value_order(
5168 &eval_trigger_value(left, event, None)?,
5169 &eval_trigger_value(right, event, None)?,
5170 ) {
5171 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
5172 None => Ok(false),
5173 },
5174 TriggerExpr::IsNull(value) => Ok(matches!(
5175 eval_trigger_value(value, event, None)?,
5176 Value::Null
5177 )),
5178 TriggerExpr::IsNotNull(value) => Ok(!matches!(
5179 eval_trigger_value(value, event, None)?,
5180 Value::Null
5181 )),
5182 TriggerExpr::And { left, right } => {
5183 if !eval_trigger_expr(left, event)? {
5184 Ok(false)
5185 } else {
5186 Ok(eval_trigger_expr(right, event)?)
5187 }
5188 }
5189 TriggerExpr::Or { left, right } => {
5190 if eval_trigger_expr(left, event)? {
5191 Ok(true)
5192 } else {
5193 Ok(eval_trigger_expr(right, event)?)
5194 }
5195 }
5196 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
5197 }
5198}
5199
5200fn eval_trigger_condition(
5201 condition: &TriggerCondition,
5202 event: &WriteEvent,
5203 selected: &TriggerRowImage,
5204 schema: &Schema,
5205) -> Result<bool> {
5206 match condition {
5207 TriggerCondition::Pk { value } => {
5208 let pk = schema.primary_key().ok_or_else(|| {
5209 MongrelError::InvalidArgument(
5210 "trigger condition Pk references a table without a primary key".into(),
5211 )
5212 })?;
5213 let lhs = eval_trigger_value(value, event, Some(selected))?;
5214 Ok(values_equal(
5215 &lhs,
5216 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
5217 ))
5218 }
5219 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
5220 selected.columns.get(column_id).unwrap_or(&Value::Null),
5221 &eval_trigger_value(value, event, Some(selected))?,
5222 )),
5223 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
5224 selected.columns.get(column_id).unwrap_or(&Value::Null),
5225 &eval_trigger_value(value, event, Some(selected))?,
5226 )),
5227 TriggerCondition::Lt { column_id, value } => match value_order(
5228 selected.columns.get(column_id).unwrap_or(&Value::Null),
5229 &eval_trigger_value(value, event, Some(selected))?,
5230 ) {
5231 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
5232 None => Ok(false),
5233 },
5234 TriggerCondition::Lte { column_id, value } => match value_order(
5235 selected.columns.get(column_id).unwrap_or(&Value::Null),
5236 &eval_trigger_value(value, event, Some(selected))?,
5237 ) {
5238 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
5239 None => Ok(false),
5240 },
5241 TriggerCondition::Gt { column_id, value } => match value_order(
5242 selected.columns.get(column_id).unwrap_or(&Value::Null),
5243 &eval_trigger_value(value, event, Some(selected))?,
5244 ) {
5245 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
5246 None => Ok(false),
5247 },
5248 TriggerCondition::Gte { column_id, value } => match value_order(
5249 selected.columns.get(column_id).unwrap_or(&Value::Null),
5250 &eval_trigger_value(value, event, Some(selected))?,
5251 ) {
5252 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
5253 None => Ok(false),
5254 },
5255 TriggerCondition::IsNull { column_id } => Ok(matches!(
5256 selected.columns.get(column_id),
5257 None | Some(Value::Null)
5258 )),
5259 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
5260 selected.columns.get(column_id),
5261 None | Some(Value::Null)
5262 )),
5263 TriggerCondition::And { left, right } => {
5264 if !eval_trigger_condition(left, event, selected, schema)? {
5265 Ok(false)
5266 } else {
5267 Ok(eval_trigger_condition(right, event, selected, schema)?)
5268 }
5269 }
5270 TriggerCondition::Or { left, right } => {
5271 if eval_trigger_condition(left, event, selected, schema)? {
5272 Ok(true)
5273 } else {
5274 Ok(eval_trigger_condition(right, event, selected, schema)?)
5275 }
5276 }
5277 TriggerCondition::Not(condition) => {
5278 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
5279 }
5280 }
5281}
5282
5283fn eval_trigger_value(
5284 value: &TriggerValue,
5285 event: &WriteEvent,
5286 selected: Option<&TriggerRowImage>,
5287) -> Result<Value> {
5288 match value {
5289 TriggerValue::Literal(value) => Ok(value.clone()),
5290 TriggerValue::NewColumn(column_id) => event
5291 .new
5292 .as_ref()
5293 .and_then(|row| row.columns.get(column_id))
5294 .cloned()
5295 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
5296 TriggerValue::OldColumn(column_id) => event
5297 .old
5298 .as_ref()
5299 .and_then(|row| row.columns.get(column_id))
5300 .cloned()
5301 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
5302 TriggerValue::SelectedColumn(column_id) => selected
5303 .and_then(|row| row.columns.get(column_id))
5304 .cloned()
5305 .ok_or_else(|| {
5306 MongrelError::InvalidArgument("SELECTED column is not available".into())
5307 }),
5308 }
5309}
5310
5311fn values_equal(left: &Value, right: &Value) -> bool {
5312 match (left, right) {
5313 (Value::Null, Value::Null) => true,
5314 (Value::Bool(a), Value::Bool(b)) => a == b,
5315 (Value::Int64(a), Value::Int64(b)) => a == b,
5316 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
5317 (Value::Bytes(a), Value::Bytes(b)) => a == b,
5318 (Value::Embedding(a), Value::Embedding(b)) => {
5319 a.len() == b.len()
5320 && a.iter()
5321 .zip(b.iter())
5322 .all(|(a, b)| a.to_bits() == b.to_bits())
5323 }
5324 _ => false,
5325 }
5326}
5327
5328fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
5329 match (left, right) {
5330 (Value::Null, _) | (_, Value::Null) => None,
5331 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
5332 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
5333 (Value::Int64(a), Value::Float64(b)) => {
5336 let af = *a as f64;
5337 Some(af.total_cmp(b))
5338 }
5339 (Value::Float64(a), Value::Int64(b)) => {
5342 let bf = *b as f64;
5343 Some(a.total_cmp(&bf))
5344 }
5345 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
5346 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
5347 (Value::Embedding(_), Value::Embedding(_)) => None,
5348 _ => None,
5349 }
5350}
5351
5352fn trigger_message(value: Value) -> String {
5353 match value {
5354 Value::Null => "NULL".into(),
5355 Value::Bool(value) => value.to_string(),
5356 Value::Int64(value) => value.to_string(),
5357 Value::Float64(value) => value.to_string(),
5358 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
5359 Value::Embedding(value) => format!("{value:?}"),
5360 Value::Decimal(value) => value.to_string(),
5361 Value::Interval {
5362 months,
5363 days,
5364 nanos,
5365 } => format!("{months}m {days}d {nanos}ns"),
5366 Value::Uuid(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
5367 Value::Json(b) => String::from_utf8_lossy(&b).into_owned(),
5368 }
5369}
5370
5371fn validate_trigger_step<'a>(
5372 step: &TriggerStep,
5373 cat: &'a Catalog,
5374 target_schema: &Schema,
5375 event: TriggerEvent,
5376 select_schemas: &mut HashMap<String, &'a Schema>,
5377) -> Result<()> {
5378 match step {
5379 TriggerStep::SetNew { cells } => {
5380 if event == TriggerEvent::Delete {
5381 return Err(MongrelError::InvalidArgument(
5382 "SetNew trigger step is not valid for DELETE triggers".into(),
5383 ));
5384 }
5385 for cell in cells {
5386 validate_column_id(cell.column_id, target_schema)?;
5387 validate_trigger_value(&cell.value, target_schema, event)?;
5388 }
5389 }
5390 TriggerStep::Insert { table, cells } => {
5391 let schema = trigger_write_schema(cat, table, "insert")?;
5392 for cell in cells {
5393 validate_column_id(cell.column_id, schema)?;
5394 validate_trigger_value(&cell.value, target_schema, event)?;
5395 }
5396 }
5397 TriggerStep::UpdateByPk { table, pk, cells } => {
5398 let schema = trigger_write_schema(cat, table, "update")?;
5399 if schema.primary_key().is_none() {
5400 return Err(MongrelError::InvalidArgument(format!(
5401 "trigger update_by_pk references table {table:?} without a primary key"
5402 )));
5403 }
5404 validate_trigger_value(pk, target_schema, event)?;
5405 for cell in cells {
5406 validate_column_id(cell.column_id, schema)?;
5407 validate_trigger_value(&cell.value, target_schema, event)?;
5408 }
5409 }
5410 TriggerStep::DeleteByPk { table, pk } => {
5411 let schema = trigger_write_schema(cat, table, "delete")?;
5412 if schema.primary_key().is_none() {
5413 return Err(MongrelError::InvalidArgument(format!(
5414 "trigger delete_by_pk references table {table:?} without a primary key"
5415 )));
5416 }
5417 validate_trigger_value(pk, target_schema, event)?;
5418 }
5419 TriggerStep::Select {
5420 id,
5421 table,
5422 conditions,
5423 } => {
5424 let schema = trigger_read_schema(cat, table)?;
5425 for condition in conditions {
5426 validate_trigger_condition(condition, schema, target_schema, event)?;
5427 }
5428 if select_schemas.contains_key(id) {
5429 return Err(MongrelError::InvalidArgument(format!(
5430 "duplicate select id {id:?} in trigger program"
5431 )));
5432 }
5433 select_schemas.insert(id.clone(), schema);
5434 }
5435 TriggerStep::Foreach { id, steps } => {
5436 if !select_schemas.contains_key(id) {
5437 return Err(MongrelError::InvalidArgument(format!(
5438 "foreach references unknown select id {id:?}"
5439 )));
5440 }
5441 let mut inner_select_schemas = select_schemas.clone();
5442 for step in steps {
5443 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
5444 }
5445 }
5446 TriggerStep::DeleteWhere { table, conditions } => {
5447 let schema = trigger_write_schema(cat, table, "delete")?;
5448 for condition in conditions {
5449 validate_trigger_condition(condition, schema, target_schema, event)?;
5450 }
5451 }
5452 TriggerStep::UpdateWhere {
5453 table,
5454 conditions,
5455 cells,
5456 } => {
5457 let schema = trigger_write_schema(cat, table, "update")?;
5458 for condition in conditions {
5459 validate_trigger_condition(condition, schema, target_schema, event)?;
5460 }
5461 for cell in cells {
5462 validate_column_id(cell.column_id, schema)?;
5463 validate_trigger_value(&cell.value, target_schema, event)?;
5464 }
5465 }
5466 TriggerStep::Raise { message, .. } => {
5467 validate_trigger_value(message, target_schema, event)?
5468 }
5469 }
5470 Ok(())
5471}
5472
5473fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
5474 if let Some(entry) = cat.live(table) {
5475 return Ok(&entry.schema);
5476 }
5477 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
5478 let allowed = match op {
5479 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
5480 "update" | "delete" => entry.capabilities.writable,
5481 _ => false,
5482 };
5483 if !allowed {
5484 return Err(MongrelError::InvalidArgument(format!(
5485 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
5486 entry.module
5487 )));
5488 }
5489 if !entry.capabilities.transaction_safe {
5490 return Err(MongrelError::InvalidArgument(format!(
5491 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
5492 entry.module
5493 )));
5494 }
5495 return Ok(&entry.declared_schema);
5496 }
5497 Err(MongrelError::InvalidArgument(format!(
5498 "trigger references unknown table {table:?}"
5499 )))
5500}
5501
5502fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
5503 if let Some(entry) = cat.live(table) {
5504 return Ok(&entry.schema);
5505 }
5506 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
5507 if entry.capabilities.trigger_safe {
5508 return Ok(&entry.declared_schema);
5509 }
5510 return Err(MongrelError::InvalidArgument(format!(
5511 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
5512 entry.module
5513 )));
5514 }
5515 Err(MongrelError::InvalidArgument(format!(
5516 "trigger references unknown table {table:?}"
5517 )))
5518}
5519
5520fn validate_trigger_condition(
5521 condition: &TriggerCondition,
5522 schema: &Schema,
5523 target_schema: &Schema,
5524 event: TriggerEvent,
5525) -> Result<()> {
5526 match condition {
5527 TriggerCondition::Pk { value } => {
5528 if schema.primary_key().is_none() {
5529 return Err(MongrelError::InvalidArgument(
5530 "trigger condition Pk references a table without a primary key".into(),
5531 ));
5532 }
5533 validate_trigger_value(value, target_schema, event)
5534 }
5535 TriggerCondition::Eq { column_id, value }
5536 | TriggerCondition::NotEq { column_id, value }
5537 | TriggerCondition::Lt { column_id, value }
5538 | TriggerCondition::Lte { column_id, value }
5539 | TriggerCondition::Gt { column_id, value }
5540 | TriggerCondition::Gte { column_id, value } => {
5541 validate_column_id(*column_id, schema)?;
5542 validate_trigger_value(value, target_schema, event)
5543 }
5544 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
5545 validate_column_id(*column_id, schema)
5546 }
5547 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
5548 validate_trigger_condition(left, schema, target_schema, event)?;
5549 validate_trigger_condition(right, schema, target_schema, event)
5550 }
5551 TriggerCondition::Not(condition) => {
5552 validate_trigger_condition(condition, schema, target_schema, event)
5553 }
5554 }
5555}
5556
5557fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
5558 match expr {
5559 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
5560 validate_trigger_value(value, schema, event)
5561 }
5562 TriggerExpr::Eq { left, right }
5563 | TriggerExpr::NotEq { left, right }
5564 | TriggerExpr::Lt { left, right }
5565 | TriggerExpr::Lte { left, right }
5566 | TriggerExpr::Gt { left, right }
5567 | TriggerExpr::Gte { left, right } => {
5568 validate_trigger_value(left, schema, event)?;
5569 validate_trigger_value(right, schema, event)
5570 }
5571 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
5572 validate_trigger_expr(left, schema, event)?;
5573 validate_trigger_expr(right, schema, event)
5574 }
5575 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
5576 }
5577}
5578
5579fn validate_trigger_value(
5580 value: &TriggerValue,
5581 schema: &Schema,
5582 event: TriggerEvent,
5583) -> Result<()> {
5584 match value {
5585 TriggerValue::Literal(_) => Ok(()),
5586 TriggerValue::NewColumn(id) => {
5587 if event == TriggerEvent::Delete {
5588 return Err(MongrelError::InvalidArgument(
5589 "DELETE triggers cannot reference NEW".into(),
5590 ));
5591 }
5592 validate_column_id(*id, schema)
5593 }
5594 TriggerValue::OldColumn(id) => {
5595 if event == TriggerEvent::Insert {
5596 return Err(MongrelError::InvalidArgument(
5597 "INSERT triggers cannot reference OLD".into(),
5598 ));
5599 }
5600 validate_column_id(*id, schema)
5601 }
5602 TriggerValue::SelectedColumn(_) => Ok(()),
5606 }
5607}
5608
5609fn recover_ddl_from_wal(
5615 root: &Path,
5616 cat: &mut Catalog,
5617 meta_dek: Option<&[u8; META_DEK_LEN]>,
5618 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
5619) -> Result<()> {
5620 use crate::wal::{DdlOp, Op, SharedWal};
5621
5622 let records = match SharedWal::replay_with_dek(root, wal_dek) {
5623 Ok(r) => r,
5624 Err(_) => return Ok(()),
5625 };
5626
5627 let mut committed: HashMap<u64, u64> = HashMap::new();
5628 for r in &records {
5629 if let Op::TxnCommit { epoch: ce, .. } = r.op {
5630 committed.insert(r.txn_id, ce);
5631 }
5632 }
5633
5634 let mut changed = false;
5635 for r in records {
5636 let Some(&ce) = committed.get(&r.txn_id) else {
5637 continue;
5638 };
5639 match r.op {
5640 Op::Ddl(DdlOp::CreateTable {
5641 table_id,
5642 ref name,
5643 ref schema_json,
5644 }) => {
5645 if cat.tables.iter().any(|t| t.table_id == table_id) {
5646 continue;
5647 }
5648 let schema = DdlOp::decode_schema(schema_json)?;
5649 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
5650 if !tdir.exists() {
5651 std::fs::create_dir_all(tdir.join(crate::engine::WAL_DIR))?;
5652 std::fs::create_dir_all(tdir.join(crate::engine::RUNS_DIR))?;
5653 crate::engine::write_schema(&tdir, &schema)?;
5654 let mut m = crate::manifest::Manifest::new(table_id, schema.schema_id);
5660 crate::manifest::write_atomic(&tdir, &mut m, meta_dek)?;
5661 }
5662 cat.tables.push(CatalogEntry {
5663 table_id,
5664 name: name.clone(),
5665 schema,
5666 state: TableState::Live,
5667 created_epoch: ce,
5668 });
5669 cat.next_table_id = cat.next_table_id.max(table_id + 1);
5670 changed = true;
5671 }
5672 Op::Ddl(DdlOp::DropTable { table_id }) => {
5673 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
5674 if matches!(entry.state, TableState::Live) {
5675 entry.state = TableState::Dropped { at_epoch: ce };
5676 changed = true;
5677 }
5678 }
5679 }
5680 Op::Ddl(DdlOp::RenameTable {
5681 table_id,
5682 ref new_name,
5683 }) => {
5684 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
5685 if entry.name != *new_name {
5686 entry.name = new_name.clone();
5687 changed = true;
5688 }
5689 }
5690 }
5694 Op::Ddl(DdlOp::AlterTable {
5695 table_id,
5696 ref column_json,
5697 }) => {
5698 let column = DdlOp::decode_column(column_json)?;
5699 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
5700 if apply_recovered_column_def(&mut entry.schema, column) {
5701 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
5702 if tdir.exists() {
5703 crate::engine::write_schema(&tdir, &entry.schema)?;
5704 }
5705 changed = true;
5706 }
5707 }
5708 }
5709 _ => {}
5710 }
5711 }
5712
5713 if changed {
5714 catalog::write_atomic(root, cat, meta_dek)?;
5715 }
5716 Ok(())
5717}
5718
5719fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> bool {
5720 match schema.columns.iter_mut().find(|c| c.id == column.id) {
5721 Some(existing) if *existing == column => false,
5722 Some(existing) => {
5723 *existing = column;
5724 schema.schema_id = schema.schema_id.saturating_add(1);
5725 true
5726 }
5727 None => {
5728 schema.columns.push(column);
5729 schema.schema_id = schema.schema_id.saturating_add(1);
5730 true
5731 }
5732 }
5733}
5734
5735fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
5740 for entry in &cat.tables {
5741 let txn_dir = root
5742 .join(TABLES_DIR)
5743 .join(entry.table_id.to_string())
5744 .join("_txn");
5745 if txn_dir.exists() {
5746 let _ = std::fs::remove_dir_all(&txn_dir);
5747 }
5748 }
5749}
5750
5751#[cfg(test)]
5752mod trigger_engine_tests {
5753 use super::*;
5754
5755 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
5756 WriteEvent {
5757 table: "test".into(),
5758 kind: TriggerEvent::Insert,
5759 new: Some(TriggerRowImage {
5760 columns: new_cells.iter().cloned().collect(),
5761 }),
5762 old: Some(TriggerRowImage {
5763 columns: old_cells.iter().cloned().collect(),
5764 }),
5765 changed_columns: Vec::new(),
5766 op_indices: Vec::new(),
5767 put_idx: None,
5768 trigger_stack: Vec::new(),
5769 }
5770 }
5771
5772 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
5773 WriteEvent {
5774 table: "test".into(),
5775 kind: TriggerEvent::Insert,
5776 new: Some(TriggerRowImage {
5777 columns: new_cells.iter().cloned().collect(),
5778 }),
5779 old: None,
5780 changed_columns: Vec::new(),
5781 op_indices: Vec::new(),
5782 put_idx: None,
5783 trigger_stack: Vec::new(),
5784 }
5785 }
5786
5787 #[test]
5788 fn value_order_int64_vs_float64() {
5789 assert_eq!(
5790 value_order(&Value::Int64(5), &Value::Float64(5.0)),
5791 Some(std::cmp::Ordering::Equal)
5792 );
5793 assert_eq!(
5794 value_order(&Value::Int64(5), &Value::Float64(3.0)),
5795 Some(std::cmp::Ordering::Greater)
5796 );
5797 assert_eq!(
5798 value_order(&Value::Int64(2), &Value::Float64(3.0)),
5799 Some(std::cmp::Ordering::Less)
5800 );
5801 }
5802
5803 #[test]
5804 fn value_order_null_returns_none() {
5805 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
5806 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
5807 assert_eq!(value_order(&Value::Null, &Value::Null), None);
5808 }
5809
5810 #[test]
5811 fn value_order_cross_group_returns_none() {
5812 assert_eq!(
5813 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
5814 None
5815 );
5816 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
5817 assert_eq!(
5818 value_order(
5819 &Value::Embedding(vec![1.0, 2.0]),
5820 &Value::Embedding(vec![1.0, 2.0])
5821 ),
5822 None
5823 );
5824 }
5825
5826 #[test]
5827 fn eval_trigger_expr_ranges_and_booleans() {
5828 let expr = TriggerExpr::And {
5829 left: Box::new(TriggerExpr::Gt {
5830 left: TriggerValue::NewColumn(1),
5831 right: TriggerValue::Literal(Value::Int64(0)),
5832 }),
5833 right: Box::new(TriggerExpr::Lte {
5834 left: TriggerValue::NewColumn(1),
5835 right: TriggerValue::Literal(Value::Int64(100)),
5836 }),
5837 };
5838 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
5839 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
5840 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
5841
5842 let or_expr = TriggerExpr::Or {
5843 left: Box::new(TriggerExpr::Lt {
5844 left: TriggerValue::NewColumn(1),
5845 right: TriggerValue::Literal(Value::Int64(0)),
5846 }),
5847 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
5848 TriggerValue::OldColumn(2),
5849 )))),
5850 };
5851 assert!(eval_trigger_expr(
5852 &or_expr,
5853 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
5854 )
5855 .unwrap());
5856 assert!(!eval_trigger_expr(
5857 &or_expr,
5858 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
5859 )
5860 .unwrap());
5861
5862 assert!(eval_trigger_expr(
5863 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
5864 &event_insert(&[])
5865 )
5866 .unwrap());
5867 assert!(!eval_trigger_expr(
5868 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
5869 &event_insert(&[])
5870 )
5871 .unwrap());
5872 assert!(!eval_trigger_expr(
5873 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
5874 &event_insert(&[])
5875 )
5876 .unwrap());
5877 }
5878}