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 {
1322 username: admin_username.to_string(),
1323 is_admin: true,
1324 roles: Vec::new(),
1325 permissions: Vec::new(),
1326 });
1327 self.auth_state.set_require_auth(true);
1328 Ok(())
1329 }
1330
1331 pub fn disable_auth(&self) -> Result<()> {
1348 let epoch = self.epoch.bump_assigned();
1349 {
1350 let mut cat = self.catalog.write();
1351 if !cat.require_auth {
1352 return Err(MongrelError::InvalidArgument(
1353 "database does not have require_auth enabled".into(),
1354 ));
1355 }
1356 cat.require_auth = false;
1357 cat.db_epoch = epoch.0;
1358 }
1359 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1360 *self.principal.write() = None;
1362 self.auth_state.set_require_auth(false);
1364 Ok(())
1365 }
1366
1367 pub fn require(&self, perm: &crate::auth::Permission) -> Result<()> {
1374 if !self.catalog.read().require_auth {
1375 return Ok(());
1376 }
1377 let guard = self.principal.read();
1378 let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
1379 if p.has_permission(perm) {
1380 Ok(())
1381 } else {
1382 Err(MongrelError::PermissionDenied {
1383 required: perm.clone(),
1384 principal: p.username.clone(),
1385 })
1386 }
1387 }
1388
1389 pub fn require_table(
1394 &self,
1395 table: &str,
1396 perm: crate::auth_state::RequiredPermission,
1397 ) -> Result<()> {
1398 self.require(&perm.into_permission(table))
1399 }
1400
1401 pub fn triggers(&self) -> Vec<StoredTrigger> {
1402 self.catalog
1403 .read()
1404 .triggers
1405 .iter()
1406 .map(|t| t.trigger.clone())
1407 .collect()
1408 }
1409
1410 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
1411 self.catalog
1412 .read()
1413 .triggers
1414 .iter()
1415 .find(|t| t.trigger.name == name)
1416 .map(|t| t.trigger.clone())
1417 }
1418
1419 pub fn create_trigger(&self, mut trigger: StoredTrigger) -> Result<StoredTrigger> {
1420 self.require(&crate::auth::Permission::Ddl)?;
1421 let _g = self.ddl_lock.lock();
1422 trigger.validate()?;
1423 self.validate_trigger_references(&trigger)?;
1424 {
1425 let cat = self.catalog.read();
1426 if cat.triggers.iter().any(|t| t.trigger.name == trigger.name) {
1427 return Err(MongrelError::InvalidArgument(format!(
1428 "trigger {:?} already exists",
1429 trigger.name
1430 )));
1431 }
1432 }
1433 let commit_lock = Arc::clone(&self.commit_lock);
1434 let _c = commit_lock.lock();
1435 let epoch = self.epoch.bump_assigned();
1436 trigger.created_epoch = epoch.0;
1437 trigger.updated_epoch = epoch.0;
1438 {
1439 let mut cat = self.catalog.write();
1440 cat.triggers.push(TriggerEntry::from(trigger.clone()));
1441 cat.db_epoch = epoch.0;
1442 }
1443 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1444 self.advance_visible(epoch);
1445 Ok(trigger)
1446 }
1447
1448 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
1449 let _g = self.ddl_lock.lock();
1450 trigger.validate()?;
1451 self.validate_trigger_references(&trigger)?;
1452 let commit_lock = Arc::clone(&self.commit_lock);
1453 let _c = commit_lock.lock();
1454 let epoch = self.epoch.bump_assigned();
1455 let replaced = {
1456 let mut cat = self.catalog.write();
1457 let next = match cat
1458 .triggers
1459 .iter()
1460 .position(|t| t.trigger.name == trigger.name)
1461 {
1462 Some(idx) => {
1463 let next = cat.triggers[idx]
1464 .trigger
1465 .replaced(trigger.clone(), epoch.0)?;
1466 cat.triggers[idx] = TriggerEntry::from(next.clone());
1467 next
1468 }
1469 None => {
1470 let mut next = trigger;
1471 next.created_epoch = epoch.0;
1472 next.updated_epoch = epoch.0;
1473 cat.triggers.push(TriggerEntry::from(next.clone()));
1474 next
1475 }
1476 };
1477 cat.db_epoch = epoch.0;
1478 next
1479 };
1480 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1481 self.advance_visible(epoch);
1482 Ok(replaced)
1483 }
1484
1485 pub fn drop_trigger(&self, name: &str) -> Result<()> {
1486 self.require(&crate::auth::Permission::Ddl)?;
1487 let _g = self.ddl_lock.lock();
1488 let commit_lock = Arc::clone(&self.commit_lock);
1489 let _c = commit_lock.lock();
1490 let epoch = self.epoch.bump_assigned();
1491 {
1492 let mut cat = self.catalog.write();
1493 let before = cat.triggers.len();
1494 cat.triggers.retain(|t| t.trigger.name != name);
1495 if cat.triggers.len() == before {
1496 return Err(MongrelError::NotFound(format!(
1497 "trigger {name:?} not found"
1498 )));
1499 }
1500 cat.db_epoch = epoch.0;
1501 }
1502 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1503 self.advance_visible(epoch);
1504 Ok(())
1505 }
1506
1507 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
1508 self.catalog.read().external_tables.clone()
1509 }
1510
1511 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
1512 self.catalog
1513 .read()
1514 .external_tables
1515 .iter()
1516 .find(|t| t.name == name)
1517 .cloned()
1518 }
1519
1520 pub fn create_external_table(
1521 &self,
1522 mut entry: ExternalTableEntry,
1523 ) -> Result<ExternalTableEntry> {
1524 self.require(&crate::auth::Permission::Ddl)?;
1525 let _g = self.ddl_lock.lock();
1526 entry.validate()?;
1527 {
1528 let cat = self.catalog.read();
1529 if cat.live(&entry.name).is_some()
1530 || cat.external_tables.iter().any(|t| t.name == entry.name)
1531 {
1532 return Err(MongrelError::InvalidArgument(format!(
1533 "table {:?} already exists",
1534 entry.name
1535 )));
1536 }
1537 }
1538 let commit_lock = Arc::clone(&self.commit_lock);
1539 let _c = commit_lock.lock();
1540 let epoch = self.epoch.bump_assigned();
1541 entry.created_epoch = epoch.0;
1542 {
1543 let mut cat = self.catalog.write();
1544 cat.external_tables.push(entry.clone());
1545 cat.db_epoch = epoch.0;
1546 }
1547 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1548 self.advance_visible(epoch);
1549 Ok(entry)
1550 }
1551
1552 pub fn drop_external_table(&self, name: &str) -> Result<()> {
1553 self.require(&crate::auth::Permission::Ddl)?;
1554 let _g = self.ddl_lock.lock();
1555 let commit_lock = Arc::clone(&self.commit_lock);
1556 let _c = commit_lock.lock();
1557 let epoch = self.epoch.bump_assigned();
1558 {
1559 let mut cat = self.catalog.write();
1560 let before = cat.external_tables.len();
1561 cat.external_tables.retain(|t| t.name != name);
1562 if cat.external_tables.len() == before {
1563 return Err(MongrelError::NotFound(format!(
1564 "external table {name:?} not found"
1565 )));
1566 }
1567 cat.db_epoch = epoch.0;
1568 }
1569 let state_dir = self.root.join(VTAB_DIR).join(name);
1570 if state_dir.exists() {
1571 std::fs::remove_dir_all(state_dir)?;
1572 }
1573 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1574 self.advance_visible(epoch);
1575 Ok(())
1576 }
1577
1578 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
1579 let txn_id = self.alloc_txn_id();
1580 self.commit_transaction_with_external_states(
1581 txn_id,
1582 self.epoch.visible(),
1583 Vec::new(),
1584 vec![(name.to_string(), state.to_vec())],
1585 None,
1586 )
1587 }
1588
1589 pub fn trigger_config(&self) -> TriggerConfig {
1590 use std::sync::atomic::Ordering;
1591 TriggerConfig {
1592 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
1593 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
1594 max_loop_iterations: self.trigger_max_loop_iterations.load(Ordering::Relaxed),
1595 }
1596 }
1597
1598 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
1599 use std::sync::atomic::Ordering;
1600 if config.max_depth == 0 {
1601 return Err(MongrelError::InvalidArgument(
1602 "trigger max_depth must be greater than 0".into(),
1603 ));
1604 }
1605 self.trigger_recursive
1606 .store(config.recursive_triggers, Ordering::Relaxed);
1607 self.trigger_max_depth
1608 .store(config.max_depth, Ordering::Relaxed);
1609 self.trigger_max_loop_iterations
1610 .store(config.max_loop_iterations, Ordering::Relaxed);
1611 Ok(())
1612 }
1613
1614 pub fn set_recursive_triggers(&self, recursive: bool) {
1615 use std::sync::atomic::Ordering;
1616 self.trigger_recursive.store(recursive, Ordering::Relaxed);
1617 }
1618
1619 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent> {
1622 self.notify.subscribe()
1623 }
1624
1625 pub fn notify(&self, channel: &str, message: Option<String>) {
1628 let _ = self.notify.send(ChangeEvent {
1629 channel: channel.to_string(),
1630 table: String::new(),
1631 op: "notify".into(),
1632 epoch: self.epoch.visible().0,
1633 message,
1634 });
1635 }
1636
1637 pub fn call_procedure(
1638 &self,
1639 name: &str,
1640 args: HashMap<String, crate::Value>,
1641 ) -> Result<ProcedureCallResult> {
1642 self.require(&crate::auth::Permission::All)?;
1646 let procedure = self
1647 .procedure(name)
1648 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
1649 let args = bind_procedure_args(&procedure, args)?;
1650 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
1651 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
1652 if has_writes {
1653 let mut tx = self.begin();
1654 let run = (|| {
1655 for step in &procedure.body.steps {
1656 let output =
1657 self.execute_procedure_step(step, &args, &outputs, Some(&mut tx))?;
1658 outputs.insert(step.id().to_string(), output);
1659 }
1660 eval_return_output(&procedure.body.return_value, &args, &outputs)
1661 })();
1662 match run {
1663 Ok(output) => {
1664 let epoch = tx.commit()?.0;
1665 Ok(ProcedureCallResult {
1666 epoch: Some(epoch),
1667 output,
1668 })
1669 }
1670 Err(e) => {
1671 tx.rollback();
1672 Err(e)
1673 }
1674 }
1675 } else {
1676 for step in &procedure.body.steps {
1677 let output = self.execute_procedure_step(step, &args, &outputs, None)?;
1678 outputs.insert(step.id().to_string(), output);
1679 }
1680 Ok(ProcedureCallResult {
1681 epoch: None,
1682 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
1683 })
1684 }
1685 }
1686
1687 fn execute_procedure_step(
1688 &self,
1689 step: &ProcedureStep,
1690 args: &HashMap<String, crate::Value>,
1691 outputs: &HashMap<String, ProcedureCallOutput>,
1692 tx: Option<&mut crate::txn::Transaction<'_>>,
1693 ) -> Result<ProcedureCallOutput> {
1694 match step {
1695 ProcedureStep::NativeQuery {
1696 table,
1697 conditions,
1698 projection,
1699 limit,
1700 ..
1701 } => {
1702 let mut q = crate::Query::new();
1703 for condition in conditions {
1704 q = q.and(eval_condition(condition, args, outputs)?);
1705 }
1706 let handle = self.table(table)?;
1707 let mut rows = handle.lock().query(&q)?;
1708 if let Some(limit) = limit {
1709 rows.truncate(*limit);
1710 }
1711 let projection = projection.as_ref();
1712 Ok(ProcedureCallOutput::Rows(
1713 rows.into_iter()
1714 .map(|row| ProcedureCallRow {
1715 row_id: Some(row.row_id),
1716 columns: match projection {
1717 Some(ids) => row
1718 .columns
1719 .into_iter()
1720 .filter(|(id, _)| ids.contains(id))
1721 .collect(),
1722 None => row.columns,
1723 },
1724 })
1725 .collect(),
1726 ))
1727 }
1728 ProcedureStep::Put {
1729 table,
1730 cells,
1731 returning,
1732 ..
1733 } => {
1734 let tx = tx.ok_or_else(|| {
1735 MongrelError::InvalidArgument(
1736 "write procedure step requires a transaction".into(),
1737 )
1738 })?;
1739 let cells = eval_cells(cells, args, outputs)?;
1740 if *returning {
1741 let out = tx.put_returning(table, cells)?;
1742 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
1743 row_id: None,
1744 columns: out.row.columns.into_iter().collect(),
1745 }))
1746 } else {
1747 tx.put(table, cells)?;
1748 Ok(ProcedureCallOutput::Null)
1749 }
1750 }
1751 ProcedureStep::Upsert {
1752 table,
1753 cells,
1754 update_cells,
1755 returning,
1756 ..
1757 } => {
1758 let tx = tx.ok_or_else(|| {
1759 MongrelError::InvalidArgument(
1760 "write procedure step requires a transaction".into(),
1761 )
1762 })?;
1763 let cells = eval_cells(cells, args, outputs)?;
1764 let action = match update_cells {
1765 Some(update_cells) => {
1766 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
1767 }
1768 None => crate::UpsertAction::DoNothing,
1769 };
1770 let out = tx.upsert(table, cells, action)?;
1771 if *returning {
1772 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
1773 row_id: None,
1774 columns: out.row.columns.into_iter().collect(),
1775 }))
1776 } else {
1777 Ok(ProcedureCallOutput::Null)
1778 }
1779 }
1780 ProcedureStep::DeleteByPk { table, pk, .. } => {
1781 let tx = tx.ok_or_else(|| {
1782 MongrelError::InvalidArgument(
1783 "write procedure step requires a transaction".into(),
1784 )
1785 })?;
1786 let pk = eval_value(pk, args, outputs)?;
1787 let handle = self.table(table)?;
1788 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
1789 MongrelError::NotFound("procedure delete_by_pk target not found".into())
1790 })?;
1791 tx.delete(table, row_id)?;
1792 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
1793 }
1794 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
1795 "DeleteRows procedure step is not supported by the core executor yet".into(),
1796 )),
1797 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
1798 "SqlQuery procedure step must be executed by mongreldb-query".into(),
1799 )),
1800 }
1801 }
1802
1803 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
1804 let cat = self.catalog.read();
1805 for step in &procedure.body.steps {
1806 let Some(table_name) = step.table() else {
1807 continue;
1808 };
1809 let schema = &cat
1810 .live(table_name)
1811 .ok_or_else(|| {
1812 MongrelError::InvalidArgument(format!(
1813 "procedure {:?} references unknown table {table_name:?}",
1814 procedure.name
1815 ))
1816 })?
1817 .schema;
1818 match step {
1819 ProcedureStep::NativeQuery {
1820 conditions,
1821 projection,
1822 ..
1823 } => {
1824 for condition in conditions {
1825 validate_condition_columns(condition, schema)?;
1826 }
1827 if let Some(projection) = projection {
1828 for id in projection {
1829 validate_column_id(*id, schema)?;
1830 }
1831 }
1832 }
1833 ProcedureStep::Put { cells, .. } => {
1834 for cell in cells {
1835 validate_column_id(cell.column_id, schema)?;
1836 }
1837 }
1838 ProcedureStep::Upsert {
1839 cells,
1840 update_cells,
1841 ..
1842 } => {
1843 for cell in cells {
1844 validate_column_id(cell.column_id, schema)?;
1845 }
1846 if let Some(update_cells) = update_cells {
1847 for cell in update_cells {
1848 validate_column_id(cell.column_id, schema)?;
1849 }
1850 }
1851 }
1852 ProcedureStep::DeleteByPk { .. } => {
1853 if schema.primary_key().is_none() {
1854 return Err(MongrelError::InvalidArgument(format!(
1855 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
1856 procedure.name
1857 )));
1858 }
1859 }
1860 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
1861 }
1862 }
1863 Ok(())
1864 }
1865
1866 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
1867 let cat = self.catalog.read();
1868 let target_schema = match &trigger.target {
1869 TriggerTarget::Table(target_name) => cat
1870 .live(target_name)
1871 .ok_or_else(|| {
1872 MongrelError::InvalidArgument(format!(
1873 "trigger {:?} references unknown target table {target_name:?}",
1874 trigger.name
1875 ))
1876 })?
1877 .schema
1878 .clone(),
1879 TriggerTarget::View(_) => Schema {
1880 columns: trigger.target_columns.clone(),
1881 ..Schema::default()
1882 },
1883 };
1884 for col in &trigger.update_of {
1885 if target_schema.column(col).is_none() {
1886 return Err(MongrelError::InvalidArgument(format!(
1887 "trigger {:?} UPDATE OF references unknown column {col:?}",
1888 trigger.name
1889 )));
1890 }
1891 }
1892 if let Some(expr) = &trigger.when {
1893 validate_trigger_expr(expr, &target_schema, trigger.event)?;
1894 }
1895 let mut select_schemas: HashMap<String, &Schema> = HashMap::new();
1896 for step in &trigger.program.steps {
1897 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
1898 {
1899 return Err(MongrelError::InvalidArgument(
1900 "SetNew trigger steps are only valid in BEFORE triggers".into(),
1901 ));
1902 }
1903 validate_trigger_step(
1904 step,
1905 &cat,
1906 &target_schema,
1907 trigger.event,
1908 &mut select_schemas,
1909 )?;
1910 }
1911 Ok(())
1912 }
1913
1914 pub fn begin(&self) -> crate::txn::Transaction<'_> {
1916 self.begin_with_isolation(crate::txn::IsolationLevel::default())
1917 }
1918
1919 pub fn begin_with_isolation(
1921 &self,
1922 level: crate::txn::IsolationLevel,
1923 ) -> crate::txn::Transaction<'_> {
1924 let txn_id = self.alloc_txn_id();
1925 let epoch = match level {
1926 crate::txn::IsolationLevel::ReadCommitted => self.epoch.visible(),
1927 _ => self.epoch.visible(),
1928 };
1929 let read = Snapshot::at(epoch);
1930 crate::txn::Transaction::new(self, txn_id, read)
1931 }
1932
1933 pub fn begin_with_external_trigger_bridge<'a>(
1936 &'a self,
1937 bridge: &'a dyn ExternalTriggerBridge,
1938 ) -> crate::txn::Transaction<'a> {
1939 let txn_id = self.alloc_txn_id();
1940 let read = Snapshot::at(self.epoch.visible());
1941 crate::txn::Transaction::new(self, txn_id, read).with_external_trigger_bridge(bridge)
1942 }
1943
1944 pub fn transaction<T>(
1946 &self,
1947 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
1948 ) -> Result<T> {
1949 let mut tx = self.begin();
1950 match f(&mut tx) {
1951 Ok(out) => {
1952 tx.commit()?;
1953 Ok(out)
1954 }
1955 Err(e) => {
1956 tx.rollback();
1957 Err(e)
1958 }
1959 }
1960 }
1961
1962 pub fn transaction_with_external_trigger_bridge<'a, T>(
1965 &'a self,
1966 bridge: &'a dyn ExternalTriggerBridge,
1967 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
1968 ) -> Result<T> {
1969 let mut tx = self.begin_with_external_trigger_bridge(bridge);
1970 match f(&mut tx) {
1971 Ok(out) => {
1972 tx.commit()?;
1973 Ok(out)
1974 }
1975 Err(e) => {
1976 tx.rollback();
1977 Err(e)
1978 }
1979 }
1980 }
1981
1982 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
1985 self.active_txns.register(epoch)
1986 }
1987
1988 fn fill_auto_increment_for_staging(
1989 &self,
1990 staging: &mut [(u64, crate::txn::Staged)],
1991 ) -> Result<()> {
1992 let tables = self.tables.read();
1993 for (table_id, staged) in staging {
1994 if let crate::txn::Staged::Put(cells) = staged {
1995 if let Some(handle) = tables.get(table_id) {
1996 let mut t = handle.lock();
1997 t.fill_auto_inc(cells)?;
1998 }
1999 }
2000 }
2001 Ok(())
2002 }
2003
2004 fn expand_table_triggers(
2005 &self,
2006 staging: &mut Vec<(u64, crate::txn::Staged)>,
2007 read_epoch: Epoch,
2008 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
2009 external_states: &mut Vec<(String, Vec<u8>)>,
2010 ) -> Result<()> {
2011 let mut external_writes = Vec::new();
2012 let config = self.trigger_config();
2013 if config.recursive_triggers {
2014 let chunk = std::mem::take(staging);
2015 let stacks = vec![Vec::new(); chunk.len()];
2016 *staging = self.expand_trigger_chunk(
2017 chunk,
2018 stacks,
2019 read_epoch,
2020 0,
2021 config.max_depth,
2022 &mut external_writes,
2023 &config,
2024 )?;
2025 self.apply_external_trigger_writes(
2026 external_writes,
2027 external_trigger_bridge,
2028 external_states,
2029 staging,
2030 )?;
2031 return Ok(());
2032 }
2033
2034 let mut expansion = self.expand_table_triggers_once(staging, read_epoch, None, &config)?;
2035 if !expansion.before.is_empty() {
2036 let mut final_staging = expansion.before;
2037 final_staging.extend(filter_ignored_staging(
2038 std::mem::take(staging),
2039 &expansion.ignored_indices,
2040 ));
2041 *staging = final_staging;
2042 } else if !expansion.ignored_indices.is_empty() {
2043 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
2044 }
2045 staging.append(&mut expansion.after);
2046 external_writes.append(&mut expansion.before_external);
2047 external_writes.append(&mut expansion.after_external);
2048 self.apply_external_trigger_writes(
2049 external_writes,
2050 external_trigger_bridge,
2051 external_states,
2052 staging,
2053 )?;
2054 Ok(())
2055 }
2056
2057 #[allow(clippy::too_many_arguments)]
2058 fn expand_trigger_chunk(
2059 &self,
2060 mut chunk: Vec<(u64, crate::txn::Staged)>,
2061 stacks: Vec<Vec<String>>,
2062 read_epoch: Epoch,
2063 depth: u32,
2064 max_depth: u32,
2065 external_writes: &mut Vec<ExternalTriggerWrite>,
2066 config: &TriggerConfig,
2067 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
2068 if chunk.is_empty() {
2069 return Ok(Vec::new());
2070 }
2071 self.fill_auto_increment_for_staging(&mut chunk)?;
2072 let expansion =
2073 self.expand_table_triggers_once(&mut chunk, read_epoch, Some(&stacks), config)?;
2074 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
2075 let stack = expansion
2076 .before_stacks
2077 .first()
2078 .or_else(|| expansion.after_stacks.first())
2079 .cloned()
2080 .unwrap_or_default();
2081 return Err(MongrelError::Conflict(format!(
2082 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
2083 Self::format_trigger_stack(&stack)
2084 )));
2085 }
2086
2087 let mut out = Vec::new();
2088 external_writes.extend(expansion.before_external);
2089 out.extend(self.expand_trigger_chunk(
2090 expansion.before,
2091 expansion.before_stacks,
2092 read_epoch,
2093 depth + 1,
2094 max_depth,
2095 external_writes,
2096 config,
2097 )?);
2098 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
2099 external_writes.extend(expansion.after_external);
2100 out.extend(self.expand_trigger_chunk(
2101 expansion.after,
2102 expansion.after_stacks,
2103 read_epoch,
2104 depth + 1,
2105 max_depth,
2106 external_writes,
2107 config,
2108 )?);
2109 Ok(out)
2110 }
2111
2112 fn apply_external_trigger_writes(
2113 &self,
2114 writes: Vec<ExternalTriggerWrite>,
2115 bridge: Option<&dyn ExternalTriggerBridge>,
2116 external_states: &mut Vec<(String, Vec<u8>)>,
2117 staging: &mut Vec<(u64, crate::txn::Staged)>,
2118 ) -> Result<()> {
2119 if writes.is_empty() {
2120 return Ok(());
2121 }
2122 let bridge = bridge.ok_or_else(|| {
2123 MongrelError::InvalidArgument(
2124 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
2125 )
2126 })?;
2127 for write in writes {
2128 let table = write.table().to_string();
2129 let entry = self.external_table(&table).ok_or_else(|| {
2130 MongrelError::NotFound(format!("external table {table:?} not found"))
2131 })?;
2132 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
2133 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
2134 external_states.push((table, result.state));
2135 for base_write in result.base_writes {
2136 match base_write {
2137 ExternalTriggerBaseWrite::Put { table, cells } => {
2138 let table_id = self.table_id(&table)?;
2139 staging.push((table_id, crate::txn::Staged::Put(cells)));
2140 }
2141 ExternalTriggerBaseWrite::Delete { table, row_id } => {
2142 let table_id = self.table_id(&table)?;
2143 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
2144 }
2145 }
2146 }
2147 }
2148 dedup_external_states_in_place(external_states);
2149 Ok(())
2150 }
2151
2152 fn expand_table_triggers_once(
2153 &self,
2154 staging: &mut Vec<(u64, crate::txn::Staged)>,
2155 read_epoch: Epoch,
2156 trigger_stacks: Option<&[Vec<String>]>,
2157 config: &TriggerConfig,
2158 ) -> Result<TriggerExpansion> {
2159 let triggers: Vec<StoredTrigger> = self
2160 .catalog
2161 .read()
2162 .triggers
2163 .iter()
2164 .filter(|entry| {
2165 entry.trigger.enabled
2166 && matches!(
2167 entry.trigger.timing,
2168 TriggerTiming::Before | TriggerTiming::After
2169 )
2170 && matches!(entry.trigger.target, TriggerTarget::Table(_))
2171 })
2172 .map(|entry| entry.trigger.clone())
2173 .collect();
2174 if triggers.is_empty() || staging.is_empty() {
2175 return Ok(TriggerExpansion::default());
2176 }
2177
2178 let before_triggers = triggers
2179 .iter()
2180 .filter(|trigger| trigger.timing == TriggerTiming::Before)
2181 .cloned()
2182 .collect::<Vec<_>>();
2183 let after_triggers = triggers
2184 .iter()
2185 .filter(|trigger| trigger.timing == TriggerTiming::After)
2186 .cloned()
2187 .collect::<Vec<_>>();
2188
2189 let mut before_added = Vec::new();
2190 let mut before_stacks = Vec::new();
2191 let mut before_external = Vec::new();
2192 let mut ignored_indices = std::collections::BTreeSet::new();
2193 if !before_triggers.is_empty() {
2194 let before_events =
2195 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks)?;
2196 let mut out = TriggerProgramOutput {
2197 added: &mut before_added,
2198 added_stacks: &mut before_stacks,
2199 added_external: &mut before_external,
2200 ignored_indices: &mut ignored_indices,
2201 };
2202 self.execute_triggers_for_events(
2203 &before_triggers,
2204 &before_events,
2205 Some(staging),
2206 &mut out,
2207 config,
2208 read_epoch,
2209 )?;
2210 }
2211
2212 let after_events = if after_triggers.is_empty() {
2213 Vec::new()
2214 } else {
2215 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks)?
2216 .into_iter()
2217 .filter(|event| {
2218 !event
2219 .op_indices
2220 .iter()
2221 .any(|idx| ignored_indices.contains(idx))
2222 })
2223 .collect()
2224 };
2225
2226 let mut after_added = Vec::new();
2227 let mut after_stacks = Vec::new();
2228 let mut after_external = Vec::new();
2229 let mut out = TriggerProgramOutput {
2230 added: &mut after_added,
2231 added_stacks: &mut after_stacks,
2232 added_external: &mut after_external,
2233 ignored_indices: &mut ignored_indices,
2234 };
2235 self.execute_triggers_for_events(
2236 &after_triggers,
2237 &after_events,
2238 None,
2239 &mut out,
2240 config,
2241 read_epoch,
2242 )?;
2243 Ok(TriggerExpansion {
2244 before: before_added,
2245 before_stacks,
2246 before_external,
2247 after: after_added,
2248 after_stacks,
2249 after_external,
2250 ignored_indices,
2251 })
2252 }
2253
2254 fn execute_triggers_for_events(
2255 &self,
2256 triggers: &[StoredTrigger],
2257 events: &[WriteEvent],
2258 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
2259 out: &mut TriggerProgramOutput<'_>,
2260 config: &TriggerConfig,
2261 read_epoch: Epoch,
2262 ) -> Result<()> {
2263 for event in events {
2264 for trigger in triggers {
2265 if event
2266 .op_indices
2267 .iter()
2268 .any(|idx| out.ignored_indices.contains(idx))
2269 {
2270 break;
2271 }
2272 let matches = {
2273 let cat = self.catalog.read();
2274 trigger_matches_event(trigger, event, &cat)?
2275 };
2276 if !matches {
2277 continue;
2278 }
2279 if let Some(when) = &trigger.when {
2280 if !eval_trigger_expr(when, event)? {
2281 continue;
2282 }
2283 }
2284 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
2285 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
2286 return Err(MongrelError::Conflict(format!(
2287 "trigger recursion cycle detected; trigger stack: {}",
2288 Self::format_trigger_stack(&trigger_stack)
2289 )));
2290 }
2291 let outcome = match staging.as_mut() {
2292 Some(staging) => self.execute_trigger_program(
2293 trigger,
2294 event,
2295 Some(&mut **staging),
2296 out,
2297 &trigger_stack,
2298 config,
2299 read_epoch,
2300 )?,
2301 None => self.execute_trigger_program(
2302 trigger,
2303 event,
2304 None,
2305 out,
2306 &trigger_stack,
2307 config,
2308 read_epoch,
2309 )?,
2310 };
2311 if outcome == TriggerProgramOutcome::Ignore {
2312 out.ignored_indices.extend(event.op_indices.iter().copied());
2313 break;
2314 }
2315 }
2316 }
2317 Ok(())
2318 }
2319
2320 fn trigger_events_for_staging(
2321 &self,
2322 staging: &[(u64, crate::txn::Staged)],
2323 read_epoch: Epoch,
2324 trigger_stacks: Option<&[Vec<String>]>,
2325 ) -> Result<Vec<WriteEvent>> {
2326 use crate::txn::Staged;
2327 use std::collections::{HashMap, VecDeque};
2328
2329 let snapshot = Snapshot::at(read_epoch);
2330 let cat = self.catalog.read();
2331 let mut table_names = HashMap::new();
2332 let mut table_schemas = HashMap::new();
2333 for entry in cat
2334 .tables
2335 .iter()
2336 .filter(|entry| matches!(entry.state, TableState::Live))
2337 {
2338 table_names.insert(entry.table_id, entry.name.clone());
2339 table_schemas.insert(entry.table_id, entry.schema.clone());
2340 }
2341 drop(cat);
2342
2343 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
2344 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
2345 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
2346
2347 for (idx, (table_id, staged)) in staging.iter().enumerate() {
2348 let Some(schema) = table_schemas.get(table_id) else {
2349 continue;
2350 };
2351 let Some(pk) = schema.primary_key() else {
2352 continue;
2353 };
2354 match staged {
2355 Staged::Delete(row_id) => {
2356 let handle = self.table_by_id(*table_id)?;
2357 let Some(row) = handle.lock().get(*row_id, snapshot) else {
2358 continue;
2359 };
2360 let Some(pk_value) = row.columns.get(&pk.id) else {
2361 continue;
2362 };
2363 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
2364 delete_by_key
2365 .entry((*table_id, pk_value.encode_key()))
2366 .or_default()
2367 .push_back(idx);
2368 }
2369 Staged::Put(cells) => {
2370 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
2371 put_by_key
2372 .entry((*table_id, value.encode_key()))
2373 .or_default()
2374 .push_back(idx);
2375 }
2376 }
2377 Staged::Truncate => {}
2378 }
2379 }
2380
2381 let mut paired_delete = std::collections::HashSet::new();
2382 let mut paired_put = std::collections::HashSet::new();
2383 let mut events = Vec::new();
2384
2385 for (key, deletes) in delete_by_key.iter_mut() {
2386 let Some(puts) = put_by_key.get_mut(key) else {
2387 continue;
2388 };
2389 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
2390 paired_delete.insert(delete_idx);
2391 paired_put.insert(put_idx);
2392 let (table_id, _) = &staging[put_idx];
2393 let Some(table_name) = table_names.get(table_id).cloned() else {
2394 continue;
2395 };
2396 let old = old_rows.get(&delete_idx).cloned();
2397 let new = match &staging[put_idx].1 {
2398 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
2399 _ => None,
2400 };
2401 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
2402 events.push(WriteEvent {
2403 table: table_name,
2404 kind: TriggerEvent::Update,
2405 old,
2406 new,
2407 changed_columns,
2408 op_indices: vec![delete_idx, put_idx],
2409 put_idx: Some(put_idx),
2410 trigger_stack: Self::trigger_stack_for_indices(
2411 trigger_stacks,
2412 &[delete_idx, put_idx],
2413 ),
2414 });
2415 }
2416 }
2417
2418 for (idx, (table_id, staged)) in staging.iter().enumerate() {
2419 let Some(table_name) = table_names.get(table_id).cloned() else {
2420 continue;
2421 };
2422 match staged {
2423 Staged::Put(cells) if !paired_put.contains(&idx) => {
2424 let new = Some(TriggerRowImage::from_cells(cells));
2425 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
2426 events.push(WriteEvent {
2427 table: table_name,
2428 kind: TriggerEvent::Insert,
2429 old: None,
2430 new,
2431 changed_columns,
2432 op_indices: vec![idx],
2433 put_idx: Some(idx),
2434 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
2435 });
2436 }
2437 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
2438 let old = match old_rows.get(&idx).cloned() {
2439 Some(old) => Some(old),
2440 None => {
2441 let handle = self.table_by_id(*table_id)?;
2442 let row = handle.lock().get(*row_id, snapshot);
2443 row.map(TriggerRowImage::from_row)
2444 }
2445 };
2446 let Some(old) = old else {
2447 continue;
2448 };
2449 let changed_columns = old.columns.keys().copied().collect();
2450 events.push(WriteEvent {
2451 table: table_name,
2452 kind: TriggerEvent::Delete,
2453 old: Some(old),
2454 new: None,
2455 changed_columns,
2456 op_indices: vec![idx],
2457 put_idx: None,
2458 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
2459 });
2460 }
2461 Staged::Truncate => {}
2462 _ => {}
2463 }
2464 }
2465
2466 Ok(events)
2467 }
2468
2469 #[allow(clippy::too_many_arguments)]
2470 fn execute_trigger_program(
2471 &self,
2472 trigger: &StoredTrigger,
2473 event: &WriteEvent,
2474 staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
2475 out: &mut TriggerProgramOutput<'_>,
2476 trigger_stack: &[String],
2477 config: &TriggerConfig,
2478 read_epoch: Epoch,
2479 ) -> Result<TriggerProgramOutcome> {
2480 let mut event = event.clone();
2481 let mut select_results: HashMap<String, Vec<TriggerRowImage>> = HashMap::new();
2482 self.execute_trigger_steps(
2483 trigger,
2484 &trigger.program.steps,
2485 &mut event,
2486 staging,
2487 out,
2488 trigger_stack,
2489 config,
2490 &mut select_results,
2491 0,
2492 None,
2493 read_epoch,
2494 )
2495 }
2496
2497 #[allow(clippy::too_many_arguments)]
2498 fn execute_trigger_steps(
2499 &self,
2500 trigger: &StoredTrigger,
2501 steps: &[TriggerStep],
2502 event: &mut WriteEvent,
2503 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
2504 out: &mut TriggerProgramOutput<'_>,
2505 trigger_stack: &[String],
2506 config: &TriggerConfig,
2507 select_results: &mut HashMap<String, Vec<TriggerRowImage>>,
2508 depth: u32,
2509 selected: Option<&TriggerRowImage>,
2510 read_epoch: Epoch,
2511 ) -> Result<TriggerProgramOutcome> {
2512 let _ = depth;
2513 for step in steps {
2514 match step {
2515 TriggerStep::SetNew { cells } => {
2516 if trigger.timing != TriggerTiming::Before {
2517 return Err(MongrelError::InvalidArgument(
2518 "SetNew trigger step is only valid in BEFORE triggers".into(),
2519 ));
2520 }
2521 let put_idx = event.put_idx.ok_or_else(|| {
2522 MongrelError::InvalidArgument(
2523 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
2524 )
2525 })?;
2526 let staging = staging.as_deref_mut().ok_or_else(|| {
2527 MongrelError::InvalidArgument(
2528 "SetNew trigger step requires mutable trigger staging".into(),
2529 )
2530 })?;
2531 let Some((_, crate::txn::Staged::Put(row_cells))) = staging.get_mut(put_idx)
2532 else {
2533 return Err(MongrelError::InvalidArgument(
2534 "SetNew trigger step target row is not mutable".into(),
2535 ));
2536 };
2537 for (column_id, value) in eval_trigger_cells(cells, event, selected)? {
2538 row_cells.retain(|(id, _)| *id != column_id);
2539 row_cells.push((column_id, value.clone()));
2540 if let Some(new) = &mut event.new {
2541 new.columns.insert(column_id, value);
2542 }
2543 }
2544 row_cells.sort_by_key(|(id, _)| *id);
2545 }
2546 TriggerStep::Insert { table, cells } => {
2547 let cells = eval_trigger_cells(cells, event, selected)?;
2548 if let Ok(table_id) = self.table_id(table) {
2549 out.added.push((table_id, crate::txn::Staged::Put(cells)));
2550 out.added_stacks.push(trigger_stack.to_vec());
2551 } else if self.external_table(table).is_some() {
2552 out.added_external.push(ExternalTriggerWrite::Insert {
2553 table: table.clone(),
2554 cells,
2555 });
2556 } else {
2557 return Err(MongrelError::NotFound(format!(
2558 "trigger {:?} insert target {table:?} not found",
2559 trigger.name
2560 )));
2561 }
2562 }
2563 TriggerStep::UpdateByPk { table, pk, cells } => {
2564 let pk = eval_trigger_value(pk, event, selected)?;
2565 let cells = eval_trigger_cells(cells, event, selected)?;
2566 if self.external_table(table).is_some() {
2567 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
2568 table: table.clone(),
2569 pk,
2570 cells,
2571 });
2572 } else {
2573 let row_id = self
2574 .table(table)?
2575 .lock()
2576 .lookup_pk(&pk.encode_key())
2577 .ok_or_else(|| {
2578 MongrelError::NotFound(format!(
2579 "trigger {:?} update target not found",
2580 trigger.name
2581 ))
2582 })?;
2583 let handle = self.table(table)?;
2584 let snapshot = Snapshot::at(self.epoch.visible());
2585 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
2586 MongrelError::NotFound(format!(
2587 "trigger {:?} update target not visible",
2588 trigger.name
2589 ))
2590 })?;
2591 let mut merged = old.columns;
2592 for (column_id, value) in cells {
2593 merged.insert(column_id, value);
2594 }
2595 out.added
2596 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
2597 out.added_stacks.push(trigger_stack.to_vec());
2598 out.added.push((
2599 self.table_id(table)?,
2600 crate::txn::Staged::Put(merged.into_iter().collect()),
2601 ));
2602 out.added_stacks.push(trigger_stack.to_vec());
2603 }
2604 }
2605 TriggerStep::DeleteByPk { table, pk } => {
2606 let pk = eval_trigger_value(pk, event, selected)?;
2607 if self.external_table(table).is_some() {
2608 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
2609 table: table.clone(),
2610 pk,
2611 });
2612 } else {
2613 let row_id = self
2614 .table(table)?
2615 .lock()
2616 .lookup_pk(&pk.encode_key())
2617 .ok_or_else(|| {
2618 MongrelError::NotFound(format!(
2619 "trigger {:?} delete target not found",
2620 trigger.name
2621 ))
2622 })?;
2623 out.added
2624 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
2625 out.added_stacks.push(trigger_stack.to_vec());
2626 }
2627 }
2628 TriggerStep::Select {
2629 id,
2630 table,
2631 conditions,
2632 } => {
2633 let schema = self.table(table)?.lock().schema().clone();
2634 let snapshot = Snapshot::at(read_epoch);
2635 let rows = self.table(table)?.lock().visible_rows(snapshot)?;
2636 let mut matched = Vec::new();
2637 for row in rows {
2638 let image = TriggerRowImage::from_row(row);
2639 let passes = conditions
2640 .iter()
2641 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
2642 .collect::<Result<Vec<_>>>()?
2643 .into_iter()
2644 .all(|b| b);
2645 if passes {
2646 matched.push(image);
2647 }
2648 }
2649 if let Some(pk) = schema.primary_key() {
2650 matched.sort_by(|a, b| {
2651 let av = a.columns.get(&pk.id).unwrap_or(&Value::Null);
2652 let bv = b.columns.get(&pk.id).unwrap_or(&Value::Null);
2653 value_order(av, bv).unwrap_or(std::cmp::Ordering::Equal)
2654 });
2655 }
2656 select_results.insert(id.clone(), matched);
2657 }
2658 TriggerStep::Foreach { id, steps } => {
2659 let rows = select_results.get(id).ok_or_else(|| {
2660 MongrelError::InvalidArgument(format!(
2661 "trigger {:?} foreach references unknown select id {id:?}",
2662 trigger.name
2663 ))
2664 })?;
2665 if rows.len() > config.max_loop_iterations as usize {
2666 return Err(MongrelError::InvalidArgument(format!(
2667 "trigger {:?} foreach exceeded max_loop_iterations ({})",
2668 trigger.name, config.max_loop_iterations
2669 )));
2670 }
2671 for row in rows.clone() {
2672 let result = self.execute_trigger_steps(
2673 trigger,
2674 steps,
2675 event,
2676 staging.as_deref_mut(),
2677 out,
2678 trigger_stack,
2679 config,
2680 select_results,
2681 depth + 1,
2682 Some(&row),
2683 read_epoch,
2684 )?;
2685 if result == TriggerProgramOutcome::Ignore {
2686 return Ok(TriggerProgramOutcome::Ignore);
2687 }
2688 }
2689 }
2690 TriggerStep::DeleteWhere { table, conditions } => {
2691 let schema = self.table(table)?.lock().schema().clone();
2692 let snapshot = Snapshot::at(read_epoch);
2693 let rows = self.table(table)?.lock().visible_rows(snapshot)?;
2694 let table_id = self.table_id(table)?;
2695 let mut to_delete = Vec::new();
2696 for row in rows {
2697 let image = TriggerRowImage::from_row(row.clone());
2698 let passes = conditions
2699 .iter()
2700 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
2701 .collect::<Result<Vec<_>>>()?
2702 .into_iter()
2703 .all(|b| b);
2704 if passes {
2705 to_delete.push((table_id, row.row_id));
2706 }
2707 }
2708 for (table_id, row_id) in to_delete {
2709 out.added
2710 .push((table_id, crate::txn::Staged::Delete(row_id)));
2711 out.added_stacks.push(trigger_stack.to_vec());
2712 }
2713 }
2714 TriggerStep::UpdateWhere {
2715 table,
2716 conditions,
2717 cells,
2718 } => {
2719 let schema = self.table(table)?.lock().schema().clone();
2720 let snapshot = Snapshot::at(read_epoch);
2721 let rows = self.table(table)?.lock().visible_rows(snapshot)?;
2722 let table_id = self.table_id(table)?;
2723 let mut to_update = Vec::new();
2724 for row in rows {
2725 let image = TriggerRowImage::from_row(row.clone());
2726 let passes = conditions
2727 .iter()
2728 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
2729 .collect::<Result<Vec<_>>>()?
2730 .into_iter()
2731 .all(|b| b);
2732 if passes {
2733 let new_cells = cells
2734 .iter()
2735 .map(|cell| {
2736 Ok((
2737 cell.column_id,
2738 eval_trigger_value(&cell.value, event, Some(&image))?,
2739 ))
2740 })
2741 .collect::<Result<Vec<_>>>()?;
2742 let mut merged = row.columns.clone();
2743 for (column_id, value) in new_cells {
2744 merged.insert(column_id, value);
2745 }
2746 to_update.push((table_id, row.row_id, merged));
2747 }
2748 }
2749 for (table_id, row_id, merged) in to_update {
2750 out.added
2751 .push((table_id, crate::txn::Staged::Delete(row_id)));
2752 out.added_stacks.push(trigger_stack.to_vec());
2753 out.added.push((
2754 table_id,
2755 crate::txn::Staged::Put(merged.into_iter().collect()),
2756 ));
2757 out.added_stacks.push(trigger_stack.to_vec());
2758 }
2759 }
2760 TriggerStep::Raise { action, message } => match action {
2761 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
2762 TriggerRaiseAction::Abort
2763 | TriggerRaiseAction::Fail
2764 | TriggerRaiseAction::Rollback => {
2765 let message = eval_trigger_value(message, event, selected)?;
2766 return Err(MongrelError::Conflict(format!(
2767 "trigger {:?} raised: {}; trigger stack: {}",
2768 trigger.name,
2769 trigger_message(message),
2770 Self::format_trigger_stack(trigger_stack)
2771 )));
2772 }
2773 },
2774 }
2775 }
2776 Ok(TriggerProgramOutcome::Continue)
2777 }
2778
2779 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
2780 let Some(stacks) = stacks else {
2781 return Vec::new();
2782 };
2783 let mut out = Vec::new();
2784 for idx in indices {
2785 let Some(stack) = stacks.get(*idx) else {
2786 continue;
2787 };
2788 for name in stack {
2789 if !out.iter().any(|existing| existing == name) {
2790 out.push(name.clone());
2791 }
2792 }
2793 }
2794 out
2795 }
2796
2797 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
2798 let mut out = stack.to_vec();
2799 out.push(trigger_name.to_string());
2800 out
2801 }
2802
2803 fn format_trigger_stack(stack: &[String]) -> String {
2804 if stack.is_empty() {
2805 "<root>".into()
2806 } else {
2807 stack.join(" -> ")
2808 }
2809 }
2810
2811 fn validate_constraints(
2826 &self,
2827 staging: &mut Vec<(u64, crate::txn::Staged)>,
2828 read_epoch: Epoch,
2829 ) -> Result<()> {
2830 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
2831 use crate::memtable::Row;
2832 use crate::txn::Staged;
2833 use std::collections::HashSet;
2834
2835 let snapshot = Snapshot::at(read_epoch);
2836 let cat = self.catalog.read();
2837
2838 let live: Vec<(u64, &str, &crate::schema::Schema)> = cat
2840 .tables
2841 .iter()
2842 .filter(|e| matches!(e.state, TableState::Live))
2843 .map(|e| (e.table_id, e.name.as_str(), &e.schema))
2844 .collect();
2845
2846 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
2848 if !any_constraints {
2849 return Ok(());
2850 }
2851
2852 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
2854 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
2855 if let Some(r) = rows_cache.get(&table_id) {
2856 return Ok(r.clone());
2857 }
2858 let handle = self.table_by_id(table_id)?;
2859 let rows = handle.lock().visible_rows(snapshot)?;
2860 rows_cache.insert(table_id, rows.clone());
2861 Ok(rows)
2862 };
2863
2864 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
2869 loop {
2870 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
2871 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
2872 .iter()
2873 .filter_map(|(t, op)| match op {
2874 Staged::Delete(rid) => Some((*t, *rid)),
2875 _ => None,
2876 })
2877 .collect();
2878 for (table_id, rid) in deletes {
2879 if !cascaded.insert((table_id, rid.0)) {
2880 continue;
2881 }
2882 let Some(tname) = live
2883 .iter()
2884 .find(|(t, _, _)| *t == table_id)
2885 .map(|(_, n, _)| *n)
2886 else {
2887 continue;
2888 };
2889 let parent_handle = self.table_by_id(table_id)?;
2890 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
2891 continue;
2892 };
2893 for (child_id, _child_name, child_schema) in &live {
2894 for fk in &child_schema.constraints.foreign_keys {
2895 if fk.ref_table != tname {
2896 continue;
2897 }
2898 let Some(parent_key) =
2899 encode_composite_key(&fk.ref_columns, &parent_row.columns)
2900 else {
2901 continue;
2902 };
2903 match fk.on_delete {
2904 FkAction::Restrict => continue,
2905 FkAction::Cascade => {
2906 let child_rows = load_rows(*child_id)?;
2907 for cr in &child_rows {
2908 if !cascaded.contains(&(*child_id, cr.row_id.0))
2909 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
2910 == Some(parent_key.as_slice())
2911 {
2912 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
2913 }
2914 }
2915 }
2916 FkAction::SetNull => {
2917 let child_rows = load_rows(*child_id)?;
2918 for cr in &child_rows {
2919 if !cascaded.contains(&(*child_id, cr.row_id.0))
2920 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
2921 == Some(parent_key.as_slice())
2922 {
2923 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
2926 .columns
2927 .iter()
2928 .map(|(k, v)| (*k, v.clone()))
2929 .collect();
2930 for cid in &fk.columns {
2931 cells.retain(|(k, _)| k != cid);
2932 cells.push((*cid, crate::memtable::Value::Null));
2933 }
2934 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
2935 new_ops.push((*child_id, Staged::Put(cells)));
2936 }
2937 }
2938 }
2939 }
2940 }
2941 }
2942 }
2943 if new_ops.is_empty() {
2944 break;
2945 }
2946 staging.extend(new_ops);
2947 }
2948
2949 let staged_deletes: HashSet<(u64, u64)> = staging
2953 .iter()
2954 .filter_map(|(t, op)| match op {
2955 Staged::Delete(rid) => Some((*t, rid.0)),
2956 _ => None,
2957 })
2958 .collect();
2959
2960 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
2962
2963 for (table_id, op) in staging.iter() {
2965 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id).copied()
2966 else {
2967 continue;
2968 };
2969 let cells_map: HashMap<u16, crate::memtable::Value>;
2970 match op {
2971 Staged::Put(cells) => {
2972 cells_map = cells.iter().cloned().collect();
2973
2974 if !schema.constraints.checks.is_empty() {
2976 validate_checks(&schema.constraints.checks, &cells_map)?;
2977 }
2978
2979 for uc in &schema.constraints.uniques {
2981 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
2982 continue; };
2984 let marker = (*table_id, uc.id, key.clone());
2985 if !seen_unique.insert(marker) {
2986 return Err(MongrelError::Conflict(format!(
2987 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
2988 uc.name
2989 )));
2990 }
2991 let rows = load_rows(*table_id)?;
2992 for r in &rows {
2993 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
2996 continue;
2997 }
2998 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
2999 if theirs == key {
3000 return Err(MongrelError::Conflict(format!(
3001 "UNIQUE constraint '{}' on table '{tname}' violated",
3002 uc.name
3003 )));
3004 }
3005 }
3006 }
3007 }
3008
3009 for fk in &schema.constraints.foreign_keys {
3011 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
3012 continue; };
3014 let Some(parent_id) = cat
3015 .tables
3016 .iter()
3017 .find(|t| t.name == fk.ref_table)
3018 .map(|t| t.table_id)
3019 else {
3020 return Err(MongrelError::InvalidArgument(format!(
3021 "FOREIGN KEY '{}' references unknown table '{}'",
3022 fk.name, fk.ref_table
3023 )));
3024 };
3025 let parent_rows = load_rows(parent_id)?;
3026 let mut found = false;
3027 for r in &parent_rows {
3028 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
3029 continue;
3030 }
3031 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
3032 if pkey == child_key {
3033 found = true;
3034 break;
3035 }
3036 }
3037 }
3038 if !found {
3039 return Err(MongrelError::Conflict(format!(
3040 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
3041 fk.name, fk.ref_table
3042 )));
3043 }
3044 }
3045 }
3046 Staged::Delete(rid) => {
3047 let parent_handle = self.table_by_id(*table_id)?;
3051 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
3052 continue;
3053 };
3054 for (child_id, child_name, child_schema) in &live {
3055 for fk in &child_schema.constraints.foreign_keys {
3056 if fk.ref_table != tname || fk.on_delete != FkAction::Restrict {
3057 continue;
3058 }
3059 let Some(parent_key) =
3060 encode_composite_key(&fk.ref_columns, &parent_row.columns)
3061 else {
3062 continue;
3063 };
3064 let child_rows = load_rows(*child_id)?;
3065 for r in &child_rows {
3066 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
3069 continue;
3070 }
3071 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
3072 if ck == parent_key {
3073 return Err(MongrelError::Conflict(format!(
3074 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
3075 fk.name
3076 )));
3077 }
3078 }
3079 }
3080 }
3081 }
3082 }
3083 Staged::Truncate => {
3084 for (child_id, child_name, child_schema) in &live {
3088 for fk in &child_schema.constraints.foreign_keys {
3089 if fk.ref_table != tname {
3090 continue;
3091 }
3092 let child_rows = load_rows(*child_id)?;
3093 if child_rows
3094 .iter()
3095 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
3096 {
3097 return Err(MongrelError::Conflict(format!(
3098 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
3099 fk.name
3100 )));
3101 }
3102 }
3103 }
3104 }
3105 }
3106 }
3107 Ok(())
3108 }
3109
3110 pub(crate) fn commit_transaction_with_external_states(
3117 &self,
3118 txn_id: u64,
3119 read_epoch: Epoch,
3120 mut staging: Vec<(u64, crate::txn::Staged)>,
3121 external_states: Vec<(String, Vec<u8>)>,
3122 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
3123 ) -> Result<Epoch> {
3124 use crate::memtable::Row;
3125 use crate::txn::{Staged, StagedOp, WriteKey};
3126 use crate::wal::Op;
3127 use std::collections::hash_map::DefaultHasher;
3128 use std::hash::{Hash, Hasher};
3129 use std::sync::atomic::Ordering;
3130
3131 if self.poisoned.load(Ordering::Relaxed) {
3132 return Err(MongrelError::Other(
3133 "database poisoned by fsync error".into(),
3134 ));
3135 }
3136 let mut external_states = dedup_external_states(external_states);
3137 if !external_states.is_empty() {
3138 let cat = self.catalog.read();
3139 for (name, _) in &external_states {
3140 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
3141 return Err(MongrelError::NotFound(format!(
3142 "external table {name:?} not found"
3143 )));
3144 }
3145 }
3146 }
3147
3148 self.fill_auto_increment_for_staging(&mut staging)?;
3151 self.expand_table_triggers(
3152 &mut staging,
3153 read_epoch,
3154 external_trigger_bridge,
3155 &mut external_states,
3156 )?;
3157 self.fill_auto_increment_for_staging(&mut staging)?;
3158 external_states = dedup_external_states(external_states);
3159
3160 self.validate_constraints(&mut staging, read_epoch)?;
3165
3166 let write_keys = {
3167 let cat = self.catalog.read();
3168 let mut keys: Vec<WriteKey> = Vec::new();
3169 for (table_id, staged) in &staging {
3170 match staged {
3171 Staged::Put(cells) => {
3172 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
3173 for col in &entry.schema.columns {
3174 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
3175 if let Some((_, val)) =
3176 cells.iter().find(|(id, _)| *id == col.id)
3177 {
3178 let mut h = DefaultHasher::new();
3179 val.encode_key().hash(&mut h);
3180 keys.push(WriteKey::Unique {
3181 table_id: *table_id,
3182 index_id: 0,
3183 key_hash: h.finish(),
3184 });
3185 }
3186 }
3187 }
3188 for uc in &entry.schema.constraints.uniques {
3195 if let Some(key_bytes) = crate::constraint::encode_composite_key(
3196 &uc.columns,
3197 &cells.iter().cloned().collect(),
3198 ) {
3199 let mut h = DefaultHasher::new();
3200 key_bytes.hash(&mut h);
3201 keys.push(WriteKey::Unique {
3202 table_id: *table_id,
3203 index_id: uc.id | 0x8000,
3204 key_hash: h.finish(),
3205 });
3206 }
3207 }
3208 }
3209 }
3210 Staged::Delete(rid) => keys.push(WriteKey::Row {
3211 table_id: *table_id,
3212 row_id: rid.0,
3213 }),
3214 Staged::Truncate => keys.push(WriteKey::Table {
3215 table_id: *table_id,
3216 }),
3217 }
3218 }
3219 for (name, _) in &external_states {
3220 let mut h = DefaultHasher::new();
3221 name.hash(&mut h);
3222 keys.push(WriteKey::Unique {
3223 table_id: EXTERNAL_TABLE_ID,
3224 index_id: 0,
3225 key_hash: h.finish(),
3226 });
3227 }
3228 keys
3229 };
3230
3231 let min_active = self.active_txns.min_read_epoch();
3233 if min_active < u64::MAX {
3234 self.conflicts.prune_below(Epoch(min_active));
3235 }
3236
3237 if self.conflicts.conflicts(&write_keys, read_epoch) {
3241 return Err(MongrelError::Conflict(
3242 "write-write conflict (pre-validate, first-committer-wins)".into(),
3243 ));
3244 }
3245 let pre_validate_version = self.conflicts.version();
3246
3247 let mut spilled: Vec<SpilledRun> = Vec::new();
3251 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
3252 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
3256 {
3257 let mut table_bytes: HashMap<u64, usize> = HashMap::new();
3258 for (table_id, staged) in &staging {
3259 if let Staged::Put(cells) = staged {
3260 *table_bytes.entry(*table_id).or_default() += cells.len() * 16;
3261 }
3262 }
3263 let tables = self.tables.read();
3264 for (&table_id, &bytes) in &table_bytes {
3265 if bytes as u64
3266 <= self
3267 .spill_threshold
3268 .load(std::sync::atomic::Ordering::Relaxed)
3269 {
3270 continue;
3271 }
3272 let Some(handle) = tables.get(&table_id) else {
3273 continue;
3274 };
3275 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
3276 let mut t = handle.lock();
3277 let tdir = t.table_dir().to_path_buf();
3278 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
3279 std::fs::create_dir_all(&txn_dir)?;
3280 let run_id = t.alloc_run_id() as u128;
3281 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
3282
3283 let mut rows: Vec<Row> = Vec::new();
3284 for (tid, staged) in &staging {
3285 if *tid != table_id {
3286 continue;
3287 }
3288 if let Staged::Put(cells) = staged {
3289 t.validate_cells_not_null(cells)?;
3290 let row_id = t.alloc_row_id();
3291 let mut row = Row::new(row_id, Epoch(0));
3292 for (c, v) in cells {
3293 row.columns.insert(*c, v.clone());
3294 }
3295 rows.push(row);
3296 }
3297 }
3298 let schema = t.schema_ref().clone();
3299 let kek = t.kek_ref().cloned();
3300 let specs = t.indexable_column_specs();
3301 drop(t);
3302
3303 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
3304 .uniform_epoch(true);
3305 if let Some(ref kek) = kek {
3306 writer = writer.with_encryption(kek.as_ref(), specs);
3307 }
3308 let header = writer.write(&pending_path, &rows)?;
3309 let row_count = header.row_count;
3310 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
3311 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
3312
3313 spilled.push(SpilledRun {
3314 table_id,
3315 run_id,
3316 pending_path,
3317 rows,
3318 row_count,
3319 min_rid,
3320 max_rid,
3321 });
3322 spilled_tables.insert(table_id);
3323 }
3324 }
3325
3326 if spill_guard.is_some() {
3328 if let Some(hook) = self.spill_hook.lock().as_ref() {
3329 hook();
3330 }
3331 }
3332
3333 let mut prebuilt: Vec<Option<Row>> = Vec::with_capacity(staging.len());
3344 {
3345 let tables = self.tables.read();
3346 for (table_id, staged) in &staging {
3347 match staged {
3348 Staged::Put(cells) if !spilled_tables.contains(table_id) => {
3349 let handle = tables.get(table_id).ok_or_else(|| {
3350 MongrelError::NotFound(format!("table {table_id} not mounted"))
3351 })?;
3352 let mut t = handle.lock();
3353 t.validate_cells_not_null(cells)?;
3354 let row_id = t.alloc_row_id();
3355 drop(t);
3356 let mut row = Row::new(row_id, Epoch(0));
3357 for (c, v) in cells {
3358 row.columns.insert(*c, v.clone());
3359 }
3360 prebuilt.push(Some(row));
3361 }
3362 Staged::Put(_) | Staged::Delete(_) | Staged::Truncate => prebuilt.push(None),
3363 }
3364 }
3365 }
3366
3367 let mut prepared_external = Vec::with_capacity(external_states.len());
3368 for (name, state) in &external_states {
3369 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
3370 prepared_external.push((name.clone(), state.clone(), pending));
3371 }
3372
3373 let added_runs: Vec<crate::wal::AddedRun> = spilled
3375 .iter()
3376 .map(|s| crate::wal::AddedRun {
3377 table_id: s.table_id,
3378 run_id: s.run_id,
3379 row_count: s.row_count,
3380 level: 0,
3381 min_row_id: s.min_rid,
3382 max_row_id: s.max_rid,
3383 content_hash: [0u8; 32],
3384 })
3385 .collect();
3386 let (new_epoch, applies, commit_seq) = {
3387 let mut wal = self.shared_wal.lock();
3388
3389 if self.conflicts.version() != pre_validate_version
3394 && self.conflicts.conflicts(&write_keys, read_epoch)
3395 {
3396 drop(wal);
3400 for s in &spilled {
3401 if let Some(parent) = s.pending_path.parent() {
3402 let _ = std::fs::remove_dir_all(parent);
3403 }
3404 }
3405 for (_, _, pending) in &prepared_external {
3406 let _ = std::fs::remove_file(pending);
3407 }
3408 return Err(MongrelError::Conflict(
3409 "write-write conflict (sequencer delta re-check)".into(),
3410 ));
3411 }
3412
3413 let new_epoch = self.epoch.bump_assigned();
3414 let mut applies: Vec<(u64, Vec<StagedOp>)> = Vec::new();
3415
3416 for (idx, (table_id, staged)) in staging.iter().enumerate() {
3417 if spilled_tables.contains(table_id) && matches!(staged, Staged::Put(_)) {
3420 continue;
3421 }
3422 let mut ops = Vec::new();
3423 match staged {
3424 Staged::Put(_) => {
3425 let mut row = prebuilt[idx].take().expect("prebuilt put row");
3427 row.committed_epoch = new_epoch;
3428 let payload = bincode::serialize(&vec![row.clone()])
3429 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
3430 wal.append(
3431 txn_id,
3432 *table_id,
3433 Op::Put {
3434 table_id: *table_id,
3435 rows: payload,
3436 },
3437 )?;
3438 ops.push(StagedOp::Put(row));
3439 }
3440 Staged::Delete(rid) => {
3441 wal.append(
3442 txn_id,
3443 *table_id,
3444 Op::Delete {
3445 table_id: *table_id,
3446 row_ids: vec![*rid],
3447 },
3448 )?;
3449 ops.push(StagedOp::Delete(*rid));
3450 }
3451 Staged::Truncate => {
3452 wal.append(
3453 txn_id,
3454 *table_id,
3455 Op::TruncateTable {
3456 table_id: *table_id,
3457 },
3458 )?;
3459 ops.push(StagedOp::Truncate);
3460 }
3461 }
3462 applies.push((*table_id, ops));
3463 }
3464
3465 for (name, state, _) in &prepared_external {
3466 wal.append(
3467 txn_id,
3468 EXTERNAL_TABLE_ID,
3469 Op::ExternalTableState {
3470 name: name.clone(),
3471 state: state.clone(),
3472 },
3473 )?;
3474 }
3475
3476 let commit_seq = wal.append_commit(txn_id, new_epoch, &added_runs)?;
3477
3478 self.conflicts.record(&write_keys, new_epoch);
3483 (new_epoch, applies, commit_seq)
3484 };
3485
3486 self.group
3488 .await_durable(&self.shared_wal, commit_seq)
3489 .inspect_err(|_| {
3490 self.poisoned.store(true, Ordering::Relaxed);
3491 })?;
3492
3493 {
3495 let tables = self.tables.read();
3496 for s in &spilled {
3498 if let Some(handle) = tables.get(&s.table_id) {
3499 let mut t = handle.lock();
3500 let dest = t.run_path(s.run_id as u64);
3501 std::fs::rename(&s.pending_path, &dest)?;
3502 if let Some(parent) = s.pending_path.parent() {
3504 let _ = std::fs::remove_dir_all(parent);
3505 }
3506 t.link_run(crate::manifest::RunRef {
3507 run_id: s.run_id,
3508 level: 0,
3509 epoch_created: new_epoch.0,
3510 row_count: s.row_count,
3511 });
3512 t.apply_run_metadata(&s.rows)?;
3517 t.invalidate_pending_cache();
3518 t.persist_manifest(new_epoch)?;
3519 }
3520 }
3521 for (table_id, ops) in applies {
3523 if let Some(handle) = tables.get(&table_id) {
3524 let mut t = handle.lock();
3525 for op in ops {
3526 match op {
3527 StagedOp::Put(row) => t.apply_put_rows(vec![row])?,
3528 StagedOp::Delete(rid) => t.apply_delete(rid, new_epoch),
3529 StagedOp::Truncate => t.apply_truncate(new_epoch)?,
3530 }
3531 }
3532 t.invalidate_pending_cache();
3533 t.persist_manifest(new_epoch)?;
3534 }
3535 }
3536 }
3537 for (name, _, pending) in &prepared_external {
3538 publish_external_state_file(&self.root, name, pending)?;
3539 }
3540
3541 self.advance_visible(new_epoch);
3542 Ok(new_epoch)
3543 }
3544
3545 fn advance_visible(&self, published: Epoch) {
3551 self.epoch.publish_in_order(published);
3552 }
3553
3554 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
3557 let e = self.epoch.visible();
3558 let g = self.snapshots.register(e);
3559 (Snapshot::at(e), g)
3560 }
3561
3562 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
3565 let e = self.epoch.visible();
3566 let g = self.snapshots.register_owned(e);
3567 (Snapshot::at(e), g)
3568 }
3569
3570 pub fn table_names(&self) -> Vec<String> {
3572 self.catalog
3573 .read()
3574 .tables
3575 .iter()
3576 .filter(|t| matches!(t.state, TableState::Live))
3577 .map(|t| t.name.clone())
3578 .collect()
3579 }
3580
3581 pub fn close(&self) -> Result<()> {
3587 for name in self.table_names() {
3588 if let Ok(handle) = self.table(&name) {
3589 if let Err(e) = handle.lock().close() {
3590 eprintln!("[close] flush failed for {name}: {e}");
3591 }
3592 }
3593 }
3594 Ok(())
3595 }
3596
3597 pub fn compact(&self) -> Result<(usize, usize)> {
3603 self.require(&crate::auth::Permission::Ddl)?;
3604 let mut compacted = 0;
3605 let mut skipped = 0;
3606 for name in self.table_names() {
3607 let Ok(handle) = self.table(&name) else {
3608 continue;
3609 };
3610 {
3611 let mut t = handle.lock();
3612 let before = t.run_count();
3613 if before < 2 {
3614 skipped += 1;
3615 continue;
3616 }
3617 match t.compact() {
3618 Ok(()) => {
3619 let after = t.run_count();
3620 compacted += 1;
3621 eprintln!("[compact] {name}: {before} -> {after} runs");
3622 }
3623 Err(e) => {
3624 eprintln!("[compact] {name}: compaction failed: {e}");
3625 skipped += 1;
3626 }
3627 }
3628 }
3629 }
3630 Ok((compacted, skipped))
3631 }
3632
3633 pub fn compact_table(&self, name: &str) -> Result<bool> {
3636 self.require(&crate::auth::Permission::Ddl)?;
3637 let handle = self.table(name)?;
3638 let mut t = handle.lock();
3639 let before = t.run_count();
3640 if before < 2 {
3641 return Ok(false);
3642 }
3643 t.compact()?;
3644 Ok(t.run_count() < before)
3645 }
3646
3647 pub fn table(&self, name: &str) -> Result<TableHandle> {
3649 let cat = self.catalog.read();
3650 let entry = cat
3651 .live(name)
3652 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
3653 let id = entry.table_id;
3654 drop(cat);
3655 self.tables
3656 .read()
3657 .get(&id)
3658 .cloned()
3659 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
3660 }
3661
3662 fn table_by_id(&self, id: u64) -> Result<TableHandle> {
3665 self.tables
3666 .read()
3667 .get(&id)
3668 .cloned()
3669 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
3670 }
3671
3672 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
3678 use crate::wal::DdlOp;
3679 use std::sync::atomic::Ordering;
3680
3681 self.require(&crate::auth::Permission::Ddl)?;
3682 if self.poisoned.load(Ordering::Relaxed) {
3683 return Err(MongrelError::Other(
3684 "database poisoned by fsync error".into(),
3685 ));
3686 }
3687
3688 let _g = self.ddl_lock.lock();
3689 {
3690 let cat = self.catalog.read();
3691 if cat.live(name).is_some() {
3692 return Err(MongrelError::InvalidArgument(format!(
3693 "table {name:?} already exists"
3694 )));
3695 }
3696 }
3697
3698 let commit_lock = Arc::clone(&self.commit_lock);
3701 let _c = commit_lock.lock();
3702 let table_id = {
3703 let mut cat = self.catalog.write();
3704 let id = cat.next_table_id;
3705 cat.next_table_id += 1;
3706 id
3707 };
3708 let epoch = self.epoch.bump_assigned();
3709 let txn_id = self.alloc_txn_id();
3710
3711 let mut schema = schema;
3715 schema.schema_id = table_id;
3716 schema.validate_auto_increment()?;
3723
3724 let schema_json = DdlOp::encode_schema(&schema)?;
3727 let commit_seq = {
3728 let mut wal = self.shared_wal.lock();
3729 wal.append(
3730 txn_id,
3731 table_id,
3732 crate::wal::Op::Ddl(DdlOp::CreateTable {
3733 table_id,
3734 name: name.to_string(),
3735 schema_json,
3736 }),
3737 )?;
3738 wal.append_commit(txn_id, epoch, &[])?
3739 };
3740 self.group
3741 .await_durable(&self.shared_wal, commit_seq)
3742 .inspect_err(|_| {
3743 self.poisoned.store(true, Ordering::Relaxed);
3744 })?;
3745
3746 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
3748 std::fs::create_dir_all(&tdir)?;
3749 let ctx = SharedCtx {
3750 epoch: Arc::clone(&self.epoch),
3751 page_cache: Arc::clone(&self.page_cache),
3752 decoded_cache: Arc::clone(&self.decoded_cache),
3753 snapshots: Arc::clone(&self.snapshots),
3754 kek: self.kek.clone(),
3755 commit_lock: Arc::clone(&self.commit_lock),
3756 shared: Some(crate::engine::SharedWalCtx {
3757 wal: Arc::clone(&self.shared_wal),
3758 group: Arc::clone(&self.group),
3759 poisoned: Arc::clone(&self.poisoned),
3760 txn_ids: Arc::clone(&self.next_txn_id),
3761 }),
3762 table_name: Some(name.to_string()),
3763 auth: self.table_auth_checker(),
3764 };
3765 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
3766
3767 {
3770 let mut cat = self.catalog.write();
3771 cat.tables.push(CatalogEntry {
3772 table_id,
3773 name: name.to_string(),
3774 schema,
3775 state: TableState::Live,
3776 created_epoch: epoch.0,
3777 });
3778 }
3779 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3780 self.tables
3781 .write()
3782 .insert(table_id, Arc::new(Mutex::new(table)));
3783
3784 self.advance_visible(epoch);
3785 Ok(table_id)
3786 }
3787
3788 pub fn drop_table(&self, name: &str) -> Result<()> {
3790 use crate::wal::DdlOp;
3791 use std::sync::atomic::Ordering;
3792
3793 self.require(&crate::auth::Permission::Ddl)?;
3794 if self.poisoned.load(Ordering::Relaxed) {
3795 return Err(MongrelError::Other(
3796 "database poisoned by fsync error".into(),
3797 ));
3798 }
3799
3800 let _g = self.ddl_lock.lock();
3801 let table_id = {
3802 let cat = self.catalog.read();
3803 cat.live(name)
3804 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
3805 .table_id
3806 };
3807
3808 let commit_lock = Arc::clone(&self.commit_lock);
3809 let _c = commit_lock.lock();
3810 let epoch = self.epoch.bump_assigned();
3811 let txn_id = self.alloc_txn_id();
3812 let commit_seq = {
3813 let mut wal = self.shared_wal.lock();
3814 wal.append(
3815 txn_id,
3816 table_id,
3817 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
3818 )?;
3819 wal.append_commit(txn_id, epoch, &[])?
3820 };
3821 self.group
3822 .await_durable(&self.shared_wal, commit_seq)
3823 .inspect_err(|_| {
3824 self.poisoned.store(true, Ordering::Relaxed);
3825 })?;
3826
3827 {
3828 let mut cat = self.catalog.write();
3829 let entry = cat
3830 .tables
3831 .iter_mut()
3832 .find(|t| t.table_id == table_id)
3833 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
3834 entry.state = TableState::Dropped { at_epoch: epoch.0 };
3835 cat.triggers.retain(|trigger| {
3836 !matches!(
3837 &trigger.trigger.target,
3838 TriggerTarget::Table(target) if target == name
3839 )
3840 });
3841 }
3842 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3843 self.tables.write().remove(&table_id);
3844
3845 self.advance_visible(epoch);
3846 Ok(())
3847 }
3848
3849 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
3858 use crate::wal::DdlOp;
3859 use std::sync::atomic::Ordering;
3860
3861 self.require(&crate::auth::Permission::Ddl)?;
3862 if self.poisoned.load(Ordering::Relaxed) {
3863 return Err(MongrelError::Other(
3864 "database poisoned by fsync error".into(),
3865 ));
3866 }
3867
3868 if name == new_name {
3871 return Ok(());
3872 }
3873 if new_name.is_empty() {
3874 return Err(MongrelError::InvalidArgument(
3875 "rename_table: new name must not be empty".into(),
3876 ));
3877 }
3878
3879 let _g = self.ddl_lock.lock();
3880 let table_id = {
3881 let cat = self.catalog.read();
3882 let src = cat
3883 .live(name)
3884 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
3885 if cat.live(new_name).is_some() {
3889 return Err(MongrelError::InvalidArgument(format!(
3890 "rename_table: a table named {new_name:?} already exists"
3891 )));
3892 }
3893 src.table_id
3894 };
3895
3896 let commit_lock = Arc::clone(&self.commit_lock);
3897 let _c = commit_lock.lock();
3898 let epoch = self.epoch.bump_assigned();
3899 let txn_id = self.alloc_txn_id();
3900 let commit_seq = {
3901 let mut wal = self.shared_wal.lock();
3902 wal.append(
3903 txn_id,
3904 table_id,
3905 crate::wal::Op::Ddl(DdlOp::RenameTable {
3906 table_id,
3907 new_name: new_name.to_string(),
3908 }),
3909 )?;
3910 wal.append_commit(txn_id, epoch, &[])?
3911 };
3912 self.group
3913 .await_durable(&self.shared_wal, commit_seq)
3914 .inspect_err(|_| {
3915 self.poisoned.store(true, Ordering::Relaxed);
3916 })?;
3917
3918 {
3919 let mut cat = self.catalog.write();
3920 let entry = cat
3921 .tables
3922 .iter_mut()
3923 .find(|t| t.table_id == table_id)
3924 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
3925 entry.name = new_name.to_string();
3926 for trigger in &mut cat.triggers {
3927 if matches!(
3928 &trigger.trigger.target,
3929 TriggerTarget::Table(target) if target == name
3930 ) {
3931 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
3932 }
3933 }
3934 }
3935 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3936 self.advance_visible(epoch);
3939 Ok(())
3940 }
3941
3942 pub fn alter_column(
3943 &self,
3944 table_name: &str,
3945 column_name: &str,
3946 change: AlterColumn,
3947 ) -> Result<ColumnDef> {
3948 use crate::wal::DdlOp;
3949 use std::sync::atomic::Ordering;
3950
3951 self.require(&crate::auth::Permission::Ddl)?;
3952 if self.poisoned.load(Ordering::Relaxed) {
3953 return Err(MongrelError::Other(
3954 "database poisoned by fsync error".into(),
3955 ));
3956 }
3957
3958 let _g = self.ddl_lock.lock();
3959 let table_id = {
3960 let cat = self.catalog.read();
3961 cat.live(table_name)
3962 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
3963 .table_id
3964 };
3965 let handle =
3966 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
3967 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
3968 })?;
3969 let mut table = handle.lock();
3970 let column = table.prepare_alter_column(column_name, &change)?;
3971 let renamed_column = (column.name != column_name).then(|| column.name.clone());
3972 if table
3973 .schema()
3974 .columns
3975 .iter()
3976 .find(|c| c.id == column.id)
3977 .is_some_and(|c| c == &column)
3978 {
3979 return Ok(column);
3980 }
3981
3982 let commit_lock = Arc::clone(&self.commit_lock);
3983 let _c = commit_lock.lock();
3984 let epoch = self.epoch.bump_assigned();
3985 let txn_id = self.alloc_txn_id();
3986 let column_json = DdlOp::encode_column(&column)?;
3987 let commit_seq = {
3988 let mut wal = self.shared_wal.lock();
3989 wal.append(
3990 txn_id,
3991 table_id,
3992 crate::wal::Op::Ddl(DdlOp::AlterTable {
3993 table_id,
3994 column_json,
3995 }),
3996 )?;
3997 wal.append_commit(txn_id, epoch, &[])?
3998 };
3999 self.group
4000 .await_durable(&self.shared_wal, commit_seq)
4001 .inspect_err(|_| {
4002 self.poisoned.store(true, Ordering::Relaxed);
4003 })?;
4004
4005 table.apply_altered_column(column.clone())?;
4006 let schema = table.schema().clone();
4007 drop(table);
4008
4009 {
4010 let mut cat = self.catalog.write();
4011 let entry = cat
4012 .tables
4013 .iter_mut()
4014 .find(|t| t.table_id == table_id)
4015 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
4016 entry.schema = schema;
4017 if let Some(new_column_name) = renamed_column {
4018 for trigger in &mut cat.triggers {
4019 if matches!(
4020 &trigger.trigger.target,
4021 TriggerTarget::Table(target) if target == table_name
4022 ) {
4023 trigger.trigger = trigger.trigger.renamed_update_column(
4024 column_name,
4025 new_column_name.clone(),
4026 epoch.0,
4027 )?;
4028 }
4029 }
4030 }
4031 }
4032 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
4033
4034 self.advance_visible(epoch);
4035 Ok(column)
4036 }
4037
4038 pub fn gc(&self) -> Result<usize> {
4044 self.require(&crate::auth::Permission::Ddl)?;
4045 let min_active = self.snapshots.min_active(self.epoch.visible()).0;
4046 let mut reclaimed = 0;
4047
4048 let cat = self.catalog.read();
4050 for entry in &cat.tables {
4051 if let TableState::Dropped { at_epoch } = entry.state {
4052 if at_epoch <= min_active {
4053 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
4054 if tdir.exists() {
4055 std::fs::remove_dir_all(&tdir)?;
4056 reclaimed += 1;
4057 }
4058 }
4059 }
4060 }
4061 drop(cat);
4062
4063 let cat = self.catalog.read();
4068 for entry in &cat.tables {
4069 if !matches!(entry.state, TableState::Live) {
4070 continue;
4071 }
4072 let txn_dir = self
4073 .root
4074 .join(TABLES_DIR)
4075 .join(entry.table_id.to_string())
4076 .join("_txn");
4077 if !txn_dir.exists() {
4078 continue;
4079 }
4080 for sub in std::fs::read_dir(&txn_dir)? {
4081 let sub = sub?;
4082 let name = sub.file_name();
4083 let Some(name) = name.to_str() else { continue };
4084 let is_active = name
4086 .parse::<u64>()
4087 .map(|id| self.active_spills.is_active(id))
4088 .unwrap_or(false);
4089 if is_active {
4090 continue;
4091 }
4092 std::fs::remove_dir_all(sub.path())?;
4093 reclaimed += 1;
4094 }
4095 }
4096 drop(cat);
4097
4098 let external_names = {
4099 let cat = self.catalog.read();
4100 cat.external_tables
4101 .iter()
4102 .map(|entry| entry.name.clone())
4103 .collect::<std::collections::HashSet<_>>()
4104 };
4105 let vtab_dir = self.root.join(VTAB_DIR);
4106 if vtab_dir.exists() {
4107 for entry in std::fs::read_dir(&vtab_dir)? {
4108 let entry = entry?;
4109 let name = entry.file_name();
4110 let Some(name) = name.to_str() else { continue };
4111 if external_names.contains(name) {
4112 continue;
4113 }
4114 let path = entry.path();
4115 if path.is_dir() {
4116 std::fs::remove_dir_all(path)?;
4117 } else {
4118 std::fs::remove_file(path)?;
4119 }
4120 reclaimed += 1;
4121 }
4122 }
4123
4124 let tables = self.tables.read();
4128 for handle in tables.values() {
4129 reclaimed += handle.lock().reap_retiring(Epoch(min_active))?;
4130 }
4131
4132 let all_durable = self.active_spills.is_idle()
4140 && tables.values().all(|h| {
4141 let g = h.lock();
4142 g.memtable_len() == 0 && g.mutable_run_len() == 0
4143 });
4144 drop(tables);
4145 if all_durable {
4146 reclaimed += self.shared_wal.lock().gc_segments(u64::MAX)?;
4147 }
4148
4149 Ok(reclaimed)
4150 }
4151 fn alloc_txn_id(&self) -> u64 {
4152 let mut g = self.next_txn_id.lock();
4153 let id = *g;
4154 *g = g.wrapping_add(1);
4155 id
4156 }
4157
4158 pub fn set_spill_threshold(&self, bytes: u64) {
4162 self.spill_threshold
4163 .store(bytes, std::sync::atomic::Ordering::Relaxed);
4164 }
4165
4166 #[doc(hidden)]
4170 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
4171 *self.spill_hook.lock() = Some(Box::new(f));
4172 }
4173
4174 #[doc(hidden)]
4178 pub fn __wal_group_sync_count(&self) -> u64 {
4179 self.shared_wal.lock().group_sync_count()
4180 }
4181
4182 #[doc(hidden)]
4185 pub fn __poison(&self) {
4186 self.poisoned
4187 .store(true, std::sync::atomic::Ordering::Relaxed);
4188 }
4189
4190 pub fn check(&self) -> Vec<CheckIssue> {
4203 let mut issues = Vec::new();
4204 let cat = self.catalog.read();
4205 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
4206 for entry in &cat.tables {
4207 if !matches!(entry.state, TableState::Live) {
4208 continue;
4209 }
4210 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
4211 let mut err = |sev: &str, desc: String| {
4212 issues.push(CheckIssue {
4213 table_id: entry.table_id,
4214 table_name: entry.name.clone(),
4215 severity: sev.into(),
4216 description: desc,
4217 });
4218 };
4219 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
4220 Ok(m) => m,
4221 Err(e) => {
4222 err("error", format!("manifest read failed: {e}"));
4223 continue;
4224 }
4225 };
4226 if m.flushed_epoch > m.current_epoch {
4227 err(
4228 "error",
4229 format!(
4230 "flushed_epoch {} exceeds current_epoch {} (impossible)",
4231 m.flushed_epoch, m.current_epoch
4232 ),
4233 );
4234 }
4235
4236 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
4237 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
4238 for rr in &m.runs {
4239 referenced.insert(rr.run_id);
4240 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
4241 if !run_path.exists() {
4242 err("error", format!("missing run file: r-{}.sr", rr.run_id));
4243 continue;
4244 }
4245 match crate::sorted_run::RunReader::open(
4246 &run_path,
4247 entry.schema.clone(),
4248 self.kek.clone(),
4249 ) {
4250 Ok(reader) => {
4251 if reader.row_count() as u64 != rr.row_count {
4252 err(
4253 "error",
4254 format!(
4255 "run r-{} row count mismatch: manifest {} vs run {}",
4256 rr.run_id,
4257 rr.row_count,
4258 reader.row_count()
4259 ),
4260 );
4261 }
4262 }
4263 Err(e) => {
4264 err(
4265 "error",
4266 format!("run r-{} integrity check failed: {e}", rr.run_id),
4267 );
4268 }
4269 }
4270 }
4271
4272 for r in &m.retiring {
4276 referenced.insert(r.run_id);
4277 }
4278
4279 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
4281 for ent in rd.flatten() {
4282 let p = ent.path();
4283 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
4284 continue;
4285 }
4286 let run_id = p
4287 .file_stem()
4288 .and_then(|s| s.to_str())
4289 .and_then(|s| s.strip_prefix("r-"))
4290 .and_then(|s| s.parse::<u128>().ok());
4291 if let Some(id) = run_id {
4292 if !referenced.contains(&id) {
4293 err(
4294 "warning",
4295 format!("orphan run file r-{id}.sr not referenced by the manifest"),
4296 );
4297 }
4298 }
4299 }
4300 }
4301 }
4302
4303 let external_names = cat
4304 .external_tables
4305 .iter()
4306 .map(|entry| entry.name.clone())
4307 .collect::<std::collections::HashSet<_>>();
4308 let vtab_dir = self.root.join(VTAB_DIR);
4309 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
4310 for entry in entries.flatten() {
4311 let name = entry.file_name();
4312 let Some(name) = name.to_str() else { continue };
4313 if !external_names.contains(name) {
4314 issues.push(CheckIssue {
4315 table_id: EXTERNAL_TABLE_ID,
4316 table_name: name.to_string(),
4317 severity: "warning".into(),
4318 description: format!(
4319 "orphan external table state entry {:?} not referenced by the catalog",
4320 entry.path()
4321 ),
4322 });
4323 }
4324 }
4325 }
4326
4327 for (seg, msg) in self.shared_wal.lock().verify_segments() {
4334 issues.push(CheckIssue {
4335 table_id: WAL_TABLE_ID,
4336 table_name: "<wal>".into(),
4337 severity: "error".into(),
4338 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
4339 });
4340 }
4341 issues
4342 }
4343
4344 pub fn doctor(&self) -> Result<Vec<u64>> {
4348 let _ddl = self.ddl_lock.lock();
4351 let issues = self.check();
4352 let bad_tables: std::collections::HashSet<u64> = issues
4357 .iter()
4358 .filter(|i| {
4359 i.severity == "error"
4360 && i.table_id != WAL_TABLE_ID
4361 && i.table_id != EXTERNAL_TABLE_ID
4362 })
4363 .map(|i| i.table_id)
4364 .collect();
4365 if bad_tables.is_empty() {
4366 return Ok(Vec::new());
4367 }
4368
4369 let qdir = self.root.join("_quarantine");
4370 std::fs::create_dir_all(&qdir)?;
4371 let mut quarantined = Vec::new();
4372 for &table_id in &bad_tables {
4373 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
4374 if tdir.exists() {
4375 let dest = qdir.join(table_id.to_string());
4376 std::fs::rename(&tdir, &dest)?;
4377 }
4378 {
4379 let mut cat = self.catalog.write();
4380 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
4381 entry.state = TableState::Dropped {
4382 at_epoch: self.epoch.visible().0,
4383 };
4384 }
4385 }
4386 self.tables.write().remove(&table_id);
4388 quarantined.push(table_id);
4389 }
4390 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
4391 Ok(quarantined)
4392 }
4393
4394 #[allow(dead_code)]
4396 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
4397 self.kek.as_ref()
4398 }
4399
4400 #[allow(dead_code)]
4402 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
4403 &self.epoch
4404 }
4405
4406 #[allow(dead_code)]
4408 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
4409 &self.snapshots
4410 }
4411}
4412
4413fn external_state_dir(root: &Path, name: &str) -> PathBuf {
4414 root.join(VTAB_DIR).join(name)
4415}
4416
4417fn filter_ignored_staging(
4418 staging: Vec<(u64, crate::txn::Staged)>,
4419 ignored_indices: &std::collections::BTreeSet<usize>,
4420) -> Vec<(u64, crate::txn::Staged)> {
4421 if ignored_indices.is_empty() {
4422 return staging;
4423 }
4424 staging
4425 .into_iter()
4426 .enumerate()
4427 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
4428 .collect()
4429}
4430
4431fn external_state_file(root: &Path, name: &str) -> PathBuf {
4432 external_state_dir(root, name).join("state.json")
4433}
4434
4435fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
4436 let path = external_state_file(root, name);
4437 match std::fs::read(path) {
4438 Ok(bytes) => Ok(bytes),
4439 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
4440 Err(e) => Err(e.into()),
4441 }
4442}
4443
4444fn current_external_state_bytes(
4445 root: &Path,
4446 external_states: &[(String, Vec<u8>)],
4447 name: &str,
4448) -> Result<Vec<u8>> {
4449 for (table, state) in external_states.iter().rev() {
4450 if table == name {
4451 return Ok(state.clone());
4452 }
4453 }
4454 read_external_state_file(root, name)
4455}
4456
4457fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
4458 let mut out = external_states;
4459 dedup_external_states_in_place(&mut out);
4460 out
4461}
4462
4463fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
4464 let mut seen = std::collections::HashSet::new();
4465 let mut out = Vec::with_capacity(external_states.len());
4466 for (name, state) in std::mem::take(external_states).into_iter().rev() {
4467 if seen.insert(name.clone()) {
4468 out.push((name, state));
4469 }
4470 }
4471 out.reverse();
4472 *external_states = out;
4473}
4474
4475fn prepare_external_state_file(
4476 root: &Path,
4477 name: &str,
4478 state: &[u8],
4479 txn_id: u64,
4480) -> Result<PathBuf> {
4481 let dir = external_state_dir(root, name);
4482 std::fs::create_dir_all(&dir)?;
4483 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
4484 {
4485 let mut file = std::fs::File::create(&pending)?;
4486 file.write_all(state)?;
4487 file.sync_all()?;
4488 }
4489 Ok(pending)
4490}
4491
4492fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
4493 let path = external_state_file(root, name);
4494 std::fs::rename(pending, &path)?;
4495 if let Ok(dir) = std::fs::File::open(external_state_dir(root, name)) {
4496 let _ = dir.sync_all();
4497 }
4498 Ok(())
4499}
4500
4501fn write_external_state_file(root: &Path, name: &str, state: &[u8]) -> Result<()> {
4502 let pending = prepare_external_state_file(root, name, state, 0)?;
4503 publish_external_state_file(root, name, &pending)
4504}
4505
4506fn recover_shared_wal(
4515 root: &Path,
4516 tables: &HashMap<u64, TableHandle>,
4517 epoch: &EpochAuthority,
4518 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
4519) -> Result<()> {
4520 use crate::memtable::Row;
4521 use crate::rowid::RowId;
4522 use crate::wal::{Op, SharedWal};
4523
4524 let records = SharedWal::replay_with_dek(root, wal_dek)?;
4525
4526 let mut committed: HashMap<u64, u64> = HashMap::new();
4528 let mut spilled_to_link: Vec<(
4529 u64, u64, Vec<crate::wal::AddedRun>,
4532 )> = Vec::new();
4533 for r in &records {
4534 if let Op::TxnCommit {
4535 epoch: ce,
4536 ref added_runs,
4537 } = r.op
4538 {
4539 committed.insert(r.txn_id, ce);
4540 if !added_runs.is_empty() {
4541 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
4542 }
4543 }
4544 }
4545
4546 type TableStage = (Vec<Row>, Vec<(RowId, Epoch)>, Option<Epoch>, Epoch);
4548 let mut stage: HashMap<u64, TableStage> = HashMap::new();
4549 let mut max_epoch = epoch.visible().0;
4550 for r in records {
4551 let Some(&ce) = committed.get(&r.txn_id) else {
4552 continue; };
4554 let commit_epoch = Epoch(ce);
4555 max_epoch = max_epoch.max(ce);
4556 match r.op {
4557 Op::Put { table_id, rows } => {
4558 let skip = tables
4560 .get(&table_id)
4561 .map(|h| h.lock().flushed_epoch() >= ce)
4562 .unwrap_or(true);
4563 if skip {
4564 continue;
4565 }
4566 let rows: Vec<Row> = match bincode::deserialize(&rows) {
4567 Ok(v) => v,
4568 Err(_) => continue,
4569 };
4570 let rows: Vec<Row> = rows
4573 .into_iter()
4574 .map(|mut row| {
4575 row.committed_epoch = commit_epoch;
4576 row
4577 })
4578 .collect();
4579 let entry = stage
4580 .entry(table_id)
4581 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
4582 entry.0.extend(rows);
4583 entry.3 = commit_epoch;
4584 }
4585 Op::Delete { table_id, row_ids } => {
4586 let skip = tables
4587 .get(&table_id)
4588 .map(|h| h.lock().flushed_epoch() >= ce)
4589 .unwrap_or(true);
4590 if skip {
4591 continue;
4592 }
4593 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
4594 let entry = stage
4595 .entry(table_id)
4596 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
4597 entry.1.extend(dels);
4598 entry.3 = commit_epoch;
4599 }
4600 Op::TruncateTable { table_id } => {
4601 let skip = tables
4602 .get(&table_id)
4603 .map(|h| h.lock().flushed_epoch() >= ce)
4604 .unwrap_or(true);
4605 if skip {
4606 continue;
4607 }
4608 stage.insert(
4609 table_id,
4610 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
4611 );
4612 }
4613 Op::ExternalTableState { name, state } => {
4614 write_external_state_file(root, &name, &state)?;
4615 }
4616 Op::Flush { .. } | Op::TxnCommit { .. } | Op::TxnAbort | Op::Ddl(_) => {}
4617 }
4618 }
4619 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
4620 let Some(handle) = tables.get(&table_id) else {
4621 continue;
4622 };
4623 let mut t = handle.lock();
4624 if let Some(epoch) = truncate_epoch {
4625 t.apply_truncate(epoch)?;
4626 }
4627 t.recover_apply(rows, deletes)?;
4628 if truncate_epoch.is_some() {
4629 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
4630 t.live_count = rows.len() as u64;
4631 t.persist_manifest(table_epoch)?;
4632 }
4633 }
4634
4635 for (txn_id, ce, added_runs) in &spilled_to_link {
4639 for ar in added_runs {
4640 let Some(handle) = tables.get(&ar.table_id) else {
4641 continue;
4642 };
4643 let mut t = handle.lock();
4644 let dest = t.run_path(ar.run_id as u64);
4645 if !dest.exists() {
4646 let pending = root
4647 .join(TABLES_DIR)
4648 .join(ar.table_id.to_string())
4649 .join("_txn")
4650 .join(txn_id.to_string())
4651 .join(format!("r-{}.sr", ar.run_id));
4652 if pending.exists() {
4653 if let Some(parent) = pending.parent() {
4654 std::fs::rename(&pending, &dest)?;
4655 let _ = std::fs::remove_dir_all(parent);
4656 }
4657 }
4658 }
4659 if t.run_path(ar.run_id as u64).exists() {
4665 t.recover_spilled_run(crate::manifest::RunRef {
4666 run_id: ar.run_id,
4667 level: ar.level,
4668 epoch_created: *ce,
4669 row_count: ar.row_count,
4670 });
4671 }
4672 }
4673 }
4674
4675 epoch.advance_recovered(Epoch(max_epoch));
4676 Ok(())
4677}
4678
4679fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
4680 match condition {
4681 ProcedureCondition::Pk { .. } => {
4682 if schema.primary_key().is_none() {
4683 return Err(MongrelError::InvalidArgument(
4684 "procedure condition Pk references a table without a primary key".into(),
4685 ));
4686 }
4687 }
4688 ProcedureCondition::BitmapEq { column_id, .. }
4689 | ProcedureCondition::BitmapIn { column_id, .. }
4690 | ProcedureCondition::Range { column_id, .. }
4691 | ProcedureCondition::RangeF64 { column_id, .. }
4692 | ProcedureCondition::IsNull { column_id }
4693 | ProcedureCondition::IsNotNull { column_id }
4694 | ProcedureCondition::FmContains { column_id, .. } => {
4695 validate_column_id(*column_id, schema)?;
4696 }
4697 }
4698 Ok(())
4699}
4700
4701fn bind_procedure_args(
4702 procedure: &StoredProcedure,
4703 mut args: HashMap<String, crate::Value>,
4704) -> Result<HashMap<String, crate::Value>> {
4705 let mut out = HashMap::new();
4706 for param in &procedure.params {
4707 let value = match args.remove(¶m.name) {
4708 Some(value) => value,
4709 None => param.default.clone().ok_or_else(|| {
4710 MongrelError::InvalidArgument(format!(
4711 "missing required procedure parameter {:?}",
4712 param.name
4713 ))
4714 })?,
4715 };
4716 if !param.nullable && matches!(value, crate::Value::Null) {
4717 return Err(MongrelError::InvalidArgument(format!(
4718 "procedure parameter {:?} must not be NULL",
4719 param.name
4720 )));
4721 }
4722 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty) {
4723 return Err(MongrelError::InvalidArgument(format!(
4724 "procedure parameter {:?} has wrong type",
4725 param.name
4726 )));
4727 }
4728 out.insert(param.name.clone(), value);
4729 }
4730 if let Some(extra) = args.keys().next() {
4731 return Err(MongrelError::InvalidArgument(format!(
4732 "unknown procedure parameter {extra:?}"
4733 )));
4734 }
4735 Ok(out)
4736}
4737
4738fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
4739 matches!(
4740 (value, ty),
4741 (crate::Value::Bool(_), crate::TypeId::Bool)
4742 | (crate::Value::Int64(_), crate::TypeId::Int8)
4743 | (crate::Value::Int64(_), crate::TypeId::Int16)
4744 | (crate::Value::Int64(_), crate::TypeId::Int32)
4745 | (crate::Value::Int64(_), crate::TypeId::Int64)
4746 | (crate::Value::Int64(_), crate::TypeId::UInt8)
4747 | (crate::Value::Int64(_), crate::TypeId::UInt16)
4748 | (crate::Value::Int64(_), crate::TypeId::UInt32)
4749 | (crate::Value::Int64(_), crate::TypeId::UInt64)
4750 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
4751 | (crate::Value::Int64(_), crate::TypeId::Date32)
4752 | (crate::Value::Float64(_), crate::TypeId::Float32)
4753 | (crate::Value::Float64(_), crate::TypeId::Float64)
4754 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
4755 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
4756 )
4757}
4758
4759fn eval_cells(
4760 cells: &[crate::procedure::ProcedureCell],
4761 args: &HashMap<String, crate::Value>,
4762 outputs: &HashMap<String, ProcedureCallOutput>,
4763) -> Result<Vec<(u16, crate::Value)>> {
4764 cells
4765 .iter()
4766 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
4767 .collect()
4768}
4769
4770fn eval_condition(
4771 condition: &ProcedureCondition,
4772 args: &HashMap<String, crate::Value>,
4773 outputs: &HashMap<String, ProcedureCallOutput>,
4774) -> Result<crate::Condition> {
4775 Ok(match condition {
4776 ProcedureCondition::Pk { value } => {
4777 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
4778 }
4779 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
4780 column_id: *column_id,
4781 value: eval_value(value, args, outputs)?.encode_key(),
4782 },
4783 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
4784 column_id: *column_id,
4785 values: values
4786 .iter()
4787 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
4788 .collect::<Result<Vec<_>>>()?,
4789 },
4790 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
4791 column_id: *column_id,
4792 lo: expect_i64(eval_value(lo, args, outputs)?)?,
4793 hi: expect_i64(eval_value(hi, args, outputs)?)?,
4794 },
4795 ProcedureCondition::RangeF64 {
4796 column_id,
4797 lo,
4798 lo_inclusive,
4799 hi,
4800 hi_inclusive,
4801 } => crate::Condition::RangeF64 {
4802 column_id: *column_id,
4803 lo: expect_f64(eval_value(lo, args, outputs)?)?,
4804 lo_inclusive: *lo_inclusive,
4805 hi: expect_f64(eval_value(hi, args, outputs)?)?,
4806 hi_inclusive: *hi_inclusive,
4807 },
4808 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
4809 column_id: *column_id,
4810 },
4811 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
4812 column_id: *column_id,
4813 },
4814 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
4815 column_id: *column_id,
4816 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
4817 },
4818 })
4819}
4820
4821fn eval_value(
4822 value: &ProcedureValue,
4823 args: &HashMap<String, crate::Value>,
4824 outputs: &HashMap<String, ProcedureCallOutput>,
4825) -> Result<crate::Value> {
4826 match value {
4827 ProcedureValue::Literal(value) => Ok(value.clone()),
4828 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
4829 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
4830 }),
4831 ProcedureValue::StepScalar(id) => match outputs.get(id) {
4832 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
4833 _ => Err(MongrelError::InvalidArgument(format!(
4834 "procedure step {id:?} did not return a scalar"
4835 ))),
4836 },
4837 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
4838 Err(MongrelError::InvalidArgument(
4839 "row-valued procedure reference cannot be used as a scalar".into(),
4840 ))
4841 }
4842 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
4843 "structured procedure value cannot be used as a scalar cell".into(),
4844 )),
4845 }
4846}
4847
4848fn eval_return_output(
4849 value: &ProcedureValue,
4850 args: &HashMap<String, crate::Value>,
4851 outputs: &HashMap<String, ProcedureCallOutput>,
4852) -> Result<ProcedureCallOutput> {
4853 match value {
4854 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
4855 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
4856 args.get(name).cloned().ok_or_else(|| {
4857 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
4858 })?,
4859 )),
4860 ProcedureValue::StepRows(id)
4861 | ProcedureValue::StepRow(id)
4862 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
4863 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
4864 }),
4865 ProcedureValue::Object(fields) => {
4866 let mut out = Vec::with_capacity(fields.len());
4867 for (name, value) in fields {
4868 out.push((name.clone(), eval_return_output(value, args, outputs)?));
4869 }
4870 Ok(ProcedureCallOutput::Object(out))
4871 }
4872 ProcedureValue::Array(values) => {
4873 let mut out = Vec::with_capacity(values.len());
4874 for value in values {
4875 out.push(eval_return_output(value, args, outputs)?);
4876 }
4877 Ok(ProcedureCallOutput::Array(out))
4878 }
4879 }
4880}
4881
4882fn expect_i64(value: crate::Value) -> Result<i64> {
4883 match value {
4884 crate::Value::Int64(value) => Ok(value),
4885 _ => Err(MongrelError::InvalidArgument(
4886 "procedure value must be Int64".into(),
4887 )),
4888 }
4889}
4890
4891fn expect_f64(value: crate::Value) -> Result<f64> {
4892 match value {
4893 crate::Value::Float64(value) => Ok(value),
4894 _ => Err(MongrelError::InvalidArgument(
4895 "procedure value must be Float64".into(),
4896 )),
4897 }
4898}
4899
4900fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
4901 match value {
4902 crate::Value::Bytes(value) => Ok(value),
4903 _ => Err(MongrelError::InvalidArgument(
4904 "procedure value must be Bytes".into(),
4905 )),
4906 }
4907}
4908
4909fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
4910 if schema.columns.iter().any(|c| c.id == column_id) {
4911 Ok(())
4912 } else {
4913 Err(MongrelError::InvalidArgument(format!(
4914 "unknown column id {column_id}"
4915 )))
4916 }
4917}
4918
4919fn trigger_matches_event(
4920 trigger: &StoredTrigger,
4921 event: &WriteEvent,
4922 cat: &Catalog,
4923) -> Result<bool> {
4924 if trigger.event != event.kind {
4925 return Ok(false);
4926 }
4927 let TriggerTarget::Table(target) = &trigger.target else {
4928 return Ok(false);
4929 };
4930 if target != &event.table {
4931 return Ok(false);
4932 }
4933 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
4934 let schema = &cat
4935 .live(target)
4936 .ok_or_else(|| {
4937 MongrelError::InvalidArgument(format!(
4938 "trigger {:?} references unknown table {target:?}",
4939 trigger.name
4940 ))
4941 })?
4942 .schema;
4943 let mut watched = Vec::with_capacity(trigger.update_of.len());
4944 for name in &trigger.update_of {
4945 let col = schema.column(name).ok_or_else(|| {
4946 MongrelError::InvalidArgument(format!(
4947 "trigger {:?} references unknown UPDATE OF column {name:?}",
4948 trigger.name
4949 ))
4950 })?;
4951 watched.push(col.id);
4952 }
4953 if !event
4954 .changed_columns
4955 .iter()
4956 .any(|column_id| watched.contains(column_id))
4957 {
4958 return Ok(false);
4959 }
4960 }
4961 Ok(true)
4962}
4963
4964fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
4965 let mut ids = std::collections::BTreeSet::new();
4966 if let Some(old) = old {
4967 ids.extend(old.columns.keys().copied());
4968 }
4969 if let Some(new) = new {
4970 ids.extend(new.columns.keys().copied());
4971 }
4972 ids.into_iter()
4973 .filter(|id| {
4974 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
4975 })
4976 .collect()
4977}
4978
4979fn eval_trigger_cells(
4980 cells: &[crate::trigger::TriggerCell],
4981 event: &WriteEvent,
4982 selected: Option<&TriggerRowImage>,
4983) -> Result<Vec<(u16, Value)>> {
4984 cells
4985 .iter()
4986 .map(|cell| {
4987 Ok((
4988 cell.column_id,
4989 eval_trigger_value(&cell.value, event, selected)?,
4990 ))
4991 })
4992 .collect()
4993}
4994
4995fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
4996 match expr {
4997 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
4998 Value::Bool(value) => Ok(value),
4999 Value::Null => Ok(false),
5000 other => Err(MongrelError::InvalidArgument(format!(
5001 "trigger WHEN value must be boolean, got {other:?}"
5002 ))),
5003 },
5004 TriggerExpr::Eq { left, right } => Ok(values_equal(
5005 &eval_trigger_value(left, event, None)?,
5006 &eval_trigger_value(right, event, None)?,
5007 )),
5008 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
5009 &eval_trigger_value(left, event, None)?,
5010 &eval_trigger_value(right, event, None)?,
5011 )),
5012 TriggerExpr::Lt { left, right } => match value_order(
5013 &eval_trigger_value(left, event, None)?,
5014 &eval_trigger_value(right, event, None)?,
5015 ) {
5016 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
5017 None => Ok(false),
5018 },
5019 TriggerExpr::Lte { left, right } => match value_order(
5020 &eval_trigger_value(left, event, None)?,
5021 &eval_trigger_value(right, event, None)?,
5022 ) {
5023 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
5024 None => Ok(false),
5025 },
5026 TriggerExpr::Gt { left, right } => match value_order(
5027 &eval_trigger_value(left, event, None)?,
5028 &eval_trigger_value(right, event, None)?,
5029 ) {
5030 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
5031 None => Ok(false),
5032 },
5033 TriggerExpr::Gte { left, right } => match value_order(
5034 &eval_trigger_value(left, event, None)?,
5035 &eval_trigger_value(right, event, None)?,
5036 ) {
5037 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
5038 None => Ok(false),
5039 },
5040 TriggerExpr::IsNull(value) => Ok(matches!(
5041 eval_trigger_value(value, event, None)?,
5042 Value::Null
5043 )),
5044 TriggerExpr::IsNotNull(value) => Ok(!matches!(
5045 eval_trigger_value(value, event, None)?,
5046 Value::Null
5047 )),
5048 TriggerExpr::And { left, right } => {
5049 if !eval_trigger_expr(left, event)? {
5050 Ok(false)
5051 } else {
5052 Ok(eval_trigger_expr(right, event)?)
5053 }
5054 }
5055 TriggerExpr::Or { left, right } => {
5056 if eval_trigger_expr(left, event)? {
5057 Ok(true)
5058 } else {
5059 Ok(eval_trigger_expr(right, event)?)
5060 }
5061 }
5062 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
5063 }
5064}
5065
5066fn eval_trigger_condition(
5067 condition: &TriggerCondition,
5068 event: &WriteEvent,
5069 selected: &TriggerRowImage,
5070 schema: &Schema,
5071) -> Result<bool> {
5072 match condition {
5073 TriggerCondition::Pk { value } => {
5074 let pk = schema.primary_key().ok_or_else(|| {
5075 MongrelError::InvalidArgument(
5076 "trigger condition Pk references a table without a primary key".into(),
5077 )
5078 })?;
5079 let lhs = eval_trigger_value(value, event, Some(selected))?;
5080 Ok(values_equal(
5081 &lhs,
5082 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
5083 ))
5084 }
5085 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
5086 selected.columns.get(column_id).unwrap_or(&Value::Null),
5087 &eval_trigger_value(value, event, Some(selected))?,
5088 )),
5089 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
5090 selected.columns.get(column_id).unwrap_or(&Value::Null),
5091 &eval_trigger_value(value, event, Some(selected))?,
5092 )),
5093 TriggerCondition::Lt { column_id, value } => match value_order(
5094 selected.columns.get(column_id).unwrap_or(&Value::Null),
5095 &eval_trigger_value(value, event, Some(selected))?,
5096 ) {
5097 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
5098 None => Ok(false),
5099 },
5100 TriggerCondition::Lte { column_id, value } => match value_order(
5101 selected.columns.get(column_id).unwrap_or(&Value::Null),
5102 &eval_trigger_value(value, event, Some(selected))?,
5103 ) {
5104 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
5105 None => Ok(false),
5106 },
5107 TriggerCondition::Gt { column_id, value } => match value_order(
5108 selected.columns.get(column_id).unwrap_or(&Value::Null),
5109 &eval_trigger_value(value, event, Some(selected))?,
5110 ) {
5111 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
5112 None => Ok(false),
5113 },
5114 TriggerCondition::Gte { column_id, value } => match value_order(
5115 selected.columns.get(column_id).unwrap_or(&Value::Null),
5116 &eval_trigger_value(value, event, Some(selected))?,
5117 ) {
5118 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
5119 None => Ok(false),
5120 },
5121 TriggerCondition::IsNull { column_id } => Ok(matches!(
5122 selected.columns.get(column_id),
5123 None | Some(Value::Null)
5124 )),
5125 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
5126 selected.columns.get(column_id),
5127 None | Some(Value::Null)
5128 )),
5129 TriggerCondition::And { left, right } => {
5130 if !eval_trigger_condition(left, event, selected, schema)? {
5131 Ok(false)
5132 } else {
5133 Ok(eval_trigger_condition(right, event, selected, schema)?)
5134 }
5135 }
5136 TriggerCondition::Or { left, right } => {
5137 if eval_trigger_condition(left, event, selected, schema)? {
5138 Ok(true)
5139 } else {
5140 Ok(eval_trigger_condition(right, event, selected, schema)?)
5141 }
5142 }
5143 TriggerCondition::Not(condition) => {
5144 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
5145 }
5146 }
5147}
5148
5149fn eval_trigger_value(
5150 value: &TriggerValue,
5151 event: &WriteEvent,
5152 selected: Option<&TriggerRowImage>,
5153) -> Result<Value> {
5154 match value {
5155 TriggerValue::Literal(value) => Ok(value.clone()),
5156 TriggerValue::NewColumn(column_id) => event
5157 .new
5158 .as_ref()
5159 .and_then(|row| row.columns.get(column_id))
5160 .cloned()
5161 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
5162 TriggerValue::OldColumn(column_id) => event
5163 .old
5164 .as_ref()
5165 .and_then(|row| row.columns.get(column_id))
5166 .cloned()
5167 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
5168 TriggerValue::SelectedColumn(column_id) => selected
5169 .and_then(|row| row.columns.get(column_id))
5170 .cloned()
5171 .ok_or_else(|| {
5172 MongrelError::InvalidArgument("SELECTED column is not available".into())
5173 }),
5174 }
5175}
5176
5177fn values_equal(left: &Value, right: &Value) -> bool {
5178 match (left, right) {
5179 (Value::Null, Value::Null) => true,
5180 (Value::Bool(a), Value::Bool(b)) => a == b,
5181 (Value::Int64(a), Value::Int64(b)) => a == b,
5182 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
5183 (Value::Bytes(a), Value::Bytes(b)) => a == b,
5184 (Value::Embedding(a), Value::Embedding(b)) => {
5185 a.len() == b.len()
5186 && a.iter()
5187 .zip(b.iter())
5188 .all(|(a, b)| a.to_bits() == b.to_bits())
5189 }
5190 _ => false,
5191 }
5192}
5193
5194fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
5195 match (left, right) {
5196 (Value::Null, _) | (_, Value::Null) => None,
5197 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
5198 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
5199 (Value::Int64(a), Value::Float64(b)) => {
5202 let af = *a as f64;
5203 Some(af.total_cmp(b))
5204 }
5205 (Value::Float64(a), Value::Int64(b)) => {
5208 let bf = *b as f64;
5209 Some(a.total_cmp(&bf))
5210 }
5211 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
5212 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
5213 (Value::Embedding(_), Value::Embedding(_)) => None,
5214 _ => None,
5215 }
5216}
5217
5218fn trigger_message(value: Value) -> String {
5219 match value {
5220 Value::Null => "NULL".into(),
5221 Value::Bool(value) => value.to_string(),
5222 Value::Int64(value) => value.to_string(),
5223 Value::Float64(value) => value.to_string(),
5224 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
5225 Value::Embedding(value) => format!("{value:?}"),
5226 Value::Decimal(value) => value.to_string(),
5227 Value::Interval {
5228 months,
5229 days,
5230 nanos,
5231 } => format!("{months}m {days}d {nanos}ns"),
5232 Value::Uuid(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
5233 Value::Json(b) => String::from_utf8_lossy(&b).into_owned(),
5234 }
5235}
5236
5237fn validate_trigger_step<'a>(
5238 step: &TriggerStep,
5239 cat: &'a Catalog,
5240 target_schema: &Schema,
5241 event: TriggerEvent,
5242 select_schemas: &mut HashMap<String, &'a Schema>,
5243) -> Result<()> {
5244 match step {
5245 TriggerStep::SetNew { cells } => {
5246 if event == TriggerEvent::Delete {
5247 return Err(MongrelError::InvalidArgument(
5248 "SetNew trigger step is not valid for DELETE triggers".into(),
5249 ));
5250 }
5251 for cell in cells {
5252 validate_column_id(cell.column_id, target_schema)?;
5253 validate_trigger_value(&cell.value, target_schema, event)?;
5254 }
5255 }
5256 TriggerStep::Insert { table, cells } => {
5257 let schema = trigger_write_schema(cat, table, "insert")?;
5258 for cell in cells {
5259 validate_column_id(cell.column_id, schema)?;
5260 validate_trigger_value(&cell.value, target_schema, event)?;
5261 }
5262 }
5263 TriggerStep::UpdateByPk { table, pk, cells } => {
5264 let schema = trigger_write_schema(cat, table, "update")?;
5265 if schema.primary_key().is_none() {
5266 return Err(MongrelError::InvalidArgument(format!(
5267 "trigger update_by_pk references table {table:?} without a primary key"
5268 )));
5269 }
5270 validate_trigger_value(pk, target_schema, event)?;
5271 for cell in cells {
5272 validate_column_id(cell.column_id, schema)?;
5273 validate_trigger_value(&cell.value, target_schema, event)?;
5274 }
5275 }
5276 TriggerStep::DeleteByPk { table, pk } => {
5277 let schema = trigger_write_schema(cat, table, "delete")?;
5278 if schema.primary_key().is_none() {
5279 return Err(MongrelError::InvalidArgument(format!(
5280 "trigger delete_by_pk references table {table:?} without a primary key"
5281 )));
5282 }
5283 validate_trigger_value(pk, target_schema, event)?;
5284 }
5285 TriggerStep::Select {
5286 id,
5287 table,
5288 conditions,
5289 } => {
5290 let schema = trigger_read_schema(cat, table)?;
5291 for condition in conditions {
5292 validate_trigger_condition(condition, schema, target_schema, event)?;
5293 }
5294 if select_schemas.contains_key(id) {
5295 return Err(MongrelError::InvalidArgument(format!(
5296 "duplicate select id {id:?} in trigger program"
5297 )));
5298 }
5299 select_schemas.insert(id.clone(), schema);
5300 }
5301 TriggerStep::Foreach { id, steps } => {
5302 if !select_schemas.contains_key(id) {
5303 return Err(MongrelError::InvalidArgument(format!(
5304 "foreach references unknown select id {id:?}"
5305 )));
5306 }
5307 let mut inner_select_schemas = select_schemas.clone();
5308 for step in steps {
5309 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
5310 }
5311 }
5312 TriggerStep::DeleteWhere { table, conditions } => {
5313 let schema = trigger_write_schema(cat, table, "delete")?;
5314 for condition in conditions {
5315 validate_trigger_condition(condition, schema, target_schema, event)?;
5316 }
5317 }
5318 TriggerStep::UpdateWhere {
5319 table,
5320 conditions,
5321 cells,
5322 } => {
5323 let schema = trigger_write_schema(cat, table, "update")?;
5324 for condition in conditions {
5325 validate_trigger_condition(condition, schema, target_schema, event)?;
5326 }
5327 for cell in cells {
5328 validate_column_id(cell.column_id, schema)?;
5329 validate_trigger_value(&cell.value, target_schema, event)?;
5330 }
5331 }
5332 TriggerStep::Raise { message, .. } => {
5333 validate_trigger_value(message, target_schema, event)?
5334 }
5335 }
5336 Ok(())
5337}
5338
5339fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
5340 if let Some(entry) = cat.live(table) {
5341 return Ok(&entry.schema);
5342 }
5343 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
5344 let allowed = match op {
5345 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
5346 "update" | "delete" => entry.capabilities.writable,
5347 _ => false,
5348 };
5349 if !allowed {
5350 return Err(MongrelError::InvalidArgument(format!(
5351 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
5352 entry.module
5353 )));
5354 }
5355 if !entry.capabilities.transaction_safe {
5356 return Err(MongrelError::InvalidArgument(format!(
5357 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
5358 entry.module
5359 )));
5360 }
5361 return Ok(&entry.declared_schema);
5362 }
5363 Err(MongrelError::InvalidArgument(format!(
5364 "trigger references unknown table {table:?}"
5365 )))
5366}
5367
5368fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
5369 if let Some(entry) = cat.live(table) {
5370 return Ok(&entry.schema);
5371 }
5372 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
5373 if entry.capabilities.trigger_safe {
5374 return Ok(&entry.declared_schema);
5375 }
5376 return Err(MongrelError::InvalidArgument(format!(
5377 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
5378 entry.module
5379 )));
5380 }
5381 Err(MongrelError::InvalidArgument(format!(
5382 "trigger references unknown table {table:?}"
5383 )))
5384}
5385
5386fn validate_trigger_condition(
5387 condition: &TriggerCondition,
5388 schema: &Schema,
5389 target_schema: &Schema,
5390 event: TriggerEvent,
5391) -> Result<()> {
5392 match condition {
5393 TriggerCondition::Pk { value } => {
5394 if schema.primary_key().is_none() {
5395 return Err(MongrelError::InvalidArgument(
5396 "trigger condition Pk references a table without a primary key".into(),
5397 ));
5398 }
5399 validate_trigger_value(value, target_schema, event)
5400 }
5401 TriggerCondition::Eq { column_id, value }
5402 | TriggerCondition::NotEq { column_id, value }
5403 | TriggerCondition::Lt { column_id, value }
5404 | TriggerCondition::Lte { column_id, value }
5405 | TriggerCondition::Gt { column_id, value }
5406 | TriggerCondition::Gte { column_id, value } => {
5407 validate_column_id(*column_id, schema)?;
5408 validate_trigger_value(value, target_schema, event)
5409 }
5410 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
5411 validate_column_id(*column_id, schema)
5412 }
5413 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
5414 validate_trigger_condition(left, schema, target_schema, event)?;
5415 validate_trigger_condition(right, schema, target_schema, event)
5416 }
5417 TriggerCondition::Not(condition) => {
5418 validate_trigger_condition(condition, schema, target_schema, event)
5419 }
5420 }
5421}
5422
5423fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
5424 match expr {
5425 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
5426 validate_trigger_value(value, schema, event)
5427 }
5428 TriggerExpr::Eq { left, right }
5429 | TriggerExpr::NotEq { left, right }
5430 | TriggerExpr::Lt { left, right }
5431 | TriggerExpr::Lte { left, right }
5432 | TriggerExpr::Gt { left, right }
5433 | TriggerExpr::Gte { left, right } => {
5434 validate_trigger_value(left, schema, event)?;
5435 validate_trigger_value(right, schema, event)
5436 }
5437 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
5438 validate_trigger_expr(left, schema, event)?;
5439 validate_trigger_expr(right, schema, event)
5440 }
5441 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
5442 }
5443}
5444
5445fn validate_trigger_value(
5446 value: &TriggerValue,
5447 schema: &Schema,
5448 event: TriggerEvent,
5449) -> Result<()> {
5450 match value {
5451 TriggerValue::Literal(_) => Ok(()),
5452 TriggerValue::NewColumn(id) => {
5453 if event == TriggerEvent::Delete {
5454 return Err(MongrelError::InvalidArgument(
5455 "DELETE triggers cannot reference NEW".into(),
5456 ));
5457 }
5458 validate_column_id(*id, schema)
5459 }
5460 TriggerValue::OldColumn(id) => {
5461 if event == TriggerEvent::Insert {
5462 return Err(MongrelError::InvalidArgument(
5463 "INSERT triggers cannot reference OLD".into(),
5464 ));
5465 }
5466 validate_column_id(*id, schema)
5467 }
5468 TriggerValue::SelectedColumn(_) => Ok(()),
5472 }
5473}
5474
5475fn recover_ddl_from_wal(
5481 root: &Path,
5482 cat: &mut Catalog,
5483 meta_dek: Option<&[u8; META_DEK_LEN]>,
5484 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
5485) -> Result<()> {
5486 use crate::wal::{DdlOp, Op, SharedWal};
5487
5488 let records = match SharedWal::replay_with_dek(root, wal_dek) {
5489 Ok(r) => r,
5490 Err(_) => return Ok(()),
5491 };
5492
5493 let mut committed: HashMap<u64, u64> = HashMap::new();
5494 for r in &records {
5495 if let Op::TxnCommit { epoch: ce, .. } = r.op {
5496 committed.insert(r.txn_id, ce);
5497 }
5498 }
5499
5500 let mut changed = false;
5501 for r in records {
5502 let Some(&ce) = committed.get(&r.txn_id) else {
5503 continue;
5504 };
5505 match r.op {
5506 Op::Ddl(DdlOp::CreateTable {
5507 table_id,
5508 ref name,
5509 ref schema_json,
5510 }) => {
5511 if cat.tables.iter().any(|t| t.table_id == table_id) {
5512 continue;
5513 }
5514 let schema = DdlOp::decode_schema(schema_json)?;
5515 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
5516 if !tdir.exists() {
5517 std::fs::create_dir_all(tdir.join(crate::engine::WAL_DIR))?;
5518 std::fs::create_dir_all(tdir.join(crate::engine::RUNS_DIR))?;
5519 crate::engine::write_schema(&tdir, &schema)?;
5520 let mut m = crate::manifest::Manifest::new(table_id, schema.schema_id);
5526 crate::manifest::write_atomic(&tdir, &mut m, meta_dek)?;
5527 }
5528 cat.tables.push(CatalogEntry {
5529 table_id,
5530 name: name.clone(),
5531 schema,
5532 state: TableState::Live,
5533 created_epoch: ce,
5534 });
5535 cat.next_table_id = cat.next_table_id.max(table_id + 1);
5536 changed = true;
5537 }
5538 Op::Ddl(DdlOp::DropTable { table_id }) => {
5539 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
5540 if matches!(entry.state, TableState::Live) {
5541 entry.state = TableState::Dropped { at_epoch: ce };
5542 changed = true;
5543 }
5544 }
5545 }
5546 Op::Ddl(DdlOp::RenameTable {
5547 table_id,
5548 ref new_name,
5549 }) => {
5550 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
5551 if entry.name != *new_name {
5552 entry.name = new_name.clone();
5553 changed = true;
5554 }
5555 }
5556 }
5560 Op::Ddl(DdlOp::AlterTable {
5561 table_id,
5562 ref column_json,
5563 }) => {
5564 let column = DdlOp::decode_column(column_json)?;
5565 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
5566 if apply_recovered_column_def(&mut entry.schema, column) {
5567 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
5568 if tdir.exists() {
5569 crate::engine::write_schema(&tdir, &entry.schema)?;
5570 }
5571 changed = true;
5572 }
5573 }
5574 }
5575 _ => {}
5576 }
5577 }
5578
5579 if changed {
5580 catalog::write_atomic(root, cat, meta_dek)?;
5581 }
5582 Ok(())
5583}
5584
5585fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> bool {
5586 match schema.columns.iter_mut().find(|c| c.id == column.id) {
5587 Some(existing) if *existing == column => false,
5588 Some(existing) => {
5589 *existing = column;
5590 schema.schema_id = schema.schema_id.saturating_add(1);
5591 true
5592 }
5593 None => {
5594 schema.columns.push(column);
5595 schema.schema_id = schema.schema_id.saturating_add(1);
5596 true
5597 }
5598 }
5599}
5600
5601fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
5606 for entry in &cat.tables {
5607 let txn_dir = root
5608 .join(TABLES_DIR)
5609 .join(entry.table_id.to_string())
5610 .join("_txn");
5611 if txn_dir.exists() {
5612 let _ = std::fs::remove_dir_all(&txn_dir);
5613 }
5614 }
5615}
5616
5617#[cfg(test)]
5618mod trigger_engine_tests {
5619 use super::*;
5620
5621 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
5622 WriteEvent {
5623 table: "test".into(),
5624 kind: TriggerEvent::Insert,
5625 new: Some(TriggerRowImage {
5626 columns: new_cells.iter().cloned().collect(),
5627 }),
5628 old: Some(TriggerRowImage {
5629 columns: old_cells.iter().cloned().collect(),
5630 }),
5631 changed_columns: Vec::new(),
5632 op_indices: Vec::new(),
5633 put_idx: None,
5634 trigger_stack: Vec::new(),
5635 }
5636 }
5637
5638 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
5639 WriteEvent {
5640 table: "test".into(),
5641 kind: TriggerEvent::Insert,
5642 new: Some(TriggerRowImage {
5643 columns: new_cells.iter().cloned().collect(),
5644 }),
5645 old: None,
5646 changed_columns: Vec::new(),
5647 op_indices: Vec::new(),
5648 put_idx: None,
5649 trigger_stack: Vec::new(),
5650 }
5651 }
5652
5653 #[test]
5654 fn value_order_int64_vs_float64() {
5655 assert_eq!(
5656 value_order(&Value::Int64(5), &Value::Float64(5.0)),
5657 Some(std::cmp::Ordering::Equal)
5658 );
5659 assert_eq!(
5660 value_order(&Value::Int64(5), &Value::Float64(3.0)),
5661 Some(std::cmp::Ordering::Greater)
5662 );
5663 assert_eq!(
5664 value_order(&Value::Int64(2), &Value::Float64(3.0)),
5665 Some(std::cmp::Ordering::Less)
5666 );
5667 }
5668
5669 #[test]
5670 fn value_order_null_returns_none() {
5671 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
5672 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
5673 assert_eq!(value_order(&Value::Null, &Value::Null), None);
5674 }
5675
5676 #[test]
5677 fn value_order_cross_group_returns_none() {
5678 assert_eq!(
5679 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
5680 None
5681 );
5682 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
5683 assert_eq!(
5684 value_order(
5685 &Value::Embedding(vec![1.0, 2.0]),
5686 &Value::Embedding(vec![1.0, 2.0])
5687 ),
5688 None
5689 );
5690 }
5691
5692 #[test]
5693 fn eval_trigger_expr_ranges_and_booleans() {
5694 let expr = TriggerExpr::And {
5695 left: Box::new(TriggerExpr::Gt {
5696 left: TriggerValue::NewColumn(1),
5697 right: TriggerValue::Literal(Value::Int64(0)),
5698 }),
5699 right: Box::new(TriggerExpr::Lte {
5700 left: TriggerValue::NewColumn(1),
5701 right: TriggerValue::Literal(Value::Int64(100)),
5702 }),
5703 };
5704 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
5705 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
5706 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
5707
5708 let or_expr = TriggerExpr::Or {
5709 left: Box::new(TriggerExpr::Lt {
5710 left: TriggerValue::NewColumn(1),
5711 right: TriggerValue::Literal(Value::Int64(0)),
5712 }),
5713 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
5714 TriggerValue::OldColumn(2),
5715 )))),
5716 };
5717 assert!(eval_trigger_expr(
5718 &or_expr,
5719 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
5720 )
5721 .unwrap());
5722 assert!(!eval_trigger_expr(
5723 &or_expr,
5724 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
5725 )
5726 .unwrap());
5727
5728 assert!(eval_trigger_expr(
5729 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
5730 &event_insert(&[])
5731 )
5732 .unwrap());
5733 assert!(!eval_trigger_expr(
5734 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
5735 &event_insert(&[])
5736 )
5737 .unwrap());
5738 assert!(!eval_trigger_expr(
5739 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
5740 &event_insert(&[])
5741 )
5742 .unwrap());
5743 }
5744}