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 {
4276 let wal = self.shared_wal.lock();
4277 let active = wal.active_segment_no();
4278 drop(wal);
4279 let wal_dir = self.root.join("_wal");
4281 if wal_dir.exists() {
4282 for entry in std::fs::read_dir(&wal_dir)? {
4283 let entry = entry?;
4284 let path = entry.path();
4285 if path.extension().is_some_and(|ext| ext == "wal") {
4286 let _ = std::fs::remove_file(&path);
4287 }
4288 }
4289 }
4290 let _ = active; }
4292
4293 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
4295
4296 let tables = self.tables.read();
4299 let visible = self.epoch.visible();
4300 for handle in tables.values() {
4301 handle.lock().persist_manifest(visible)?;
4302 }
4303
4304 Ok(())
4305 }
4306 fn alloc_txn_id(&self) -> u64 {
4307 let mut g = self.next_txn_id.lock();
4308 let id = *g;
4309 *g = g.wrapping_add(1);
4310 id
4311 }
4312
4313 pub fn set_spill_threshold(&self, bytes: u64) {
4317 self.spill_threshold
4318 .store(bytes, std::sync::atomic::Ordering::Relaxed);
4319 }
4320
4321 #[doc(hidden)]
4325 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
4326 *self.spill_hook.lock() = Some(Box::new(f));
4327 }
4328
4329 #[doc(hidden)]
4333 pub fn __wal_group_sync_count(&self) -> u64 {
4334 self.shared_wal.lock().group_sync_count()
4335 }
4336
4337 #[doc(hidden)]
4340 pub fn __poison(&self) {
4341 self.poisoned
4342 .store(true, std::sync::atomic::Ordering::Relaxed);
4343 }
4344
4345 pub fn check(&self) -> Vec<CheckIssue> {
4358 let mut issues = Vec::new();
4359 let cat = self.catalog.read();
4360 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
4361 for entry in &cat.tables {
4362 if !matches!(entry.state, TableState::Live) {
4363 continue;
4364 }
4365 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
4366 let mut err = |sev: &str, desc: String| {
4367 issues.push(CheckIssue {
4368 table_id: entry.table_id,
4369 table_name: entry.name.clone(),
4370 severity: sev.into(),
4371 description: desc,
4372 });
4373 };
4374 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
4375 Ok(m) => m,
4376 Err(e) => {
4377 err("error", format!("manifest read failed: {e}"));
4378 continue;
4379 }
4380 };
4381 if m.flushed_epoch > m.current_epoch {
4382 err(
4383 "error",
4384 format!(
4385 "flushed_epoch {} exceeds current_epoch {} (impossible)",
4386 m.flushed_epoch, m.current_epoch
4387 ),
4388 );
4389 }
4390
4391 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
4392 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
4393 for rr in &m.runs {
4394 referenced.insert(rr.run_id);
4395 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
4396 if !run_path.exists() {
4397 err("error", format!("missing run file: r-{}.sr", rr.run_id));
4398 continue;
4399 }
4400 match crate::sorted_run::RunReader::open(
4401 &run_path,
4402 entry.schema.clone(),
4403 self.kek.clone(),
4404 ) {
4405 Ok(reader) => {
4406 if reader.row_count() as u64 != rr.row_count {
4407 err(
4408 "error",
4409 format!(
4410 "run r-{} row count mismatch: manifest {} vs run {}",
4411 rr.run_id,
4412 rr.row_count,
4413 reader.row_count()
4414 ),
4415 );
4416 }
4417 }
4418 Err(e) => {
4419 err(
4420 "error",
4421 format!("run r-{} integrity check failed: {e}", rr.run_id),
4422 );
4423 }
4424 }
4425 }
4426
4427 for r in &m.retiring {
4431 referenced.insert(r.run_id);
4432 }
4433
4434 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
4436 for ent in rd.flatten() {
4437 let p = ent.path();
4438 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
4439 continue;
4440 }
4441 let run_id = p
4442 .file_stem()
4443 .and_then(|s| s.to_str())
4444 .and_then(|s| s.strip_prefix("r-"))
4445 .and_then(|s| s.parse::<u128>().ok());
4446 if let Some(id) = run_id {
4447 if !referenced.contains(&id) {
4448 err(
4449 "warning",
4450 format!("orphan run file r-{id}.sr not referenced by the manifest"),
4451 );
4452 }
4453 }
4454 }
4455 }
4456 }
4457
4458 let external_names = cat
4459 .external_tables
4460 .iter()
4461 .map(|entry| entry.name.clone())
4462 .collect::<std::collections::HashSet<_>>();
4463 let vtab_dir = self.root.join(VTAB_DIR);
4464 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
4465 for entry in entries.flatten() {
4466 let name = entry.file_name();
4467 let Some(name) = name.to_str() else { continue };
4468 if !external_names.contains(name) {
4469 issues.push(CheckIssue {
4470 table_id: EXTERNAL_TABLE_ID,
4471 table_name: name.to_string(),
4472 severity: "warning".into(),
4473 description: format!(
4474 "orphan external table state entry {:?} not referenced by the catalog",
4475 entry.path()
4476 ),
4477 });
4478 }
4479 }
4480 }
4481
4482 for (seg, msg) in self.shared_wal.lock().verify_segments() {
4489 issues.push(CheckIssue {
4490 table_id: WAL_TABLE_ID,
4491 table_name: "<wal>".into(),
4492 severity: "error".into(),
4493 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
4494 });
4495 }
4496 issues
4497 }
4498
4499 pub fn doctor(&self) -> Result<Vec<u64>> {
4503 let _ddl = self.ddl_lock.lock();
4506 let issues = self.check();
4507 let bad_tables: std::collections::HashSet<u64> = issues
4512 .iter()
4513 .filter(|i| {
4514 i.severity == "error"
4515 && i.table_id != WAL_TABLE_ID
4516 && i.table_id != EXTERNAL_TABLE_ID
4517 })
4518 .map(|i| i.table_id)
4519 .collect();
4520 if bad_tables.is_empty() {
4521 return Ok(Vec::new());
4522 }
4523
4524 let qdir = self.root.join("_quarantine");
4525 std::fs::create_dir_all(&qdir)?;
4526 let mut quarantined = Vec::new();
4527 for &table_id in &bad_tables {
4528 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
4529 if tdir.exists() {
4530 let dest = qdir.join(table_id.to_string());
4531 std::fs::rename(&tdir, &dest)?;
4532 }
4533 {
4534 let mut cat = self.catalog.write();
4535 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
4536 entry.state = TableState::Dropped {
4537 at_epoch: self.epoch.visible().0,
4538 };
4539 }
4540 }
4541 self.tables.write().remove(&table_id);
4543 quarantined.push(table_id);
4544 }
4545 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
4546 Ok(quarantined)
4547 }
4548
4549 #[allow(dead_code)]
4551 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
4552 self.kek.as_ref()
4553 }
4554
4555 #[allow(dead_code)]
4557 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
4558 &self.epoch
4559 }
4560
4561 #[allow(dead_code)]
4563 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
4564 &self.snapshots
4565 }
4566}
4567
4568fn external_state_dir(root: &Path, name: &str) -> PathBuf {
4569 root.join(VTAB_DIR).join(name)
4570}
4571
4572fn filter_ignored_staging(
4573 staging: Vec<(u64, crate::txn::Staged)>,
4574 ignored_indices: &std::collections::BTreeSet<usize>,
4575) -> Vec<(u64, crate::txn::Staged)> {
4576 if ignored_indices.is_empty() {
4577 return staging;
4578 }
4579 staging
4580 .into_iter()
4581 .enumerate()
4582 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
4583 .collect()
4584}
4585
4586fn external_state_file(root: &Path, name: &str) -> PathBuf {
4587 external_state_dir(root, name).join("state.json")
4588}
4589
4590fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
4591 let path = external_state_file(root, name);
4592 match std::fs::read(path) {
4593 Ok(bytes) => Ok(bytes),
4594 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
4595 Err(e) => Err(e.into()),
4596 }
4597}
4598
4599fn current_external_state_bytes(
4600 root: &Path,
4601 external_states: &[(String, Vec<u8>)],
4602 name: &str,
4603) -> Result<Vec<u8>> {
4604 for (table, state) in external_states.iter().rev() {
4605 if table == name {
4606 return Ok(state.clone());
4607 }
4608 }
4609 read_external_state_file(root, name)
4610}
4611
4612fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
4613 let mut out = external_states;
4614 dedup_external_states_in_place(&mut out);
4615 out
4616}
4617
4618fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
4619 let mut seen = std::collections::HashSet::new();
4620 let mut out = Vec::with_capacity(external_states.len());
4621 for (name, state) in std::mem::take(external_states).into_iter().rev() {
4622 if seen.insert(name.clone()) {
4623 out.push((name, state));
4624 }
4625 }
4626 out.reverse();
4627 *external_states = out;
4628}
4629
4630fn prepare_external_state_file(
4631 root: &Path,
4632 name: &str,
4633 state: &[u8],
4634 txn_id: u64,
4635) -> Result<PathBuf> {
4636 let dir = external_state_dir(root, name);
4637 std::fs::create_dir_all(&dir)?;
4638 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
4639 {
4640 let mut file = std::fs::File::create(&pending)?;
4641 file.write_all(state)?;
4642 file.sync_all()?;
4643 }
4644 Ok(pending)
4645}
4646
4647fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
4648 let path = external_state_file(root, name);
4649 std::fs::rename(pending, &path)?;
4650 if let Ok(dir) = std::fs::File::open(external_state_dir(root, name)) {
4651 let _ = dir.sync_all();
4652 }
4653 Ok(())
4654}
4655
4656fn write_external_state_file(root: &Path, name: &str, state: &[u8]) -> Result<()> {
4657 let pending = prepare_external_state_file(root, name, state, 0)?;
4658 publish_external_state_file(root, name, &pending)
4659}
4660
4661fn recover_shared_wal(
4670 root: &Path,
4671 tables: &HashMap<u64, TableHandle>,
4672 epoch: &EpochAuthority,
4673 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
4674) -> Result<()> {
4675 use crate::memtable::Row;
4676 use crate::rowid::RowId;
4677 use crate::wal::{Op, SharedWal};
4678
4679 let records = SharedWal::replay_with_dek(root, wal_dek)?;
4680
4681 let mut committed: HashMap<u64, u64> = HashMap::new();
4683 let mut spilled_to_link: Vec<(
4684 u64, u64, Vec<crate::wal::AddedRun>,
4687 )> = Vec::new();
4688 for r in &records {
4689 if let Op::TxnCommit {
4690 epoch: ce,
4691 ref added_runs,
4692 } = r.op
4693 {
4694 committed.insert(r.txn_id, ce);
4695 if !added_runs.is_empty() {
4696 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
4697 }
4698 }
4699 }
4700
4701 type TableStage = (Vec<Row>, Vec<(RowId, Epoch)>, Option<Epoch>, Epoch);
4703 let mut stage: HashMap<u64, TableStage> = HashMap::new();
4704 let mut max_epoch = epoch.visible().0;
4705 for r in records {
4706 let Some(&ce) = committed.get(&r.txn_id) else {
4707 continue; };
4709 let commit_epoch = Epoch(ce);
4710 max_epoch = max_epoch.max(ce);
4711 match r.op {
4712 Op::Put { table_id, rows } => {
4713 let skip = tables
4715 .get(&table_id)
4716 .map(|h| h.lock().flushed_epoch() >= ce)
4717 .unwrap_or(true);
4718 if skip {
4719 continue;
4720 }
4721 let rows: Vec<Row> = match bincode::deserialize(&rows) {
4722 Ok(v) => v,
4723 Err(_) => continue,
4724 };
4725 let rows: Vec<Row> = rows
4728 .into_iter()
4729 .map(|mut row| {
4730 row.committed_epoch = commit_epoch;
4731 row
4732 })
4733 .collect();
4734 let entry = stage
4735 .entry(table_id)
4736 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
4737 entry.0.extend(rows);
4738 entry.3 = commit_epoch;
4739 }
4740 Op::Delete { table_id, row_ids } => {
4741 let skip = tables
4742 .get(&table_id)
4743 .map(|h| h.lock().flushed_epoch() >= ce)
4744 .unwrap_or(true);
4745 if skip {
4746 continue;
4747 }
4748 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
4749 let entry = stage
4750 .entry(table_id)
4751 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
4752 entry.1.extend(dels);
4753 entry.3 = commit_epoch;
4754 }
4755 Op::TruncateTable { table_id } => {
4756 let skip = tables
4757 .get(&table_id)
4758 .map(|h| h.lock().flushed_epoch() >= ce)
4759 .unwrap_or(true);
4760 if skip {
4761 continue;
4762 }
4763 stage.insert(
4764 table_id,
4765 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
4766 );
4767 }
4768 Op::ExternalTableState { name, state } => {
4769 write_external_state_file(root, &name, &state)?;
4770 }
4771 Op::Flush { .. } | Op::TxnCommit { .. } | Op::TxnAbort | Op::Ddl(_) => {}
4772 }
4773 }
4774 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
4775 let Some(handle) = tables.get(&table_id) else {
4776 continue;
4777 };
4778 let mut t = handle.lock();
4779 if let Some(epoch) = truncate_epoch {
4780 t.apply_truncate(epoch)?;
4781 }
4782 t.recover_apply(rows, deletes)?;
4783 if truncate_epoch.is_some() {
4784 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
4785 t.live_count = rows.len() as u64;
4786 t.persist_manifest(table_epoch)?;
4787 }
4788 }
4789
4790 for (txn_id, ce, added_runs) in &spilled_to_link {
4794 for ar in added_runs {
4795 let Some(handle) = tables.get(&ar.table_id) else {
4796 continue;
4797 };
4798 let mut t = handle.lock();
4799 let dest = t.run_path(ar.run_id as u64);
4800 if !dest.exists() {
4801 let pending = root
4802 .join(TABLES_DIR)
4803 .join(ar.table_id.to_string())
4804 .join("_txn")
4805 .join(txn_id.to_string())
4806 .join(format!("r-{}.sr", ar.run_id));
4807 if pending.exists() {
4808 if let Some(parent) = pending.parent() {
4809 std::fs::rename(&pending, &dest)?;
4810 let _ = std::fs::remove_dir_all(parent);
4811 }
4812 }
4813 }
4814 if t.run_path(ar.run_id as u64).exists() {
4820 t.recover_spilled_run(crate::manifest::RunRef {
4821 run_id: ar.run_id,
4822 level: ar.level,
4823 epoch_created: *ce,
4824 row_count: ar.row_count,
4825 });
4826 }
4827 }
4828 }
4829
4830 epoch.advance_recovered(Epoch(max_epoch));
4831 Ok(())
4832}
4833
4834fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
4835 match condition {
4836 ProcedureCondition::Pk { .. } => {
4837 if schema.primary_key().is_none() {
4838 return Err(MongrelError::InvalidArgument(
4839 "procedure condition Pk references a table without a primary key".into(),
4840 ));
4841 }
4842 }
4843 ProcedureCondition::BitmapEq { column_id, .. }
4844 | ProcedureCondition::BitmapIn { column_id, .. }
4845 | ProcedureCondition::Range { column_id, .. }
4846 | ProcedureCondition::RangeF64 { column_id, .. }
4847 | ProcedureCondition::IsNull { column_id }
4848 | ProcedureCondition::IsNotNull { column_id }
4849 | ProcedureCondition::FmContains { column_id, .. } => {
4850 validate_column_id(*column_id, schema)?;
4851 }
4852 }
4853 Ok(())
4854}
4855
4856fn bind_procedure_args(
4857 procedure: &StoredProcedure,
4858 mut args: HashMap<String, crate::Value>,
4859) -> Result<HashMap<String, crate::Value>> {
4860 let mut out = HashMap::new();
4861 for param in &procedure.params {
4862 let value = match args.remove(¶m.name) {
4863 Some(value) => value,
4864 None => param.default.clone().ok_or_else(|| {
4865 MongrelError::InvalidArgument(format!(
4866 "missing required procedure parameter {:?}",
4867 param.name
4868 ))
4869 })?,
4870 };
4871 if !param.nullable && matches!(value, crate::Value::Null) {
4872 return Err(MongrelError::InvalidArgument(format!(
4873 "procedure parameter {:?} must not be NULL",
4874 param.name
4875 )));
4876 }
4877 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty.clone()) {
4878 return Err(MongrelError::InvalidArgument(format!(
4879 "procedure parameter {:?} has wrong type",
4880 param.name
4881 )));
4882 }
4883 out.insert(param.name.clone(), value);
4884 }
4885 if let Some(extra) = args.keys().next() {
4886 return Err(MongrelError::InvalidArgument(format!(
4887 "unknown procedure parameter {extra:?}"
4888 )));
4889 }
4890 Ok(out)
4891}
4892
4893fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
4894 matches!(
4895 (value, ty),
4896 (crate::Value::Bool(_), crate::TypeId::Bool)
4897 | (crate::Value::Int64(_), crate::TypeId::Int8)
4898 | (crate::Value::Int64(_), crate::TypeId::Int16)
4899 | (crate::Value::Int64(_), crate::TypeId::Int32)
4900 | (crate::Value::Int64(_), crate::TypeId::Int64)
4901 | (crate::Value::Int64(_), crate::TypeId::UInt8)
4902 | (crate::Value::Int64(_), crate::TypeId::UInt16)
4903 | (crate::Value::Int64(_), crate::TypeId::UInt32)
4904 | (crate::Value::Int64(_), crate::TypeId::UInt64)
4905 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
4906 | (crate::Value::Int64(_), crate::TypeId::Date32)
4907 | (crate::Value::Float64(_), crate::TypeId::Float32)
4908 | (crate::Value::Float64(_), crate::TypeId::Float64)
4909 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
4910 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
4911 )
4912}
4913
4914fn eval_cells(
4915 cells: &[crate::procedure::ProcedureCell],
4916 args: &HashMap<String, crate::Value>,
4917 outputs: &HashMap<String, ProcedureCallOutput>,
4918) -> Result<Vec<(u16, crate::Value)>> {
4919 cells
4920 .iter()
4921 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
4922 .collect()
4923}
4924
4925fn eval_condition(
4926 condition: &ProcedureCondition,
4927 args: &HashMap<String, crate::Value>,
4928 outputs: &HashMap<String, ProcedureCallOutput>,
4929) -> Result<crate::Condition> {
4930 Ok(match condition {
4931 ProcedureCondition::Pk { value } => {
4932 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
4933 }
4934 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
4935 column_id: *column_id,
4936 value: eval_value(value, args, outputs)?.encode_key(),
4937 },
4938 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
4939 column_id: *column_id,
4940 values: values
4941 .iter()
4942 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
4943 .collect::<Result<Vec<_>>>()?,
4944 },
4945 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
4946 column_id: *column_id,
4947 lo: expect_i64(eval_value(lo, args, outputs)?)?,
4948 hi: expect_i64(eval_value(hi, args, outputs)?)?,
4949 },
4950 ProcedureCondition::RangeF64 {
4951 column_id,
4952 lo,
4953 lo_inclusive,
4954 hi,
4955 hi_inclusive,
4956 } => crate::Condition::RangeF64 {
4957 column_id: *column_id,
4958 lo: expect_f64(eval_value(lo, args, outputs)?)?,
4959 lo_inclusive: *lo_inclusive,
4960 hi: expect_f64(eval_value(hi, args, outputs)?)?,
4961 hi_inclusive: *hi_inclusive,
4962 },
4963 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
4964 column_id: *column_id,
4965 },
4966 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
4967 column_id: *column_id,
4968 },
4969 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
4970 column_id: *column_id,
4971 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
4972 },
4973 })
4974}
4975
4976fn eval_value(
4977 value: &ProcedureValue,
4978 args: &HashMap<String, crate::Value>,
4979 outputs: &HashMap<String, ProcedureCallOutput>,
4980) -> Result<crate::Value> {
4981 match value {
4982 ProcedureValue::Literal(value) => Ok(value.clone()),
4983 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
4984 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
4985 }),
4986 ProcedureValue::StepScalar(id) => match outputs.get(id) {
4987 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
4988 _ => Err(MongrelError::InvalidArgument(format!(
4989 "procedure step {id:?} did not return a scalar"
4990 ))),
4991 },
4992 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
4993 Err(MongrelError::InvalidArgument(
4994 "row-valued procedure reference cannot be used as a scalar".into(),
4995 ))
4996 }
4997 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
4998 "structured procedure value cannot be used as a scalar cell".into(),
4999 )),
5000 }
5001}
5002
5003fn eval_return_output(
5004 value: &ProcedureValue,
5005 args: &HashMap<String, crate::Value>,
5006 outputs: &HashMap<String, ProcedureCallOutput>,
5007) -> Result<ProcedureCallOutput> {
5008 match value {
5009 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
5010 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
5011 args.get(name).cloned().ok_or_else(|| {
5012 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
5013 })?,
5014 )),
5015 ProcedureValue::StepRows(id)
5016 | ProcedureValue::StepRow(id)
5017 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
5018 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
5019 }),
5020 ProcedureValue::Object(fields) => {
5021 let mut out = Vec::with_capacity(fields.len());
5022 for (name, value) in fields {
5023 out.push((name.clone(), eval_return_output(value, args, outputs)?));
5024 }
5025 Ok(ProcedureCallOutput::Object(out))
5026 }
5027 ProcedureValue::Array(values) => {
5028 let mut out = Vec::with_capacity(values.len());
5029 for value in values {
5030 out.push(eval_return_output(value, args, outputs)?);
5031 }
5032 Ok(ProcedureCallOutput::Array(out))
5033 }
5034 }
5035}
5036
5037fn expect_i64(value: crate::Value) -> Result<i64> {
5038 match value {
5039 crate::Value::Int64(value) => Ok(value),
5040 _ => Err(MongrelError::InvalidArgument(
5041 "procedure value must be Int64".into(),
5042 )),
5043 }
5044}
5045
5046fn expect_f64(value: crate::Value) -> Result<f64> {
5047 match value {
5048 crate::Value::Float64(value) => Ok(value),
5049 _ => Err(MongrelError::InvalidArgument(
5050 "procedure value must be Float64".into(),
5051 )),
5052 }
5053}
5054
5055fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
5056 match value {
5057 crate::Value::Bytes(value) => Ok(value),
5058 _ => Err(MongrelError::InvalidArgument(
5059 "procedure value must be Bytes".into(),
5060 )),
5061 }
5062}
5063
5064fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
5065 if schema.columns.iter().any(|c| c.id == column_id) {
5066 Ok(())
5067 } else {
5068 Err(MongrelError::InvalidArgument(format!(
5069 "unknown column id {column_id}"
5070 )))
5071 }
5072}
5073
5074fn trigger_matches_event(
5075 trigger: &StoredTrigger,
5076 event: &WriteEvent,
5077 cat: &Catalog,
5078) -> Result<bool> {
5079 if trigger.event != event.kind {
5080 return Ok(false);
5081 }
5082 let TriggerTarget::Table(target) = &trigger.target else {
5083 return Ok(false);
5084 };
5085 if target != &event.table {
5086 return Ok(false);
5087 }
5088 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
5089 let schema = &cat
5090 .live(target)
5091 .ok_or_else(|| {
5092 MongrelError::InvalidArgument(format!(
5093 "trigger {:?} references unknown table {target:?}",
5094 trigger.name
5095 ))
5096 })?
5097 .schema;
5098 let mut watched = Vec::with_capacity(trigger.update_of.len());
5099 for name in &trigger.update_of {
5100 let col = schema.column(name).ok_or_else(|| {
5101 MongrelError::InvalidArgument(format!(
5102 "trigger {:?} references unknown UPDATE OF column {name:?}",
5103 trigger.name
5104 ))
5105 })?;
5106 watched.push(col.id);
5107 }
5108 if !event
5109 .changed_columns
5110 .iter()
5111 .any(|column_id| watched.contains(column_id))
5112 {
5113 return Ok(false);
5114 }
5115 }
5116 Ok(true)
5117}
5118
5119fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
5120 let mut ids = std::collections::BTreeSet::new();
5121 if let Some(old) = old {
5122 ids.extend(old.columns.keys().copied());
5123 }
5124 if let Some(new) = new {
5125 ids.extend(new.columns.keys().copied());
5126 }
5127 ids.into_iter()
5128 .filter(|id| {
5129 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
5130 })
5131 .collect()
5132}
5133
5134fn eval_trigger_cells(
5135 cells: &[crate::trigger::TriggerCell],
5136 event: &WriteEvent,
5137 selected: Option<&TriggerRowImage>,
5138) -> Result<Vec<(u16, Value)>> {
5139 cells
5140 .iter()
5141 .map(|cell| {
5142 Ok((
5143 cell.column_id,
5144 eval_trigger_value(&cell.value, event, selected)?,
5145 ))
5146 })
5147 .collect()
5148}
5149
5150fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
5151 match expr {
5152 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
5153 Value::Bool(value) => Ok(value),
5154 Value::Null => Ok(false),
5155 other => Err(MongrelError::InvalidArgument(format!(
5156 "trigger WHEN value must be boolean, got {other:?}"
5157 ))),
5158 },
5159 TriggerExpr::Eq { left, right } => Ok(values_equal(
5160 &eval_trigger_value(left, event, None)?,
5161 &eval_trigger_value(right, event, None)?,
5162 )),
5163 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
5164 &eval_trigger_value(left, event, None)?,
5165 &eval_trigger_value(right, event, None)?,
5166 )),
5167 TriggerExpr::Lt { left, right } => match value_order(
5168 &eval_trigger_value(left, event, None)?,
5169 &eval_trigger_value(right, event, None)?,
5170 ) {
5171 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
5172 None => Ok(false),
5173 },
5174 TriggerExpr::Lte { 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::Greater),
5179 None => Ok(false),
5180 },
5181 TriggerExpr::Gt { left, right } => match value_order(
5182 &eval_trigger_value(left, event, None)?,
5183 &eval_trigger_value(right, event, None)?,
5184 ) {
5185 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
5186 None => Ok(false),
5187 },
5188 TriggerExpr::Gte { left, right } => match value_order(
5189 &eval_trigger_value(left, event, None)?,
5190 &eval_trigger_value(right, event, None)?,
5191 ) {
5192 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
5193 None => Ok(false),
5194 },
5195 TriggerExpr::IsNull(value) => Ok(matches!(
5196 eval_trigger_value(value, event, None)?,
5197 Value::Null
5198 )),
5199 TriggerExpr::IsNotNull(value) => Ok(!matches!(
5200 eval_trigger_value(value, event, None)?,
5201 Value::Null
5202 )),
5203 TriggerExpr::And { left, right } => {
5204 if !eval_trigger_expr(left, event)? {
5205 Ok(false)
5206 } else {
5207 Ok(eval_trigger_expr(right, event)?)
5208 }
5209 }
5210 TriggerExpr::Or { left, right } => {
5211 if eval_trigger_expr(left, event)? {
5212 Ok(true)
5213 } else {
5214 Ok(eval_trigger_expr(right, event)?)
5215 }
5216 }
5217 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
5218 }
5219}
5220
5221fn eval_trigger_condition(
5222 condition: &TriggerCondition,
5223 event: &WriteEvent,
5224 selected: &TriggerRowImage,
5225 schema: &Schema,
5226) -> Result<bool> {
5227 match condition {
5228 TriggerCondition::Pk { value } => {
5229 let pk = schema.primary_key().ok_or_else(|| {
5230 MongrelError::InvalidArgument(
5231 "trigger condition Pk references a table without a primary key".into(),
5232 )
5233 })?;
5234 let lhs = eval_trigger_value(value, event, Some(selected))?;
5235 Ok(values_equal(
5236 &lhs,
5237 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
5238 ))
5239 }
5240 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
5241 selected.columns.get(column_id).unwrap_or(&Value::Null),
5242 &eval_trigger_value(value, event, Some(selected))?,
5243 )),
5244 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
5245 selected.columns.get(column_id).unwrap_or(&Value::Null),
5246 &eval_trigger_value(value, event, Some(selected))?,
5247 )),
5248 TriggerCondition::Lt { column_id, value } => match value_order(
5249 selected.columns.get(column_id).unwrap_or(&Value::Null),
5250 &eval_trigger_value(value, event, Some(selected))?,
5251 ) {
5252 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
5253 None => Ok(false),
5254 },
5255 TriggerCondition::Lte { 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::Greater),
5260 None => Ok(false),
5261 },
5262 TriggerCondition::Gt { column_id, value } => match value_order(
5263 selected.columns.get(column_id).unwrap_or(&Value::Null),
5264 &eval_trigger_value(value, event, Some(selected))?,
5265 ) {
5266 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
5267 None => Ok(false),
5268 },
5269 TriggerCondition::Gte { column_id, value } => match value_order(
5270 selected.columns.get(column_id).unwrap_or(&Value::Null),
5271 &eval_trigger_value(value, event, Some(selected))?,
5272 ) {
5273 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
5274 None => Ok(false),
5275 },
5276 TriggerCondition::IsNull { column_id } => Ok(matches!(
5277 selected.columns.get(column_id),
5278 None | Some(Value::Null)
5279 )),
5280 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
5281 selected.columns.get(column_id),
5282 None | Some(Value::Null)
5283 )),
5284 TriggerCondition::And { left, right } => {
5285 if !eval_trigger_condition(left, event, selected, schema)? {
5286 Ok(false)
5287 } else {
5288 Ok(eval_trigger_condition(right, event, selected, schema)?)
5289 }
5290 }
5291 TriggerCondition::Or { left, right } => {
5292 if eval_trigger_condition(left, event, selected, schema)? {
5293 Ok(true)
5294 } else {
5295 Ok(eval_trigger_condition(right, event, selected, schema)?)
5296 }
5297 }
5298 TriggerCondition::Not(condition) => {
5299 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
5300 }
5301 }
5302}
5303
5304fn eval_trigger_value(
5305 value: &TriggerValue,
5306 event: &WriteEvent,
5307 selected: Option<&TriggerRowImage>,
5308) -> Result<Value> {
5309 match value {
5310 TriggerValue::Literal(value) => Ok(value.clone()),
5311 TriggerValue::NewColumn(column_id) => event
5312 .new
5313 .as_ref()
5314 .and_then(|row| row.columns.get(column_id))
5315 .cloned()
5316 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
5317 TriggerValue::OldColumn(column_id) => event
5318 .old
5319 .as_ref()
5320 .and_then(|row| row.columns.get(column_id))
5321 .cloned()
5322 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
5323 TriggerValue::SelectedColumn(column_id) => selected
5324 .and_then(|row| row.columns.get(column_id))
5325 .cloned()
5326 .ok_or_else(|| {
5327 MongrelError::InvalidArgument("SELECTED column is not available".into())
5328 }),
5329 }
5330}
5331
5332fn values_equal(left: &Value, right: &Value) -> bool {
5333 match (left, right) {
5334 (Value::Null, Value::Null) => true,
5335 (Value::Bool(a), Value::Bool(b)) => a == b,
5336 (Value::Int64(a), Value::Int64(b)) => a == b,
5337 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
5338 (Value::Bytes(a), Value::Bytes(b)) => a == b,
5339 (Value::Embedding(a), Value::Embedding(b)) => {
5340 a.len() == b.len()
5341 && a.iter()
5342 .zip(b.iter())
5343 .all(|(a, b)| a.to_bits() == b.to_bits())
5344 }
5345 _ => false,
5346 }
5347}
5348
5349fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
5350 match (left, right) {
5351 (Value::Null, _) | (_, Value::Null) => None,
5352 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
5353 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
5354 (Value::Int64(a), Value::Float64(b)) => {
5357 let af = *a as f64;
5358 Some(af.total_cmp(b))
5359 }
5360 (Value::Float64(a), Value::Int64(b)) => {
5363 let bf = *b as f64;
5364 Some(a.total_cmp(&bf))
5365 }
5366 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
5367 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
5368 (Value::Embedding(_), Value::Embedding(_)) => None,
5369 _ => None,
5370 }
5371}
5372
5373fn trigger_message(value: Value) -> String {
5374 match value {
5375 Value::Null => "NULL".into(),
5376 Value::Bool(value) => value.to_string(),
5377 Value::Int64(value) => value.to_string(),
5378 Value::Float64(value) => value.to_string(),
5379 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
5380 Value::Embedding(value) => format!("{value:?}"),
5381 Value::Decimal(value) => value.to_string(),
5382 Value::Interval {
5383 months,
5384 days,
5385 nanos,
5386 } => format!("{months}m {days}d {nanos}ns"),
5387 Value::Uuid(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
5388 Value::Json(b) => String::from_utf8_lossy(&b).into_owned(),
5389 }
5390}
5391
5392fn validate_trigger_step<'a>(
5393 step: &TriggerStep,
5394 cat: &'a Catalog,
5395 target_schema: &Schema,
5396 event: TriggerEvent,
5397 select_schemas: &mut HashMap<String, &'a Schema>,
5398) -> Result<()> {
5399 match step {
5400 TriggerStep::SetNew { cells } => {
5401 if event == TriggerEvent::Delete {
5402 return Err(MongrelError::InvalidArgument(
5403 "SetNew trigger step is not valid for DELETE triggers".into(),
5404 ));
5405 }
5406 for cell in cells {
5407 validate_column_id(cell.column_id, target_schema)?;
5408 validate_trigger_value(&cell.value, target_schema, event)?;
5409 }
5410 }
5411 TriggerStep::Insert { table, cells } => {
5412 let schema = trigger_write_schema(cat, table, "insert")?;
5413 for cell in cells {
5414 validate_column_id(cell.column_id, schema)?;
5415 validate_trigger_value(&cell.value, target_schema, event)?;
5416 }
5417 }
5418 TriggerStep::UpdateByPk { table, pk, cells } => {
5419 let schema = trigger_write_schema(cat, table, "update")?;
5420 if schema.primary_key().is_none() {
5421 return Err(MongrelError::InvalidArgument(format!(
5422 "trigger update_by_pk references table {table:?} without a primary key"
5423 )));
5424 }
5425 validate_trigger_value(pk, target_schema, event)?;
5426 for cell in cells {
5427 validate_column_id(cell.column_id, schema)?;
5428 validate_trigger_value(&cell.value, target_schema, event)?;
5429 }
5430 }
5431 TriggerStep::DeleteByPk { table, pk } => {
5432 let schema = trigger_write_schema(cat, table, "delete")?;
5433 if schema.primary_key().is_none() {
5434 return Err(MongrelError::InvalidArgument(format!(
5435 "trigger delete_by_pk references table {table:?} without a primary key"
5436 )));
5437 }
5438 validate_trigger_value(pk, target_schema, event)?;
5439 }
5440 TriggerStep::Select {
5441 id,
5442 table,
5443 conditions,
5444 } => {
5445 let schema = trigger_read_schema(cat, table)?;
5446 for condition in conditions {
5447 validate_trigger_condition(condition, schema, target_schema, event)?;
5448 }
5449 if select_schemas.contains_key(id) {
5450 return Err(MongrelError::InvalidArgument(format!(
5451 "duplicate select id {id:?} in trigger program"
5452 )));
5453 }
5454 select_schemas.insert(id.clone(), schema);
5455 }
5456 TriggerStep::Foreach { id, steps } => {
5457 if !select_schemas.contains_key(id) {
5458 return Err(MongrelError::InvalidArgument(format!(
5459 "foreach references unknown select id {id:?}"
5460 )));
5461 }
5462 let mut inner_select_schemas = select_schemas.clone();
5463 for step in steps {
5464 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
5465 }
5466 }
5467 TriggerStep::DeleteWhere { table, conditions } => {
5468 let schema = trigger_write_schema(cat, table, "delete")?;
5469 for condition in conditions {
5470 validate_trigger_condition(condition, schema, target_schema, event)?;
5471 }
5472 }
5473 TriggerStep::UpdateWhere {
5474 table,
5475 conditions,
5476 cells,
5477 } => {
5478 let schema = trigger_write_schema(cat, table, "update")?;
5479 for condition in conditions {
5480 validate_trigger_condition(condition, schema, target_schema, event)?;
5481 }
5482 for cell in cells {
5483 validate_column_id(cell.column_id, schema)?;
5484 validate_trigger_value(&cell.value, target_schema, event)?;
5485 }
5486 }
5487 TriggerStep::Raise { message, .. } => {
5488 validate_trigger_value(message, target_schema, event)?
5489 }
5490 }
5491 Ok(())
5492}
5493
5494fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
5495 if let Some(entry) = cat.live(table) {
5496 return Ok(&entry.schema);
5497 }
5498 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
5499 let allowed = match op {
5500 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
5501 "update" | "delete" => entry.capabilities.writable,
5502 _ => false,
5503 };
5504 if !allowed {
5505 return Err(MongrelError::InvalidArgument(format!(
5506 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
5507 entry.module
5508 )));
5509 }
5510 if !entry.capabilities.transaction_safe {
5511 return Err(MongrelError::InvalidArgument(format!(
5512 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
5513 entry.module
5514 )));
5515 }
5516 return Ok(&entry.declared_schema);
5517 }
5518 Err(MongrelError::InvalidArgument(format!(
5519 "trigger references unknown table {table:?}"
5520 )))
5521}
5522
5523fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
5524 if let Some(entry) = cat.live(table) {
5525 return Ok(&entry.schema);
5526 }
5527 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
5528 if entry.capabilities.trigger_safe {
5529 return Ok(&entry.declared_schema);
5530 }
5531 return Err(MongrelError::InvalidArgument(format!(
5532 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
5533 entry.module
5534 )));
5535 }
5536 Err(MongrelError::InvalidArgument(format!(
5537 "trigger references unknown table {table:?}"
5538 )))
5539}
5540
5541fn validate_trigger_condition(
5542 condition: &TriggerCondition,
5543 schema: &Schema,
5544 target_schema: &Schema,
5545 event: TriggerEvent,
5546) -> Result<()> {
5547 match condition {
5548 TriggerCondition::Pk { value } => {
5549 if schema.primary_key().is_none() {
5550 return Err(MongrelError::InvalidArgument(
5551 "trigger condition Pk references a table without a primary key".into(),
5552 ));
5553 }
5554 validate_trigger_value(value, target_schema, event)
5555 }
5556 TriggerCondition::Eq { column_id, value }
5557 | TriggerCondition::NotEq { column_id, value }
5558 | TriggerCondition::Lt { column_id, value }
5559 | TriggerCondition::Lte { column_id, value }
5560 | TriggerCondition::Gt { column_id, value }
5561 | TriggerCondition::Gte { column_id, value } => {
5562 validate_column_id(*column_id, schema)?;
5563 validate_trigger_value(value, target_schema, event)
5564 }
5565 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
5566 validate_column_id(*column_id, schema)
5567 }
5568 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
5569 validate_trigger_condition(left, schema, target_schema, event)?;
5570 validate_trigger_condition(right, schema, target_schema, event)
5571 }
5572 TriggerCondition::Not(condition) => {
5573 validate_trigger_condition(condition, schema, target_schema, event)
5574 }
5575 }
5576}
5577
5578fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
5579 match expr {
5580 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
5581 validate_trigger_value(value, schema, event)
5582 }
5583 TriggerExpr::Eq { left, right }
5584 | TriggerExpr::NotEq { left, right }
5585 | TriggerExpr::Lt { left, right }
5586 | TriggerExpr::Lte { left, right }
5587 | TriggerExpr::Gt { left, right }
5588 | TriggerExpr::Gte { left, right } => {
5589 validate_trigger_value(left, schema, event)?;
5590 validate_trigger_value(right, schema, event)
5591 }
5592 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
5593 validate_trigger_expr(left, schema, event)?;
5594 validate_trigger_expr(right, schema, event)
5595 }
5596 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
5597 }
5598}
5599
5600fn validate_trigger_value(
5601 value: &TriggerValue,
5602 schema: &Schema,
5603 event: TriggerEvent,
5604) -> Result<()> {
5605 match value {
5606 TriggerValue::Literal(_) => Ok(()),
5607 TriggerValue::NewColumn(id) => {
5608 if event == TriggerEvent::Delete {
5609 return Err(MongrelError::InvalidArgument(
5610 "DELETE triggers cannot reference NEW".into(),
5611 ));
5612 }
5613 validate_column_id(*id, schema)
5614 }
5615 TriggerValue::OldColumn(id) => {
5616 if event == TriggerEvent::Insert {
5617 return Err(MongrelError::InvalidArgument(
5618 "INSERT triggers cannot reference OLD".into(),
5619 ));
5620 }
5621 validate_column_id(*id, schema)
5622 }
5623 TriggerValue::SelectedColumn(_) => Ok(()),
5627 }
5628}
5629
5630fn recover_ddl_from_wal(
5636 root: &Path,
5637 cat: &mut Catalog,
5638 meta_dek: Option<&[u8; META_DEK_LEN]>,
5639 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
5640) -> Result<()> {
5641 use crate::wal::{DdlOp, Op, SharedWal};
5642
5643 let records = match SharedWal::replay_with_dek(root, wal_dek) {
5644 Ok(r) => r,
5645 Err(_) => return Ok(()),
5646 };
5647
5648 let mut committed: HashMap<u64, u64> = HashMap::new();
5649 for r in &records {
5650 if let Op::TxnCommit { epoch: ce, .. } = r.op {
5651 committed.insert(r.txn_id, ce);
5652 }
5653 }
5654
5655 let mut changed = false;
5656 for r in records {
5657 let Some(&ce) = committed.get(&r.txn_id) else {
5658 continue;
5659 };
5660 match r.op {
5661 Op::Ddl(DdlOp::CreateTable {
5662 table_id,
5663 ref name,
5664 ref schema_json,
5665 }) => {
5666 if cat.tables.iter().any(|t| t.table_id == table_id) {
5667 continue;
5668 }
5669 let schema = DdlOp::decode_schema(schema_json)?;
5670 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
5671 if !tdir.exists() {
5672 std::fs::create_dir_all(tdir.join(crate::engine::WAL_DIR))?;
5673 std::fs::create_dir_all(tdir.join(crate::engine::RUNS_DIR))?;
5674 crate::engine::write_schema(&tdir, &schema)?;
5675 let mut m = crate::manifest::Manifest::new(table_id, schema.schema_id);
5681 crate::manifest::write_atomic(&tdir, &mut m, meta_dek)?;
5682 }
5683 cat.tables.push(CatalogEntry {
5684 table_id,
5685 name: name.clone(),
5686 schema,
5687 state: TableState::Live,
5688 created_epoch: ce,
5689 });
5690 cat.next_table_id = cat.next_table_id.max(table_id + 1);
5691 changed = true;
5692 }
5693 Op::Ddl(DdlOp::DropTable { table_id }) => {
5694 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
5695 if matches!(entry.state, TableState::Live) {
5696 entry.state = TableState::Dropped { at_epoch: ce };
5697 changed = true;
5698 }
5699 }
5700 }
5701 Op::Ddl(DdlOp::RenameTable {
5702 table_id,
5703 ref new_name,
5704 }) => {
5705 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
5706 if entry.name != *new_name {
5707 entry.name = new_name.clone();
5708 changed = true;
5709 }
5710 }
5711 }
5715 Op::Ddl(DdlOp::AlterTable {
5716 table_id,
5717 ref column_json,
5718 }) => {
5719 let column = DdlOp::decode_column(column_json)?;
5720 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
5721 if apply_recovered_column_def(&mut entry.schema, column) {
5722 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
5723 if tdir.exists() {
5724 crate::engine::write_schema(&tdir, &entry.schema)?;
5725 }
5726 changed = true;
5727 }
5728 }
5729 }
5730 _ => {}
5731 }
5732 }
5733
5734 if changed {
5735 catalog::write_atomic(root, cat, meta_dek)?;
5736 }
5737 Ok(())
5738}
5739
5740fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> bool {
5741 match schema.columns.iter_mut().find(|c| c.id == column.id) {
5742 Some(existing) if *existing == column => false,
5743 Some(existing) => {
5744 *existing = column;
5745 schema.schema_id = schema.schema_id.saturating_add(1);
5746 true
5747 }
5748 None => {
5749 schema.columns.push(column);
5750 schema.schema_id = schema.schema_id.saturating_add(1);
5751 true
5752 }
5753 }
5754}
5755
5756fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
5761 for entry in &cat.tables {
5762 let txn_dir = root
5763 .join(TABLES_DIR)
5764 .join(entry.table_id.to_string())
5765 .join("_txn");
5766 if txn_dir.exists() {
5767 let _ = std::fs::remove_dir_all(&txn_dir);
5768 }
5769 }
5770}
5771
5772#[cfg(test)]
5773mod trigger_engine_tests {
5774 use super::*;
5775
5776 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
5777 WriteEvent {
5778 table: "test".into(),
5779 kind: TriggerEvent::Insert,
5780 new: Some(TriggerRowImage {
5781 columns: new_cells.iter().cloned().collect(),
5782 }),
5783 old: Some(TriggerRowImage {
5784 columns: old_cells.iter().cloned().collect(),
5785 }),
5786 changed_columns: Vec::new(),
5787 op_indices: Vec::new(),
5788 put_idx: None,
5789 trigger_stack: Vec::new(),
5790 }
5791 }
5792
5793 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
5794 WriteEvent {
5795 table: "test".into(),
5796 kind: TriggerEvent::Insert,
5797 new: Some(TriggerRowImage {
5798 columns: new_cells.iter().cloned().collect(),
5799 }),
5800 old: None,
5801 changed_columns: Vec::new(),
5802 op_indices: Vec::new(),
5803 put_idx: None,
5804 trigger_stack: Vec::new(),
5805 }
5806 }
5807
5808 #[test]
5809 fn value_order_int64_vs_float64() {
5810 assert_eq!(
5811 value_order(&Value::Int64(5), &Value::Float64(5.0)),
5812 Some(std::cmp::Ordering::Equal)
5813 );
5814 assert_eq!(
5815 value_order(&Value::Int64(5), &Value::Float64(3.0)),
5816 Some(std::cmp::Ordering::Greater)
5817 );
5818 assert_eq!(
5819 value_order(&Value::Int64(2), &Value::Float64(3.0)),
5820 Some(std::cmp::Ordering::Less)
5821 );
5822 }
5823
5824 #[test]
5825 fn value_order_null_returns_none() {
5826 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
5827 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
5828 assert_eq!(value_order(&Value::Null, &Value::Null), None);
5829 }
5830
5831 #[test]
5832 fn value_order_cross_group_returns_none() {
5833 assert_eq!(
5834 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
5835 None
5836 );
5837 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
5838 assert_eq!(
5839 value_order(
5840 &Value::Embedding(vec![1.0, 2.0]),
5841 &Value::Embedding(vec![1.0, 2.0])
5842 ),
5843 None
5844 );
5845 }
5846
5847 #[test]
5848 fn eval_trigger_expr_ranges_and_booleans() {
5849 let expr = TriggerExpr::And {
5850 left: Box::new(TriggerExpr::Gt {
5851 left: TriggerValue::NewColumn(1),
5852 right: TriggerValue::Literal(Value::Int64(0)),
5853 }),
5854 right: Box::new(TriggerExpr::Lte {
5855 left: TriggerValue::NewColumn(1),
5856 right: TriggerValue::Literal(Value::Int64(100)),
5857 }),
5858 };
5859 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
5860 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
5861 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
5862
5863 let or_expr = TriggerExpr::Or {
5864 left: Box::new(TriggerExpr::Lt {
5865 left: TriggerValue::NewColumn(1),
5866 right: TriggerValue::Literal(Value::Int64(0)),
5867 }),
5868 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
5869 TriggerValue::OldColumn(2),
5870 )))),
5871 };
5872 assert!(eval_trigger_expr(
5873 &or_expr,
5874 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
5875 )
5876 .unwrap());
5877 assert!(!eval_trigger_expr(
5878 &or_expr,
5879 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
5880 )
5881 .unwrap());
5882
5883 assert!(eval_trigger_expr(
5884 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
5885 &event_insert(&[])
5886 )
5887 .unwrap());
5888 assert!(!eval_trigger_expr(
5889 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
5890 &event_insert(&[])
5891 )
5892 .unwrap());
5893 assert!(!eval_trigger_expr(
5894 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
5895 &event_insert(&[])
5896 )
5897 .unwrap());
5898 }
5899}