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