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