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