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