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