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