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, TypeId};
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, HashSet, VecDeque};
28use std::io::Write;
29use std::path::{Path, PathBuf};
30use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize};
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";
37pub const HISTORY_RETENTION_FILENAME: &str = "history_retention";
38
39pub const WAL_TABLE_ID: u64 = u64::MAX;
43pub const EXTERNAL_TABLE_ID: u64 = u64::MAX - 1;
46
47fn current_unix_nanos() -> u64 {
48 std::time::SystemTime::now()
49 .duration_since(std::time::UNIX_EPOCH)
50 .unwrap_or_default()
51 .as_nanos() as u64
52}
53
54fn read_history_retention(root: &Path, current_epoch: Epoch) -> Result<(u64, Epoch)> {
55 let path = root.join(META_DIR).join(HISTORY_RETENTION_FILENAME);
56 let text = match std::fs::read_to_string(path) {
57 Ok(text) => text,
58 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
59 return Ok((0, current_epoch));
60 }
61 Err(error) => return Err(error.into()),
62 };
63 let mut fields = text.split_whitespace();
64 let epochs = fields
65 .next()
66 .ok_or_else(|| MongrelError::Other("history retention file is empty".into()))?
67 .parse::<u64>()
68 .map_err(|error| MongrelError::Other(format!("history retention epochs: {error}")))?;
69 let start = fields
70 .next()
71 .unwrap_or("0")
72 .parse::<u64>()
73 .map_err(|error| MongrelError::Other(format!("history retention start: {error}")))?;
74 Ok((epochs, Epoch(start)))
75}
76
77fn write_history_retention(root: &Path, epochs: u64, start: Epoch) -> Result<()> {
78 let meta = root.join(META_DIR);
79 std::fs::create_dir_all(&meta)?;
80 let path = meta.join(HISTORY_RETENTION_FILENAME);
81 let tmp = meta.join(format!("{HISTORY_RETENTION_FILENAME}.tmp"));
82 {
83 let mut file = std::fs::File::create(&tmp)?;
84 writeln!(file, "{epochs} {}", start.0)?;
85 file.sync_all()?;
86 }
87 std::fs::rename(tmp, path)?;
88 if let Ok(dir) = std::fs::File::open(meta) {
89 let _ = dir.sync_all();
90 }
91 Ok(())
92}
93
94fn prepare_backup_destination(
95 source: &Path,
96 destination: &Path,
97) -> Result<(PathBuf, PathBuf, PathBuf)> {
98 let source = source.canonicalize()?;
99 if destination.exists() {
100 return Err(MongrelError::Conflict(format!(
101 "backup destination already exists: {}",
102 destination.display()
103 )));
104 }
105 let name = destination
106 .file_name()
107 .ok_or_else(|| MongrelError::InvalidArgument("invalid backup destination".into()))?;
108 let requested_parent = destination
109 .parent()
110 .filter(|path| !path.as_os_str().is_empty())
111 .unwrap_or_else(|| Path::new("."));
112 std::fs::create_dir_all(requested_parent)?;
113 let parent = requested_parent.canonicalize()?;
114 if parent.starts_with(&source) {
115 return Err(MongrelError::InvalidArgument(
116 "backup destination must not be inside the source database".into(),
117 ));
118 }
119 let destination = parent.join(name);
120 let nonce = std::time::SystemTime::now()
121 .duration_since(std::time::UNIX_EPOCH)
122 .unwrap_or_default()
123 .as_nanos();
124 let stage = parent.join(format!(
125 ".{}.backup-stage-{}-{nonce}",
126 name.to_string_lossy(),
127 std::process::id()
128 ));
129 if stage.exists() {
130 return Err(MongrelError::Conflict(format!(
131 "backup staging path already exists: {}",
132 stage.display()
133 )));
134 }
135 Ok((destination, parent, stage))
136}
137
138fn copy_backup_boundary(
139 source_root: &Path,
140 destination_root: &Path,
141 deferred_runs: &HashSet<PathBuf>,
142 copied: &mut Vec<PathBuf>,
143) -> Result<()> {
144 fn visit(
145 source_root: &Path,
146 source: &Path,
147 destination_root: &Path,
148 deferred_runs: &HashSet<PathBuf>,
149 copied: &mut Vec<PathBuf>,
150 ) -> Result<()> {
151 let mut entries = std::fs::read_dir(source)?.collect::<std::io::Result<Vec<_>>>()?;
152 entries.sort_by_key(std::fs::DirEntry::file_name);
153 for entry in entries {
154 let path = entry.path();
155 let relative = path
156 .strip_prefix(source_root)
157 .map_err(|error| MongrelError::Other(format!("backup path: {error}")))?;
158 if backup_path_excluded(relative) {
159 continue;
160 }
161 let file_type = entry.file_type()?;
162 if file_type.is_symlink() {
163 return Err(MongrelError::InvalidArgument(format!(
164 "backup refuses symlink {}",
165 path.display()
166 )));
167 }
168 if file_type.is_dir() {
169 std::fs::create_dir_all(destination_root.join(relative))?;
170 visit(source_root, &path, destination_root, deferred_runs, copied)?;
171 } else if file_type.is_file() {
172 if deferred_runs.contains(relative) {
173 continue;
174 }
175 if relative
176 .parent()
177 .and_then(Path::file_name)
178 .is_some_and(|parent| parent == "_runs")
179 && relative
180 .extension()
181 .is_some_and(|extension| extension == "sr")
182 {
183 continue;
184 }
185 crate::backup::copy_file_synced(&path, &destination_root.join(relative))?;
186 copied.push(relative.to_path_buf());
187 }
188 }
189 Ok(())
190 }
191
192 std::fs::create_dir_all(destination_root)?;
193 visit(
194 source_root,
195 source_root,
196 destination_root,
197 deferred_runs,
198 copied,
199 )
200}
201
202fn backup_path_excluded(relative: &Path) -> bool {
203 relative == Path::new("_meta/.lock")
204 || relative == Path::new("_meta/replica")
205 || relative == Path::new("_meta/repl_epoch")
206 || relative == Path::new(crate::backup::BACKUP_MANIFEST_PATH)
207 || relative.components().any(|component| {
208 matches!(component, std::path::Component::Normal(name) if name == "_cache" || name == "_txn" || name == "backup-pins")
209 })
210}
211
212#[derive(Debug, Clone)]
213pub enum ExternalTriggerWrite {
214 Insert {
215 table: String,
216 cells: Vec<(u16, Value)>,
217 },
218 UpdateByPk {
219 table: String,
220 pk: Value,
221 cells: Vec<(u16, Value)>,
222 },
223 DeleteByPk {
224 table: String,
225 pk: Value,
226 },
227}
228
229impl ExternalTriggerWrite {
230 fn table(&self) -> &str {
231 match self {
232 Self::Insert { table, .. }
233 | Self::UpdateByPk { table, .. }
234 | Self::DeleteByPk { table, .. } => table,
235 }
236 }
237}
238
239#[derive(Debug, Clone, PartialEq)]
240pub enum ExternalTriggerBaseWrite {
241 Put {
242 table: String,
243 cells: Vec<(u16, Value)>,
244 },
245 Delete {
246 table: String,
247 row_id: RowId,
248 },
249}
250
251#[derive(Debug, Clone, PartialEq)]
252pub struct ExternalTriggerWriteResult {
253 pub state: Vec<u8>,
254 pub base_writes: Vec<ExternalTriggerBaseWrite>,
255}
256
257impl ExternalTriggerWriteResult {
258 pub fn new(state: Vec<u8>) -> Self {
259 Self {
260 state,
261 base_writes: Vec::new(),
262 }
263 }
264}
265
266pub trait ExternalTriggerBridge {
267 fn apply_trigger_external_write(
268 &self,
269 entry: &ExternalTableEntry,
270 base_state: Vec<u8>,
271 op: ExternalTriggerWrite,
272 ) -> Result<ExternalTriggerWriteResult>;
273}
274
275struct SpilledRun {
277 table_id: u64,
278 run_id: u128,
279 pending_path: PathBuf,
280 rows: Vec<crate::memtable::Row>,
281 row_count: u64,
282 min_rid: u64,
283 max_rid: u64,
284}
285
286#[derive(Debug, Clone)]
287struct TriggerRowImage {
288 columns: HashMap<u16, Value>,
289}
290
291impl TriggerRowImage {
292 fn from_row(row: crate::memtable::Row) -> Self {
293 Self {
294 columns: row.columns,
295 }
296 }
297
298 fn from_cells(cells: &[(u16, Value)]) -> Self {
299 Self {
300 columns: cells.iter().cloned().collect(),
301 }
302 }
303}
304
305#[derive(Debug, Clone)]
306struct WriteEvent {
307 table: String,
308 kind: TriggerEvent,
309 old: Option<TriggerRowImage>,
310 new: Option<TriggerRowImage>,
311 changed_columns: Vec<u16>,
312 op_indices: Vec<usize>,
313 put_idx: Option<usize>,
314 trigger_stack: Vec<String>,
315}
316
317#[derive(Default)]
318struct TriggerExpansion {
319 before: Vec<(u64, crate::txn::Staged)>,
320 before_stacks: Vec<Vec<String>>,
321 before_external: Vec<ExternalTriggerWrite>,
322 after: Vec<(u64, crate::txn::Staged)>,
323 after_stacks: Vec<Vec<String>>,
324 after_external: Vec<ExternalTriggerWrite>,
325 ignored_indices: std::collections::BTreeSet<usize>,
326}
327
328struct TriggerProgramOutput<'a> {
329 added: &'a mut Vec<(u64, crate::txn::Staged)>,
330 added_stacks: &'a mut Vec<Vec<String>>,
331 added_external: &'a mut Vec<ExternalTriggerWrite>,
332 ignored_indices: &'a mut std::collections::BTreeSet<usize>,
333}
334
335#[derive(Debug, Clone, Copy, PartialEq, Eq)]
336enum TriggerProgramOutcome {
337 Continue,
338 Ignore,
339}
340
341#[derive(Debug, Clone)]
343pub struct CheckIssue {
344 pub table_id: u64,
345 pub table_name: String,
346 pub severity: String,
347 pub description: String,
348}
349
350#[derive(Debug, Clone)]
352pub struct AuthorizedReadSnapshot {
353 pub table: String,
354 pub table_snapshot: Snapshot,
355 pub data_generation: u64,
356 pub security_version: u64,
357 pub allowed_row_ids: Option<HashSet<RowId>>,
358}
359
360type RlsCacheKey = (String, u64, u64, String);
361
362#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
364pub struct RlsCacheStats {
365 pub entries: usize,
366 pub bytes: usize,
367 pub hits: u64,
368 pub misses: u64,
369 pub evictions: u64,
370 pub build_nanos: u64,
371 pub rows_evaluated: u64,
372}
373
374const RLS_CACHE_MAX_BYTES: usize = 64 * 1024 * 1024;
375
376#[derive(Default)]
377struct RlsCache {
378 entries: HashMap<RlsCacheKey, (Arc<HashSet<RowId>>, usize)>,
379 lru: VecDeque<RlsCacheKey>,
380 bytes: usize,
381 hits: u64,
382 misses: u64,
383 evictions: u64,
384 build_nanos: u64,
385 rows_evaluated: u64,
386}
387
388impl RlsCache {
389 fn get(&mut self, key: &RlsCacheKey) -> Option<Arc<HashSet<RowId>>> {
390 let value = self.entries.get(key).map(|(value, _)| Arc::clone(value));
391 if value.is_some() {
392 self.hits = self.hits.saturating_add(1);
393 self.touch(key);
394 } else {
395 self.misses = self.misses.saturating_add(1);
396 }
397 value
398 }
399
400 fn insert(&mut self, key: RlsCacheKey, value: Arc<HashSet<RowId>>) {
401 let bytes = key
402 .0
403 .len()
404 .saturating_add(key.3.len())
405 .saturating_add(
406 value
407 .capacity()
408 .saturating_mul(std::mem::size_of::<RowId>() * 3),
409 )
410 .saturating_add(std::mem::size_of::<RlsCacheKey>());
411 if bytes > RLS_CACHE_MAX_BYTES {
412 return;
413 }
414 if let Some((_, old_bytes)) = self.entries.remove(&key) {
415 self.bytes = self.bytes.saturating_sub(old_bytes);
416 }
417 self.lru.retain(|candidate| candidate != &key);
418 while self.bytes.saturating_add(bytes) > RLS_CACHE_MAX_BYTES {
419 let Some(oldest) = self.lru.pop_front() else {
420 break;
421 };
422 if let Some((_, old_bytes)) = self.entries.remove(&oldest) {
423 self.bytes = self.bytes.saturating_sub(old_bytes);
424 self.evictions = self.evictions.saturating_add(1);
425 }
426 }
427 self.bytes = self.bytes.saturating_add(bytes);
428 self.lru.push_back(key.clone());
429 self.entries.insert(key, (value, bytes));
430 }
431
432 fn touch(&mut self, key: &RlsCacheKey) {
433 self.lru.retain(|candidate| candidate != key);
434 self.lru.push_back(key.clone());
435 }
436
437 fn stats(&self) -> RlsCacheStats {
438 RlsCacheStats {
439 entries: self.entries.len(),
440 bytes: self.bytes,
441 hits: self.hits,
442 misses: self.misses,
443 evictions: self.evictions,
444 build_nanos: self.build_nanos,
445 rows_evaluated: self.rows_evaluated,
446 }
447 }
448}
449
450pub type TableHandle = Arc<Mutex<Table>>;
454
455#[derive(Clone, Debug, Default)]
461pub struct OpenOptions {
462 pub lock_timeout_ms: u32,
477}
478
479impl OpenOptions {
480 pub fn with_lock_timeout_ms(mut self, ms: u32) -> Self {
483 self.lock_timeout_ms = ms;
484 self
485 }
486}
487
488pub struct Database {
491 root: PathBuf,
492 read_only: bool,
494 catalog: RwLock<Catalog>,
495 rls_cache: Mutex<RlsCache>,
496 epoch: Arc<EpochAuthority>,
497 snapshots: Arc<SnapshotRegistry>,
498 page_cache: Arc<crate::cache::Sharded<crate::cache::PageCache>>,
499 decoded_cache: Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>,
500 commit_lock: Arc<Mutex<()>>,
501 shared_wal: Arc<Mutex<crate::wal::SharedWal>>,
506 next_txn_id: Arc<Mutex<u64>>,
510 tables: RwLock<HashMap<u64, TableHandle>>,
511 kek: Option<Arc<crate::encryption::Kek>>,
512 ddl_lock: Mutex<()>,
515 meta_dek: Option<[u8; META_DEK_LEN]>,
516 spill_threshold: std::sync::atomic::AtomicU64,
519 conflicts: crate::txn::ConflictIndex,
522 active_txns: crate::txn::ActiveTxns,
525 poisoned: Arc<std::sync::atomic::AtomicBool>,
528 group: Arc<crate::txn::GroupCommit>,
532 active_spills: Arc<crate::retention::ActiveSpills>,
535 replication_barrier: parking_lot::RwLock<()>,
538 replication_wal_retention_segments: AtomicUsize,
540 backup_pins: Mutex<HashMap<(u64, u128), usize>>,
544 #[doc(hidden)]
548 spill_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
549 #[doc(hidden)]
552 backup_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
553 trigger_recursive: AtomicBool,
554 trigger_max_depth: AtomicU32,
555 trigger_max_loop_iterations: AtomicU32,
556 _lock: Option<std::fs::File>,
559 notify: tokio::sync::broadcast::Sender<ChangeEvent>,
562 change_wake: tokio::sync::broadcast::Sender<()>,
565 principal: RwLock<Option<crate::auth::Principal>>,
575 auth_state: crate::auth_state::AuthState,
581}
582
583struct EpochGuard<'a> {
596 authority: &'a EpochAuthority,
597 epoch: Epoch,
598 armed: bool,
599}
600
601struct BackupRunPins<'a> {
602 pins: &'a Mutex<HashMap<(u64, u128), usize>>,
603 runs: Vec<(u64, u128)>,
604}
605
606struct BackupFilePins {
607 root: PathBuf,
608}
609
610impl Drop for BackupFilePins {
611 fn drop(&mut self) {
612 let _ = std::fs::remove_dir_all(&self.root);
613 }
614}
615
616impl Drop for BackupRunPins<'_> {
617 fn drop(&mut self) {
618 let mut pins = self.pins.lock();
619 for run in &self.runs {
620 if let Some(count) = pins.get_mut(run) {
621 *count -= 1;
622 if *count == 0 {
623 pins.remove(run);
624 }
625 }
626 }
627 }
628}
629
630impl<'a> EpochGuard<'a> {
631 fn new(authority: &'a EpochAuthority, epoch: Epoch) -> Self {
632 Self {
633 authority,
634 epoch,
635 armed: true,
636 }
637 }
638
639 fn disarm(&mut self) {
642 self.armed = false;
643 }
644}
645
646impl Drop for EpochGuard<'_> {
647 fn drop(&mut self) {
648 if self.armed {
649 self.authority.abandon(self.epoch);
650 }
651 }
652}
653
654#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
657pub struct ChangeEvent {
658 pub id: Option<String>,
659 pub channel: String,
660 pub table_id: Option<u64>,
661 pub table: String,
662 pub op: String,
663 pub epoch: u64,
664 pub txn_id: Option<u64>,
665 pub message: Option<String>,
666 pub data: Option<serde_json::Value>,
667}
668
669#[derive(Debug, Clone)]
670pub struct CdcBatch {
671 pub events: Vec<ChangeEvent>,
672 pub current_epoch: u64,
673 pub earliest_epoch: Option<u64>,
674 pub gap: bool,
675}
676
677impl std::fmt::Debug for Database {
684 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
685 let cat = self.catalog.read();
686 let principal_guard = self.principal.read();
687 let principal: &str = principal_guard
688 .as_ref()
689 .map(|p| p.username.as_str())
690 .unwrap_or("<none>");
691 f.debug_struct("Database")
692 .field("root", &self.root)
693 .field("db_epoch", &cat.db_epoch)
694 .field("open_generation", &"sidecar")
695 .field("tables", &cat.tables.len())
696 .field("visible_epoch", &self.epoch.visible().0)
697 .field("encrypted", &self.kek.is_some())
698 .field("require_auth", &cat.require_auth)
699 .field("principal", &principal)
700 .finish()
701 }
702}
703
704impl Database {
705 pub fn create(root: impl AsRef<Path>) -> Result<Self> {
707 Self::create_inner(root, None)
708 }
709
710 #[cfg(feature = "encryption")]
713 pub fn create_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
714 let root = root.as_ref();
715 Self::reject_existing_database(root)?;
716 std::fs::create_dir_all(root)?;
717 std::fs::create_dir_all(root.join(META_DIR))?;
718 let salt = crate::encryption::random_salt();
719 std::fs::write(root.join(META_DIR).join(KEYS_FILENAME), salt)?;
720 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
721 Self::create_inner(root, Some(kek))
722 }
723
724 #[cfg(feature = "encryption")]
727 pub fn create_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
728 let root = root.as_ref();
729 Self::reject_existing_database(root)?;
730 std::fs::create_dir_all(root)?;
731 std::fs::create_dir_all(root.join(META_DIR))?;
732 let salt = crate::encryption::random_salt();
733 std::fs::write(root.join(META_DIR).join(KEYS_FILENAME), salt)?;
734 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
735 Self::create_inner(root, Some(kek))
736 }
737
738 fn create_inner(
739 root: impl AsRef<Path>,
740 kek: Option<Arc<crate::encryption::Kek>>,
741 ) -> Result<Self> {
742 let root = root.as_ref().to_path_buf();
743 Self::reject_existing_database(&root)?;
744 std::fs::create_dir_all(&root)?;
745 std::fs::create_dir_all(root.join(TABLES_DIR))?;
746 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
747 let cat = Catalog::empty();
748 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
749 Self::finish_open(root, cat, kek, meta_dek, false, None, 0)
750 }
751
752 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
754 Self::open_inner(root, None, None)
755 }
756
757 #[cfg(feature = "encryption")]
759 pub fn open_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
760 let root = root.as_ref();
761 let salt_bytes = std::fs::read(root.join(META_DIR).join(KEYS_FILENAME))
762 .map_err(|e| MongrelError::NotFound(format!("encryption salt file: {e}")))?;
763 let mut salt = [0u8; crate::encryption::SALT_LEN];
764 salt.copy_from_slice(&salt_bytes);
765 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
766 Self::open_inner(root, Some(kek), None)
767 }
768
769 #[cfg(feature = "encryption")]
772 pub fn open_encrypted_with_options(
773 root: impl AsRef<Path>,
774 passphrase: &str,
775 options: OpenOptions,
776 ) -> Result<Self> {
777 let root = root.as_ref();
778 let salt_bytes = std::fs::read(root.join(META_DIR).join(KEYS_FILENAME))
779 .map_err(|e| MongrelError::NotFound(format!("encryption salt file: {e}")))?;
780 let mut salt = [0u8; crate::encryption::SALT_LEN];
781 salt.copy_from_slice(&salt_bytes);
782 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
783 Self::open_inner_with_lock_timeout(root, Some(kek), None, options.lock_timeout_ms)
784 }
785
786 #[cfg(feature = "encryption")]
788 pub fn open_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
789 let root = root.as_ref();
790 let salt_path = root.join(META_DIR).join(KEYS_FILENAME);
791 let salt_bytes = std::fs::read(&salt_path).map_err(|e| {
792 MongrelError::NotFound(format!(
793 "encryption salt file {:?}: {e} (database not encrypted, or corrupted)",
794 salt_path
795 ))
796 })?;
797 if salt_bytes.len() != crate::encryption::SALT_LEN {
798 return Err(MongrelError::InvalidArgument(format!(
799 "salt file is {} bytes, expected {}",
800 salt_bytes.len(),
801 crate::encryption::SALT_LEN
802 )));
803 }
804 let mut salt = [0u8; crate::encryption::SALT_LEN];
805 salt.copy_from_slice(&salt_bytes);
806 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
807 Self::open_inner(root, Some(kek), None)
808 }
809
810 pub fn open_with_credentials(
822 root: impl AsRef<Path>,
823 username: &str,
824 password: &str,
825 ) -> Result<Self> {
826 Self::open_inner_with_credentials(root, None, username, password)
827 }
828
829 pub fn open_with_credentials_and_options(
833 root: impl AsRef<Path>,
834 username: &str,
835 password: &str,
836 options: OpenOptions,
837 ) -> Result<Self> {
838 Self::open_inner_with_credentials_and_lock_timeout(
839 root,
840 None,
841 username,
842 password,
843 options.lock_timeout_ms,
844 )
845 }
846
847 #[cfg(feature = "encryption")]
850 pub fn open_encrypted_with_credentials(
851 root: impl AsRef<Path>,
852 passphrase: &str,
853 username: &str,
854 password: &str,
855 ) -> Result<Self> {
856 let root = root.as_ref();
857 let salt_bytes = std::fs::read(root.join(META_DIR).join(KEYS_FILENAME))
858 .map_err(|e| MongrelError::NotFound(format!("encryption salt file: {e}")))?;
859 let mut salt = [0u8; crate::encryption::SALT_LEN];
860 salt.copy_from_slice(&salt_bytes);
861 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
862 Self::open_inner_with_credentials(root, Some(kek), username, password)
863 }
864
865 #[cfg(feature = "encryption")]
869 pub fn open_encrypted_with_credentials_and_options(
870 root: impl AsRef<Path>,
871 passphrase: &str,
872 username: &str,
873 password: &str,
874 options: OpenOptions,
875 ) -> Result<Self> {
876 let root = root.as_ref();
877 let salt_bytes = std::fs::read(root.join(META_DIR).join(KEYS_FILENAME))
878 .map_err(|e| MongrelError::NotFound(format!("encryption salt file: {e}")))?;
879 let mut salt = [0u8; crate::encryption::SALT_LEN];
880 salt.copy_from_slice(&salt_bytes);
881 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
882 Self::open_inner_with_credentials_and_lock_timeout(
883 root,
884 Some(kek),
885 username,
886 password,
887 options.lock_timeout_ms,
888 )
889 }
890
891 pub fn open_with_options(root: impl AsRef<Path>, options: OpenOptions) -> Result<Self> {
898 Self::open_inner_with_lock_timeout(root, None, None, options.lock_timeout_ms)
901 }
902
903 fn open_inner_with_lock_timeout(
904 root: impl AsRef<Path>,
905 kek: Option<Arc<crate::encryption::Kek>>,
906 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
907 lock_timeout_ms: u32,
908 ) -> Result<Self> {
909 let root = root.as_ref().to_path_buf();
910 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
911 let cat = catalog::read(&root, meta_dek.as_ref())?
912 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
913 Self::finish_open(root, cat, kek, meta_dek, true, None, lock_timeout_ms)
914 }
915
916 fn open_inner_with_credentials(
923 root: impl AsRef<Path>,
924 kek: Option<Arc<crate::encryption::Kek>>,
925 username: &str,
926 password: &str,
927 ) -> Result<Self> {
928 Self::open_inner_with_credentials_and_lock_timeout(root, kek, username, password, 0)
929 }
930
931 fn open_inner_with_credentials_and_lock_timeout(
935 root: impl AsRef<Path>,
936 kek: Option<Arc<crate::encryption::Kek>>,
937 username: &str,
938 password: &str,
939 lock_timeout_ms: u32,
940 ) -> Result<Self> {
941 let root = root.as_ref().to_path_buf();
942 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
943 let cat = catalog::read(&root, meta_dek.as_ref())?
944 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
945
946 if !cat.require_auth {
949 return Err(MongrelError::AuthNotRequired);
950 }
951
952 let user = cat
957 .users
958 .iter()
959 .find(|u| u.username == username)
960 .filter(|u| !u.password_hash.is_empty())
961 .ok_or_else(|| MongrelError::InvalidCredentials {
962 username: username.to_string(),
963 })?;
964 let password_ok = crate::auth::verify_password(password, &user.password_hash)
965 .map_err(MongrelError::Other)?;
966 if !password_ok {
967 return Err(MongrelError::InvalidCredentials {
968 username: username.to_string(),
969 });
970 }
971
972 let principal =
974 Self::resolve_principal_from_catalog(&cat, &user.username).ok_or_else(|| {
975 MongrelError::InvalidCredentials {
976 username: username.to_string(),
977 }
978 })?;
979
980 Self::finish_open(
981 root,
982 cat,
983 kek,
984 meta_dek,
985 true,
986 Some(principal),
987 lock_timeout_ms,
988 )
989 }
990
991 pub fn create_with_credentials(
1001 root: impl AsRef<Path>,
1002 admin_username: &str,
1003 admin_password: &str,
1004 ) -> Result<Self> {
1005 Self::create_inner_with_credentials(root, None, admin_username, admin_password)
1006 }
1007
1008 #[cfg(feature = "encryption")]
1012 pub fn create_encrypted_with_credentials(
1013 root: impl AsRef<Path>,
1014 passphrase: &str,
1015 admin_username: &str,
1016 admin_password: &str,
1017 ) -> Result<Self> {
1018 let root = root.as_ref();
1019 Self::reject_existing_database(root)?;
1020 std::fs::create_dir_all(root)?;
1021 std::fs::create_dir_all(root.join(META_DIR))?;
1022 let salt = crate::encryption::random_salt();
1023 std::fs::write(root.join(META_DIR).join(KEYS_FILENAME), salt)?;
1024 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1025 Self::create_inner_with_credentials(root, Some(kek), admin_username, admin_password)
1026 }
1027
1028 fn create_inner_with_credentials(
1029 root: impl AsRef<Path>,
1030 kek: Option<Arc<crate::encryption::Kek>>,
1031 admin_username: &str,
1032 admin_password: &str,
1033 ) -> Result<Self> {
1034 let root = root.as_ref().to_path_buf();
1035 Self::reject_existing_database(&root)?;
1036 std::fs::create_dir_all(&root)?;
1037 std::fs::create_dir_all(root.join(TABLES_DIR))?;
1038 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
1039
1040 let password_hash =
1042 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
1043 let mut cat = Catalog::empty();
1044 cat.require_auth = true;
1045 cat.next_user_id = 1;
1046 cat.users.push(crate::auth::UserEntry {
1047 id: 1,
1048 username: admin_username.to_string(),
1049 password_hash,
1050 roles: Vec::new(),
1051 is_admin: true,
1052 created_epoch: 0,
1053 });
1054 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
1055
1056 let admin_principal = crate::auth::Principal {
1059 username: admin_username.to_string(),
1060 is_admin: true,
1061 roles: Vec::new(),
1062 permissions: Vec::new(),
1063 };
1064 Self::finish_open(root, cat, kek, meta_dek, false, Some(admin_principal), 0)
1065 }
1066
1067 fn reject_existing_database(root: &Path) -> Result<()> {
1068 if root.join(catalog::CATALOG_FILENAME).exists() {
1071 return Err(MongrelError::InvalidArgument(format!(
1072 "database already exists at {}; use Database::open() to open it, \
1073 or remove the directory first",
1074 root.display()
1075 )));
1076 }
1077 Ok(())
1078 }
1079
1080 fn open_inner(
1081 root: impl AsRef<Path>,
1082 kek: Option<Arc<crate::encryption::Kek>>,
1083 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
1084 ) -> Result<Self> {
1085 let root = root.as_ref().to_path_buf();
1086 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
1087 let cat = catalog::read(&root, meta_dek.as_ref())?
1088 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
1089 Self::finish_open(root, cat, kek, meta_dek, true, None, 0)
1090 }
1091
1092 fn fs_lock_exclusive(f: &std::fs::File, timeout_ms: u32) -> std::io::Result<()> {
1104 use fs2::FileExt;
1105 if timeout_ms == 0 {
1106 return f.try_lock_exclusive();
1107 }
1108 let deadline =
1110 std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms as u64);
1111 let mut next_sleep = std::time::Duration::from_millis(1);
1112 loop {
1113 match f.try_lock_exclusive() {
1114 Ok(()) => return Ok(()),
1115 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
1116 let now = std::time::Instant::now();
1117 if now >= deadline {
1118 return Err(std::io::Error::new(
1119 std::io::ErrorKind::WouldBlock,
1120 format!("could not acquire database lock within {timeout_ms}ms"),
1121 ));
1122 }
1123 let remaining = deadline - now;
1124 let sleep = next_sleep.min(remaining);
1125 std::thread::sleep(sleep);
1126 next_sleep = next_sleep
1129 .saturating_mul(10)
1130 .min(std::time::Duration::from_millis(50));
1131 }
1132 Err(e) => return Err(e),
1133 }
1134 }
1135 }
1136
1137 fn finish_open(
1138 root: PathBuf,
1139 cat: Catalog,
1140 kek: Option<Arc<crate::encryption::Kek>>,
1141 meta_dek: Option<[u8; META_DEK_LEN]>,
1142 existing: bool,
1143 principal: Option<crate::auth::Principal>,
1144 lock_timeout_ms: u32,
1145 ) -> Result<Self> {
1146 let read_only = existing && root.join(META_DIR).join("replica").exists();
1147 std::fs::create_dir_all(root.join("_meta")).ok();
1153 let lock_path = root.join("_meta").join(".lock");
1154 let canonical = lock_path.canonicalize().unwrap_or(lock_path.clone());
1155 let lock_file = {
1156 static LOCKED_PATHS: std::sync::OnceLock<
1157 std::sync::Mutex<std::collections::HashSet<PathBuf>>,
1158 > = std::sync::OnceLock::new();
1159 let locked = LOCKED_PATHS
1160 .get_or_init(|| std::sync::Mutex::new(std::collections::HashSet::new()));
1161 let mut guard = locked.lock().unwrap();
1162 if guard.contains(&canonical) {
1163 None
1165 } else {
1166 let f = std::fs::OpenOptions::new()
1167 .create(true)
1168 .truncate(false)
1169 .write(true)
1170 .open(&lock_path)?;
1171 Self::fs_lock_exclusive(&f, lock_timeout_ms).map_err(|e| {
1172 MongrelError::Io(std::io::Error::other(format!(
1173 "database at {} is locked by another process: {e}",
1174 root.display()
1175 )))
1176 })?;
1177 guard.insert(canonical.clone());
1178 Some(f)
1179 }
1180 };
1181 if lock_file.is_some() {
1182 let stale_backup_pins = root.join(META_DIR).join("backup-pins");
1183 if stale_backup_pins.exists() {
1184 std::fs::remove_dir_all(stale_backup_pins)?;
1185 }
1186 }
1187
1188 let epoch = Arc::new(EpochAuthority::new(cat.db_epoch));
1189 let snapshots = Arc::new(SnapshotRegistry::new());
1190 let (history_epochs, history_start) = read_history_retention(&root, Epoch(cat.db_epoch))?;
1191 snapshots.configure_history(history_epochs, history_start);
1192 let page_cache = Arc::new(crate::cache::Sharded::new(
1193 crate::cache::CACHE_SHARDS,
1194 || {
1195 crate::cache::PageCache::new(
1196 crate::engine::PAGE_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
1197 )
1198 },
1199 ));
1200 let decoded_cache = Arc::new(crate::cache::Sharded::new(
1201 crate::cache::CACHE_SHARDS,
1202 || {
1203 crate::cache::DecodedPageCache::new(
1204 crate::engine::DECODED_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
1205 )
1206 },
1207 ));
1208 let commit_lock = Arc::new(Mutex::new(()));
1209 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
1210 let shared_wal = Arc::new(Mutex::new(if existing {
1211 crate::wal::SharedWal::open(&root, Epoch(cat.db_epoch), wal_dek.clone())?
1212 } else {
1213 crate::wal::SharedWal::create_with_dek(&root, Epoch(cat.db_epoch), wal_dek.clone())?
1214 }));
1215 let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));
1219 let group = Arc::new(crate::txn::GroupCommit::new(
1220 shared_wal.lock().durable_seq(),
1221 ));
1222 let (change_wake, _change_rx) = tokio::sync::broadcast::channel(256);
1223 let txn_ids = Arc::new(Mutex::new(1u64));
1227
1228 let mut cat = cat;
1234 if existing {
1235 recover_ddl_from_wal(&root, &mut cat, meta_dek.as_ref(), wal_dek.as_ref())?;
1236 }
1237
1238 let auth_state = crate::auth_state::AuthState::new(cat.require_auth, principal.clone());
1243 let auth_checker: Option<Arc<dyn crate::auth_state::TableAuthChecker>> = Some(Arc::new(
1244 crate::auth_state::DefaultTableAuthChecker::new(auth_state.clone()),
1245 ));
1246
1247 let mut tables: HashMap<u64, TableHandle> = HashMap::new();
1253 for entry in &cat.tables {
1254 if !matches!(entry.state, TableState::Live) {
1255 continue;
1256 }
1257 let tdir = root.join(TABLES_DIR).join(entry.table_id.to_string());
1258 let ctx = SharedCtx {
1259 epoch: Arc::clone(&epoch),
1260 page_cache: Arc::clone(&page_cache),
1261 decoded_cache: Arc::clone(&decoded_cache),
1262 snapshots: Arc::clone(&snapshots),
1263 kek: kek.clone(),
1264 commit_lock: Arc::clone(&commit_lock),
1265 shared: Some(crate::engine::SharedWalCtx {
1266 wal: Arc::clone(&shared_wal),
1267 group: Arc::clone(&group),
1268 poisoned: Arc::clone(&poisoned),
1269 txn_ids: Arc::clone(&txn_ids),
1270 change_wake: change_wake.clone(),
1271 }),
1272 table_name: Some(entry.name.clone()),
1273 auth: auth_checker.clone(),
1274 read_only,
1275 };
1276 let t = Table::open_in(&tdir, ctx)?;
1277 tables.insert(entry.table_id, Arc::new(Mutex::new(t)));
1278 }
1279
1280 if existing {
1286 recover_shared_wal(&root, &tables, &epoch, wal_dek.as_ref())?;
1287 sweep_pending_txn_dirs(&root, &cat);
1290 }
1291
1292 let open_generation = if existing {
1298 let meta_dir = root.join(META_DIR);
1299 let gen = catalog::read_generation(&meta_dir);
1300 let bumped = gen.wrapping_add(1);
1301 catalog::write_generation(&meta_dir, bumped)?;
1302 bumped
1303 } else {
1304 0
1305 };
1306 let next_txn_id = (open_generation << 32) | 1;
1307 *txn_ids.lock() = next_txn_id;
1309
1310 if existing && cat.require_auth && principal.is_none() {
1318 return Err(MongrelError::AuthRequired);
1319 }
1320
1321 Ok(Self {
1322 root,
1323 read_only,
1324 catalog: RwLock::new(cat),
1325 rls_cache: Mutex::new(RlsCache::default()),
1326 epoch,
1327 snapshots,
1328 page_cache,
1329 decoded_cache,
1330 commit_lock,
1331 shared_wal,
1332 next_txn_id: txn_ids,
1333 tables: RwLock::new(tables),
1334 kek,
1335 ddl_lock: Mutex::new(()),
1336 meta_dek,
1337 conflicts: crate::txn::ConflictIndex::new(),
1338 active_txns: crate::txn::ActiveTxns::new(),
1339 poisoned,
1340 group,
1341 spill_threshold: std::sync::atomic::AtomicU64::new(64 * 1024 * 1024),
1342 active_spills: Arc::new(crate::retention::ActiveSpills::new()),
1343 replication_barrier: parking_lot::RwLock::new(()),
1344 replication_wal_retention_segments: AtomicUsize::new(0),
1345 backup_pins: Mutex::new(HashMap::new()),
1346 spill_hook: Mutex::new(None),
1347 backup_hook: Mutex::new(None),
1348 trigger_recursive: AtomicBool::new(TriggerConfig::default().recursive_triggers),
1349 trigger_max_depth: AtomicU32::new(TriggerConfig::default().max_depth),
1350 trigger_max_loop_iterations: AtomicU32::new(
1351 TriggerConfig::default().max_loop_iterations,
1352 ),
1353 _lock: lock_file,
1354 notify: {
1355 let (tx, _rx) = tokio::sync::broadcast::channel(256);
1356 tx
1357 },
1358 change_wake,
1359 principal: RwLock::new(principal),
1360 auth_state,
1361 })
1362 }
1363
1364 pub fn visible_epoch(&self) -> Epoch {
1366 self.epoch.visible()
1367 }
1368
1369 pub fn catalog_snapshot(&self) -> Catalog {
1371 self.catalog.read().clone()
1372 }
1373
1374 pub fn materialized_view(&self, name: &str) -> Option<crate::catalog::MaterializedViewEntry> {
1375 self.catalog
1376 .read()
1377 .materialized_views
1378 .iter()
1379 .find(|definition| definition.name == name)
1380 .cloned()
1381 }
1382
1383 pub fn materialized_views(&self) -> Vec<crate::catalog::MaterializedViewEntry> {
1384 self.catalog.read().materialized_views.clone()
1385 }
1386
1387 pub fn security_catalog(&self) -> crate::security::SecurityCatalog {
1388 self.catalog.read().security.clone()
1389 }
1390
1391 pub fn security_active_for(&self, table: &str) -> bool {
1392 self.catalog.read().security.table_has_security(table)
1393 }
1394
1395 pub fn set_security_catalog(&self, security: crate::security::SecurityCatalog) -> Result<()> {
1397 self.set_security_catalog_as(security, None)
1398 }
1399
1400 pub fn set_security_catalog_as(
1402 &self,
1403 security: crate::security::SecurityCatalog,
1404 principal: Option<&crate::auth::Principal>,
1405 ) -> Result<()> {
1406 use crate::wal::DdlOp;
1407 use std::sync::atomic::Ordering;
1408
1409 self.require_for(principal, &crate::auth::Permission::Admin)?;
1410 if self.poisoned.load(Ordering::Relaxed) {
1411 return Err(MongrelError::Other(
1412 "database poisoned by fsync error".into(),
1413 ));
1414 }
1415 let _ddl = self.ddl_lock.lock();
1416 validate_security_catalog(&self.catalog.read(), &security)?;
1417 let payload = DdlOp::encode_security(&security)?;
1418 let _commit = self.commit_lock.lock();
1419 let epoch = self.epoch.bump_assigned();
1420 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
1421 let txn_id = self.alloc_txn_id();
1422 let commit_seq = {
1423 let mut wal = self.shared_wal.lock();
1424 wal.append(
1425 txn_id,
1426 WAL_TABLE_ID,
1427 crate::wal::Op::Ddl(DdlOp::SetSecurityCatalog {
1428 security_json: payload,
1429 }),
1430 )?;
1431 wal.append_commit(txn_id, epoch, &[])?
1432 };
1433 self.group
1434 .await_durable(&self.shared_wal, commit_seq)
1435 .inspect_err(|_| {
1436 self.poisoned.store(true, Ordering::Relaxed);
1437 })?;
1438 {
1439 let mut catalog = self.catalog.write();
1440 catalog.security = security;
1441 catalog.security_version = catalog.security_version.wrapping_add(1);
1442 catalog.db_epoch = catalog.db_epoch.max(epoch.0);
1443 }
1444 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1445 self.epoch.publish_in_order(epoch);
1446 epoch_guard.disarm();
1447 Ok(())
1448 }
1449
1450 pub fn require_for(
1451 &self,
1452 principal: Option<&crate::auth::Principal>,
1453 permission: &crate::auth::Permission,
1454 ) -> Result<()> {
1455 let Some(principal) = principal else {
1456 return self.require(permission);
1457 };
1458 if principal.has_permission(permission) {
1459 Ok(())
1460 } else {
1461 Err(MongrelError::PermissionDenied {
1462 required: permission.clone(),
1463 principal: principal.username.clone(),
1464 })
1465 }
1466 }
1467
1468 pub fn principal_snapshot(&self) -> Option<crate::auth::Principal> {
1469 self.principal.read().clone()
1470 }
1471
1472 pub fn require_columns_for(
1473 &self,
1474 table: &str,
1475 operation: crate::auth::ColumnOperation,
1476 column_ids: &[u16],
1477 principal: Option<&crate::auth::Principal>,
1478 ) -> Result<()> {
1479 let schema = self
1480 .catalog
1481 .read()
1482 .live(table)
1483 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
1484 .schema
1485 .clone();
1486 let cached = self.principal.read().clone();
1487 let principal = principal.or(cached.as_ref());
1488 let Some(principal) = principal else {
1489 let permission = match operation {
1490 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
1491 table: table.to_string(),
1492 },
1493 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
1494 table: table.to_string(),
1495 },
1496 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
1497 table: table.to_string(),
1498 },
1499 };
1500 return self.require(&permission);
1501 };
1502 match principal.column_access(table, operation) {
1503 crate::auth::ColumnAccess::All => Ok(()),
1504 crate::auth::ColumnAccess::Columns(allowed) => {
1505 let denied = column_ids.iter().find_map(|column_id| {
1506 schema
1507 .columns
1508 .iter()
1509 .find(|column| column.id == *column_id)
1510 .filter(|column| !allowed.contains(&column.name))
1511 });
1512 if denied.is_none() {
1513 Ok(())
1514 } else {
1515 Err(MongrelError::PermissionDenied {
1516 required: match operation {
1517 crate::auth::ColumnOperation::Select => {
1518 crate::auth::Permission::SelectColumns {
1519 table: table.to_string(),
1520 columns: denied
1521 .into_iter()
1522 .map(|column| column.name.clone())
1523 .collect(),
1524 }
1525 }
1526 crate::auth::ColumnOperation::Insert => {
1527 crate::auth::Permission::InsertColumns {
1528 table: table.to_string(),
1529 columns: denied
1530 .into_iter()
1531 .map(|column| column.name.clone())
1532 .collect(),
1533 }
1534 }
1535 crate::auth::ColumnOperation::Update => {
1536 crate::auth::Permission::UpdateColumns {
1537 table: table.to_string(),
1538 columns: denied
1539 .into_iter()
1540 .map(|column| column.name.clone())
1541 .collect(),
1542 }
1543 }
1544 },
1545 principal: principal.username.clone(),
1546 })
1547 }
1548 }
1549 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
1550 required: match operation {
1551 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
1552 table: table.to_string(),
1553 },
1554 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
1555 table: table.to_string(),
1556 },
1557 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
1558 table: table.to_string(),
1559 },
1560 },
1561 principal: principal.username.clone(),
1562 }),
1563 }
1564 }
1565
1566 pub fn select_column_ids_for(
1567 &self,
1568 table: &str,
1569 principal: Option<&crate::auth::Principal>,
1570 ) -> Result<Vec<u16>> {
1571 let catalog = self.catalog.read();
1572 let columns = catalog
1573 .live(table)
1574 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
1575 .schema
1576 .columns
1577 .iter()
1578 .map(|column| (column.id, column.name.clone()))
1579 .collect::<Vec<_>>();
1580 drop(catalog);
1581 let cached_principal = self.principal.read().clone();
1582 let principal = principal.or(cached_principal.as_ref());
1583 let Some(principal) = principal else {
1584 self.require(&crate::auth::Permission::Select {
1585 table: table.to_string(),
1586 })?;
1587 return Ok(columns.iter().map(|(id, _)| *id).collect());
1588 };
1589 match principal.column_access(table, crate::auth::ColumnOperation::Select) {
1590 crate::auth::ColumnAccess::All => Ok(columns.iter().map(|(id, _)| *id).collect()),
1591 crate::auth::ColumnAccess::Columns(allowed) => Ok(columns
1592 .iter()
1593 .filter(|(_, name)| allowed.contains(name))
1594 .map(|(id, _)| *id)
1595 .collect()),
1596 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
1597 required: crate::auth::Permission::Select {
1598 table: table.to_string(),
1599 },
1600 principal: principal.username.clone(),
1601 }),
1602 }
1603 }
1604
1605 pub fn secure_rows_for(
1606 &self,
1607 table: &str,
1608 rows: Vec<crate::memtable::Row>,
1609 principal: Option<&crate::auth::Principal>,
1610 ) -> Result<Vec<crate::memtable::Row>> {
1611 let security = self.catalog.read().security.clone();
1612 if !security.table_has_security(table) {
1613 return Ok(rows);
1614 }
1615 let owned;
1616 let principal = match principal {
1617 Some(principal) => principal,
1618 None => {
1619 owned = self
1620 .principal
1621 .read()
1622 .clone()
1623 .ok_or(MongrelError::AuthRequired)?;
1624 &owned
1625 }
1626 };
1627 let mut output = Vec::new();
1628 for mut row in rows {
1629 if security.row_allowed(
1630 table,
1631 crate::security::PolicyCommand::Select,
1632 &row,
1633 principal,
1634 false,
1635 ) {
1636 security.apply_masks(table, &mut row, principal);
1637 output.push(row);
1638 }
1639 }
1640 Ok(output)
1641 }
1642
1643 pub fn authorized_candidate_ids_for(
1645 &self,
1646 table: &str,
1647 principal: Option<&crate::auth::Principal>,
1648 ) -> Result<Option<std::collections::HashSet<RowId>>> {
1649 Ok(self
1650 .authorized_read_snapshot(table, principal)?
1651 .allowed_row_ids)
1652 }
1653
1654 fn allowed_row_ids_locked(
1655 &self,
1656 table_name: &str,
1657 table: &Table,
1658 table_snapshot: Snapshot,
1659 security: &crate::security::SecurityCatalog,
1660 security_version: u64,
1661 principal: Option<&crate::auth::Principal>,
1662 ) -> Result<Option<Arc<HashSet<RowId>>>> {
1663 if !security.rls_enabled(table_name) {
1664 return Ok(None);
1665 }
1666 let authorization_started = std::time::Instant::now();
1667 let principal = principal.ok_or(MongrelError::AuthRequired)?;
1668 let mut roles = principal.roles.clone();
1669 roles.sort_unstable();
1670 let principal_key = format!("{}:{}:{roles:?}", principal.username, principal.is_admin);
1671 let cache_key = (
1672 table_name.to_string(),
1673 table.data_generation(),
1674 security_version,
1675 principal_key,
1676 );
1677 if let Some(allowed) = self.rls_cache.lock().get(&cache_key) {
1678 crate::trace::QueryTrace::record(|trace| {
1679 trace.rls_cache_hit = true;
1680 trace.authorization_nanos = trace
1681 .authorization_nanos
1682 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
1683 });
1684 return Ok(Some(allowed));
1685 }
1686 let started = std::time::Instant::now();
1688 let rows = table.visible_rows(table_snapshot)?;
1689 let rows_evaluated = rows.len() as u64;
1690 let allowed = Arc::new(
1691 rows.into_iter()
1692 .filter(|row| {
1693 security.row_allowed(
1694 table_name,
1695 crate::security::PolicyCommand::Select,
1696 row,
1697 principal,
1698 false,
1699 )
1700 })
1701 .map(|row| row.row_id)
1702 .collect(),
1703 );
1704 let mut cache = self.rls_cache.lock();
1705 cache.build_nanos = cache
1706 .build_nanos
1707 .saturating_add(started.elapsed().as_nanos() as u64);
1708 cache.rows_evaluated = cache.rows_evaluated.saturating_add(rows_evaluated);
1709 cache.insert(cache_key, Arc::clone(&allowed));
1710 crate::trace::QueryTrace::record(|trace| {
1711 trace.rls_rows_evaluated = trace
1712 .rls_rows_evaluated
1713 .saturating_add(rows_evaluated as usize);
1714 trace.authorization_nanos = trace
1715 .authorization_nanos
1716 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
1717 });
1718 Ok(Some(allowed))
1719 }
1720
1721 fn principal_for_authorized_read(
1722 &self,
1723 catalog: &Catalog,
1724 principal: Option<&crate::auth::Principal>,
1725 catalog_bound: bool,
1726 ) -> Result<Option<crate::auth::Principal>> {
1727 let principal = principal.cloned().or_else(|| self.principal.read().clone());
1728 let Some(principal) = principal else {
1729 return Ok(None);
1730 };
1731 if catalog_bound
1732 || catalog
1733 .users
1734 .iter()
1735 .any(|user| user.username == principal.username)
1736 {
1737 return Self::resolve_principal_from_catalog(catalog, &principal.username)
1738 .map(Some)
1739 .ok_or(MongrelError::AuthRequired);
1740 }
1741 Ok(Some(principal))
1742 }
1743
1744 pub fn with_authorized_read<T, F>(
1748 &self,
1749 table_name: &str,
1750 principal: Option<&crate::auth::Principal>,
1751 catalog_bound: bool,
1752 mut read: F,
1753 ) -> Result<T>
1754 where
1755 F: FnMut(
1756 &mut Table,
1757 Snapshot,
1758 Option<&HashSet<RowId>>,
1759 Option<&crate::auth::Principal>,
1760 ) -> Result<T>,
1761 {
1762 const RETRIES: usize = 3;
1763 let handle = self.table(table_name)?;
1764 for attempt in 0..RETRIES {
1765 crate::trace::QueryTrace::record(|trace| {
1766 trace.authorization_retries = attempt;
1767 });
1768 let (security, security_version, effective_principal) = {
1769 let catalog = self.catalog.read();
1770 (
1771 catalog.security.clone(),
1772 catalog.security_version,
1773 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
1774 )
1775 };
1776 let result = {
1777 let mut table = handle.lock();
1778 let snapshot = table.snapshot();
1779 let allowed = self.allowed_row_ids_locked(
1780 table_name,
1781 &table,
1782 snapshot,
1783 &security,
1784 security_version,
1785 effective_principal.as_ref(),
1786 )?;
1787 read(
1788 &mut table,
1789 snapshot,
1790 allowed.as_deref(),
1791 effective_principal.as_ref(),
1792 )?
1793 };
1794 if self.catalog.read().security_version == security_version {
1795 return Ok(result);
1796 }
1797 if attempt + 1 == RETRIES {
1798 return Err(MongrelError::Conflict(
1799 "security policy changed during scored read".into(),
1800 ));
1801 }
1802 }
1803 unreachable!()
1804 }
1805
1806 pub fn authorized_read_snapshot(
1809 &self,
1810 table: &str,
1811 principal: Option<&crate::auth::Principal>,
1812 ) -> Result<AuthorizedReadSnapshot> {
1813 let (security, security_version, effective_principal) = {
1814 let catalog = self.catalog.read();
1815 (
1816 catalog.security.clone(),
1817 catalog.security_version,
1818 self.principal_for_authorized_read(&catalog, principal, false)?,
1819 )
1820 };
1821 let handle = self.table(table)?;
1822 let (table_snapshot, data_generation, allowed_row_ids) = {
1823 let table_handle = handle.lock();
1824 let table_snapshot = table_handle.snapshot();
1825 let data_generation = table_handle.data_generation();
1826 let allowed = self.allowed_row_ids_locked(
1827 table,
1828 &table_handle,
1829 table_snapshot,
1830 &security,
1831 security_version,
1832 effective_principal.as_ref(),
1833 )?;
1834 (
1835 table_snapshot,
1836 data_generation,
1837 allowed.map(|allowed| (*allowed).clone()),
1838 )
1839 };
1840 Ok(AuthorizedReadSnapshot {
1841 table: table.to_string(),
1842 table_snapshot,
1843 data_generation,
1844 security_version,
1845 allowed_row_ids,
1846 })
1847 }
1848
1849 pub fn authorized_read_snapshot_valid(&self, snapshot: &AuthorizedReadSnapshot) -> bool {
1850 if self.catalog.read().security_version != snapshot.security_version {
1851 return false;
1852 }
1853 self.table(&snapshot.table)
1854 .ok()
1855 .is_some_and(|table| table.lock().data_generation() == snapshot.data_generation)
1856 }
1857
1858 pub fn rls_cache_stats(&self) -> RlsCacheStats {
1859 self.rls_cache.lock().stats()
1860 }
1861
1862 pub fn rows_for(
1864 &self,
1865 table: &str,
1866 principal: Option<&crate::auth::Principal>,
1867 ) -> Result<Vec<crate::memtable::Row>> {
1868 let allowed = self.select_column_ids_for(table, principal)?;
1869 let handle = self.table(table)?;
1870 let rows = {
1871 let table = handle.lock();
1872 table.visible_rows(table.snapshot())?
1873 };
1874 let mut rows = self.secure_rows_for(table, rows, principal)?;
1875 for row in &mut rows {
1876 row.columns.retain(|column, _| allowed.contains(column));
1877 }
1878 Ok(rows)
1879 }
1880
1881 pub fn count_for(
1883 &self,
1884 table: &str,
1885 principal: Option<&crate::auth::Principal>,
1886 ) -> Result<u64> {
1887 self.select_column_ids_for(table, principal)?;
1888 if self.security_active_for(table) {
1889 Ok(self.rows_for(table, principal)?.len() as u64)
1890 } else {
1891 Ok(self.table(table)?.lock().count())
1892 }
1893 }
1894
1895 pub fn put_for(
1897 &self,
1898 table: &str,
1899 mut cells: Vec<(u16, crate::memtable::Value)>,
1900 principal: Option<&crate::auth::Principal>,
1901 ) -> Result<RowId> {
1902 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
1903 self.require_columns_for(
1904 table,
1905 crate::auth::ColumnOperation::Insert,
1906 &columns,
1907 principal,
1908 )?;
1909 let handle = self.table(table)?;
1910 let mut table_handle = handle.lock();
1911 table_handle.fill_auto_inc(&mut cells)?;
1912 table_handle.apply_defaults(&mut cells)?;
1913 let mut row = crate::memtable::Row::new(RowId(0), self.epoch.visible());
1914 row.columns.extend(cells.iter().cloned());
1915 self.check_row_policy_for(
1916 table,
1917 crate::security::PolicyCommand::Insert,
1918 &row,
1919 true,
1920 principal,
1921 )?;
1922 table_handle.put(cells)
1923 }
1924
1925 pub fn check_row_policy_for(
1926 &self,
1927 table: &str,
1928 command: crate::security::PolicyCommand,
1929 row: &crate::memtable::Row,
1930 check_new: bool,
1931 principal: Option<&crate::auth::Principal>,
1932 ) -> Result<()> {
1933 let security = self.catalog.read().security.clone();
1934 if !security.rls_enabled(table) {
1935 return Ok(());
1936 }
1937 let cached = self.principal.read().clone();
1938 let principal = principal
1939 .or(cached.as_ref())
1940 .ok_or(MongrelError::AuthRequired)?;
1941 if security.row_allowed(table, command, row, principal, check_new) {
1942 return Ok(());
1943 }
1944 let required = match command {
1945 crate::security::PolicyCommand::Insert => crate::auth::Permission::Insert {
1946 table: table.to_string(),
1947 },
1948 crate::security::PolicyCommand::Update => crate::auth::Permission::Update {
1949 table: table.to_string(),
1950 },
1951 crate::security::PolicyCommand::Select => crate::auth::Permission::Select {
1952 table: table.to_string(),
1953 },
1954 crate::security::PolicyCommand::Delete | crate::security::PolicyCommand::All => {
1955 crate::auth::Permission::Delete {
1956 table: table.to_string(),
1957 }
1958 }
1959 };
1960 Err(MongrelError::PermissionDenied {
1961 required,
1962 principal: principal.username.clone(),
1963 })
1964 }
1965
1966 pub fn set_materialized_view(
1969 &self,
1970 definition: crate::catalog::MaterializedViewEntry,
1971 ) -> Result<()> {
1972 use crate::wal::DdlOp;
1973 use std::sync::atomic::Ordering;
1974
1975 self.require(&crate::auth::Permission::Ddl)?;
1976 if self.poisoned.load(Ordering::Relaxed) {
1977 return Err(MongrelError::Other(
1978 "database poisoned by fsync error".into(),
1979 ));
1980 }
1981 if definition.name.is_empty() || definition.query.trim().is_empty() {
1982 return Err(MongrelError::InvalidArgument(
1983 "materialized view name and query must not be empty".into(),
1984 ));
1985 }
1986
1987 let _ddl = self.ddl_lock.lock();
1988 let table_id = self
1989 .catalog
1990 .read()
1991 .live(&definition.name)
1992 .ok_or_else(|| {
1993 MongrelError::NotFound(format!(
1994 "materialized view table {:?} not found",
1995 definition.name
1996 ))
1997 })?
1998 .table_id;
1999 let definition_json = DdlOp::encode_materialized_view(&definition)?;
2000 let _commit = self.commit_lock.lock();
2001 let epoch = self.epoch.bump_assigned();
2002 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2003 let txn_id = self.alloc_txn_id();
2004 let commit_seq = {
2005 let mut wal = self.shared_wal.lock();
2006 wal.append(
2007 txn_id,
2008 table_id,
2009 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
2010 name: definition.name.clone(),
2011 definition_json,
2012 }),
2013 )?;
2014 wal.append_commit(txn_id, epoch, &[])?
2015 };
2016 self.group
2017 .await_durable(&self.shared_wal, commit_seq)
2018 .inspect_err(|_| {
2019 self.poisoned.store(true, Ordering::Relaxed);
2020 })?;
2021
2022 {
2023 let mut catalog = self.catalog.write();
2024 if let Some(existing) = catalog
2025 .materialized_views
2026 .iter_mut()
2027 .find(|existing| existing.name == definition.name)
2028 {
2029 *existing = definition;
2030 } else {
2031 catalog.materialized_views.push(definition);
2032 }
2033 }
2034 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2035 self.epoch.publish_in_order(epoch);
2036 epoch_guard.disarm();
2037 Ok(())
2038 }
2039
2040 pub fn root(&self) -> &Path {
2042 &self.root
2043 }
2044
2045 pub fn is_read_only_replica(&self) -> bool {
2046 self.read_only
2047 }
2048
2049 pub fn set_replication_wal_retention_segments(&self, segments: usize) {
2050 self.replication_wal_retention_segments
2051 .store(segments, std::sync::atomic::Ordering::Relaxed);
2052 }
2053
2054 pub fn replication_snapshot(&self) -> Result<crate::replication::ReplicationSnapshot> {
2059 let _barrier = self.replication_barrier.write();
2060 let _ddl = self.ddl_lock.lock();
2061 let mut handles: Vec<_> = self
2062 .tables
2063 .read()
2064 .iter()
2065 .map(|(id, handle)| (*id, Arc::clone(handle)))
2066 .collect();
2067 handles.sort_by_key(|(id, _)| *id);
2068 let _table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
2069 let _commit = self.commit_lock.lock();
2070 let mut wal = self.shared_wal.lock();
2071 wal.group_sync()?;
2072 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
2073 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
2074 let epoch = records
2075 .iter()
2076 .filter_map(|record| match &record.op {
2077 crate::wal::Op::TxnCommit { epoch, .. } => Some(*epoch),
2078 _ => None,
2079 })
2080 .max()
2081 .unwrap_or(0)
2082 .max(self.visible_epoch().0);
2083 let files = crate::replication::capture_files(&self.root)?;
2084 drop(wal);
2085 Ok(crate::replication::ReplicationSnapshot::new(epoch, files))
2086 }
2087
2088 pub fn hot_backup(&self, destination: impl AsRef<Path>) -> Result<crate::backup::BackupReport> {
2096 self.require(&crate::auth::Permission::Ddl)?;
2097 let (destination, parent, stage) =
2098 prepare_backup_destination(&self.root, destination.as_ref())?;
2099 std::fs::create_dir(&stage)?;
2100
2101 let outcome = (|| {
2102 let barrier = self.replication_barrier.write();
2103 let ddl = self.ddl_lock.lock();
2104 let mut handles: Vec<_> = self
2105 .tables
2106 .read()
2107 .iter()
2108 .map(|(id, handle)| (*id, Arc::clone(handle)))
2109 .collect();
2110 handles.sort_by_key(|(id, _)| *id);
2111 let table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
2112 let commit = self.commit_lock.lock();
2113 let mut wal = self.shared_wal.lock();
2114 wal.group_sync()?;
2115 let epoch = self.visible_epoch().0;
2116
2117 let pin_nonce = std::time::SystemTime::now()
2118 .duration_since(std::time::UNIX_EPOCH)
2119 .unwrap_or_default()
2120 .as_nanos();
2121 let file_pin_root = self
2122 .root
2123 .join(META_DIR)
2124 .join("backup-pins")
2125 .join(format!("{}-{pin_nonce}", std::process::id()));
2126 std::fs::create_dir_all(&file_pin_root)?;
2127 let _file_pins = BackupFilePins {
2128 root: file_pin_root.clone(),
2129 };
2130 let mut run_files = Vec::new();
2131 for (index, (table_id, _)) in handles.iter().enumerate() {
2132 let table = &table_guards[index];
2133 for run in table.run_refs() {
2134 let source = table.runs_dir().join(format!("r-{}.sr", run.run_id));
2135 let relative = source
2136 .strip_prefix(&self.root)
2137 .map_err(|error| MongrelError::Other(format!("backup run path: {error}")))?
2138 .to_path_buf();
2139 let pinned = file_pin_root.join(format!("{table_id}-{}.sr", run.run_id));
2140 if std::fs::hard_link(&source, &pinned).is_err() {
2141 crate::backup::copy_file_synced(&source, &pinned)?;
2142 }
2143 run_files.push(((*table_id, run.run_id), pinned, relative));
2144 }
2145 }
2146 std::fs::File::open(&file_pin_root)?.sync_all()?;
2147 let run_keys: Vec<_> = run_files.iter().map(|(key, _, _)| *key).collect();
2148 {
2149 let mut pins = self.backup_pins.lock();
2150 for key in &run_keys {
2151 *pins.entry(*key).or_insert(0) += 1;
2152 }
2153 }
2154 let _run_pins = BackupRunPins {
2155 pins: &self.backup_pins,
2156 runs: run_keys,
2157 };
2158 let deferred: HashSet<_> = run_files
2159 .iter()
2160 .map(|(_, _, relative)| relative.clone())
2161 .collect();
2162 let mut copied = Vec::new();
2163 copy_backup_boundary(&self.root, &stage, &deferred, &mut copied)?;
2164
2165 drop(wal);
2166 drop(commit);
2167 drop(table_guards);
2168 drop(ddl);
2169 drop(barrier);
2170
2171 if let Some(hook) = self.backup_hook.lock().as_ref() {
2172 hook();
2173 }
2174 for (_, source, relative) in run_files {
2175 crate::backup::copy_file_synced(&source, &stage.join(&relative))?;
2176 copied.push(relative);
2177 }
2178
2179 let manifest = crate::backup::BackupManifest::create(&stage, epoch, &copied)?;
2180 manifest.write(&stage)?;
2181 crate::backup::sync_directories(&stage)?;
2182 if destination.exists() {
2183 return Err(MongrelError::Conflict(format!(
2184 "backup destination already exists: {}",
2185 destination.display()
2186 )));
2187 }
2188 std::fs::rename(&stage, &destination)?;
2189 std::fs::File::open(&parent)?.sync_all()?;
2190 Ok(crate::backup::BackupReport {
2191 destination,
2192 epoch,
2193 files: manifest.files.len(),
2194 bytes: manifest.total_bytes(),
2195 })
2196 })();
2197
2198 if outcome.is_err() && stage.exists() {
2199 let _ = std::fs::remove_dir_all(&stage);
2200 }
2201 outcome
2202 }
2203
2204 pub fn replication_batch_since(
2207 &self,
2208 since_epoch: u64,
2209 ) -> Result<crate::replication::ReplicationBatch> {
2210 use crate::wal::Op;
2211
2212 let mut wal = self.shared_wal.lock();
2213 wal.group_sync()?;
2214 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
2215 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
2216 drop(wal);
2217
2218 let commits: HashMap<u64, u64> = records
2219 .iter()
2220 .filter_map(|record| match &record.op {
2221 Op::TxnCommit { epoch, .. } => Some((record.txn_id, *epoch)),
2222 _ => None,
2223 })
2224 .collect();
2225 let earliest_epoch = commits.values().copied().min();
2226 let current_epoch = commits
2227 .values()
2228 .copied()
2229 .max()
2230 .unwrap_or(0)
2231 .max(self.visible_epoch().0);
2232 let selected: HashSet<u64> = commits
2233 .iter()
2234 .filter_map(|(txn_id, epoch)| (*epoch > since_epoch).then_some(*txn_id))
2235 .collect();
2236 let retention_gap = since_epoch < current_epoch
2237 && earliest_epoch.map_or(true, |epoch| epoch > since_epoch.saturating_add(1));
2238 let spilled = records.iter().any(|record| {
2239 selected.contains(&record.txn_id)
2240 && matches!(
2241 &record.op,
2242 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty()
2243 )
2244 });
2245 let records = records
2246 .into_iter()
2247 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
2248 .filter(|record| selected.contains(&record.txn_id))
2249 .collect();
2250 Ok(crate::replication::ReplicationBatch {
2251 current_epoch,
2252 earliest_epoch,
2253 requires_snapshot: retention_gap || spilled,
2254 records,
2255 })
2256 }
2257
2258 pub fn append_replication_batch(&self, records: &[crate::wal::Record]) -> Result<u64> {
2262 use crate::wal::Op;
2263
2264 if !self.read_only {
2265 return Err(MongrelError::InvalidArgument(
2266 "replication batches may only target a marked replica".into(),
2267 ));
2268 }
2269 let current = crate::replication::replica_epoch(&self.root)?;
2270 let mut commits = HashMap::new();
2271 let mut commit_timestamps = HashMap::new();
2272 for record in records {
2273 match &record.op {
2274 Op::TxnCommit { epoch, added_runs } => {
2275 if !added_runs.is_empty() {
2276 return Err(MongrelError::Conflict(
2277 "replication snapshot required for spilled-run transaction".into(),
2278 ));
2279 }
2280 if commits.insert(record.txn_id, *epoch).is_some() {
2281 return Err(MongrelError::InvalidArgument(format!(
2282 "duplicate commit for replication transaction {}",
2283 record.txn_id
2284 )));
2285 }
2286 }
2287 Op::CommitTimestamp { unix_nanos } => {
2288 commit_timestamps.insert(record.txn_id, *unix_nanos);
2289 }
2290 _ => {}
2291 }
2292 }
2293 for record in records {
2294 if record.txn_id != crate::wal::SYSTEM_TXN_ID
2295 && !matches!(&record.op, Op::TxnAbort)
2296 && !commits.contains_key(&record.txn_id)
2297 {
2298 return Err(MongrelError::InvalidArgument(format!(
2299 "incomplete replication transaction {}",
2300 record.txn_id
2301 )));
2302 }
2303 }
2304 let target_epoch = commits
2305 .values()
2306 .copied()
2307 .filter(|epoch| *epoch > current)
2308 .max()
2309 .unwrap_or(current);
2310 let mut selected: HashSet<u64> = commits
2311 .iter()
2312 .filter_map(|(txn_id, epoch)| (*epoch > current).then_some(*txn_id))
2313 .collect();
2314 if selected.is_empty() {
2315 return Ok(current);
2316 }
2317 let mut wal = self.shared_wal.lock();
2318 wal.group_sync()?;
2319 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
2320 let existing: HashSet<(u64, u64)> =
2321 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
2322 .into_iter()
2323 .filter_map(|record| match record.op {
2324 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
2325 _ => None,
2326 })
2327 .collect();
2328 selected.retain(|txn_id| {
2329 commits
2330 .get(txn_id)
2331 .is_some_and(|epoch| !existing.contains(&(*txn_id, *epoch)))
2332 });
2333 for record in records {
2334 if !selected.contains(&record.txn_id) {
2335 continue;
2336 }
2337 match &record.op {
2338 Op::TxnCommit { epoch, added_runs } => {
2339 let timestamp = commit_timestamps
2340 .get(&record.txn_id)
2341 .copied()
2342 .unwrap_or_else(current_unix_nanos);
2343 wal.append_commit_at(record.txn_id, Epoch(*epoch), added_runs, timestamp)?;
2344 }
2345 Op::TxnAbort | Op::Flush { .. } | Op::CommitTimestamp { .. } => {}
2346 op => {
2347 wal.append(record.txn_id, 0, op.clone())?;
2348 }
2349 }
2350 }
2351 if !selected.is_empty() {
2352 wal.group_sync()?;
2353 }
2354 Ok(target_epoch)
2355 }
2356
2357 pub fn table_id(&self, name: &str) -> Result<u64> {
2360 let cat = self.catalog.read();
2361 cat.live(name)
2362 .map(|e| e.table_id)
2363 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
2364 }
2365
2366 pub fn procedures(&self) -> Vec<StoredProcedure> {
2367 self.catalog
2368 .read()
2369 .procedures
2370 .iter()
2371 .map(|p| p.procedure.clone())
2372 .collect()
2373 }
2374
2375 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
2376 self.catalog
2377 .read()
2378 .procedures
2379 .iter()
2380 .find(|p| p.procedure.name == name)
2381 .map(|p| p.procedure.clone())
2382 }
2383
2384 pub fn create_procedure(&self, mut procedure: StoredProcedure) -> Result<StoredProcedure> {
2385 self.require(&crate::auth::Permission::Ddl)?;
2386 let _g = self.ddl_lock.lock();
2387 procedure.validate()?;
2388 self.validate_procedure_references(&procedure)?;
2389 {
2390 let cat = self.catalog.read();
2391 if cat
2392 .procedures
2393 .iter()
2394 .any(|p| p.procedure.name == procedure.name)
2395 {
2396 return Err(MongrelError::InvalidArgument(format!(
2397 "procedure {:?} already exists",
2398 procedure.name
2399 )));
2400 }
2401 }
2402 let commit_lock = Arc::clone(&self.commit_lock);
2403 let _c = commit_lock.lock();
2404 let epoch = self.epoch.bump_assigned();
2405 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2406 procedure.created_epoch = epoch.0;
2407 procedure.updated_epoch = epoch.0;
2408 {
2409 let mut cat = self.catalog.write();
2410 cat.procedures.push(ProcedureEntry::from(procedure.clone()));
2411 cat.db_epoch = epoch.0;
2412 }
2413 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2414 self.epoch.publish_in_order(epoch);
2415 _epoch_guard.disarm();
2416 Ok(procedure)
2417 }
2418
2419 pub fn create_or_replace_procedure(
2420 &self,
2421 procedure: StoredProcedure,
2422 ) -> Result<StoredProcedure> {
2423 let _g = self.ddl_lock.lock();
2424 procedure.validate()?;
2425 self.validate_procedure_references(&procedure)?;
2426 let commit_lock = Arc::clone(&self.commit_lock);
2427 let _c = commit_lock.lock();
2428 let epoch = self.epoch.bump_assigned();
2429 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2430 let replaced = {
2431 let mut cat = self.catalog.write();
2432 let next = match cat
2433 .procedures
2434 .iter()
2435 .position(|p| p.procedure.name == procedure.name)
2436 {
2437 Some(idx) => {
2438 let next = cat.procedures[idx]
2439 .procedure
2440 .replaced(procedure.clone(), epoch.0)?;
2441 cat.procedures[idx] = ProcedureEntry::from(next.clone());
2442 next
2443 }
2444 None => {
2445 let mut next = procedure;
2446 next.created_epoch = epoch.0;
2447 next.updated_epoch = epoch.0;
2448 cat.procedures.push(ProcedureEntry::from(next.clone()));
2449 next
2450 }
2451 };
2452 cat.db_epoch = epoch.0;
2453 next
2454 };
2455 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2456 self.epoch.publish_in_order(epoch);
2457 _epoch_guard.disarm();
2458 Ok(replaced)
2459 }
2460
2461 pub fn drop_procedure(&self, name: &str) -> Result<()> {
2462 self.require(&crate::auth::Permission::Ddl)?;
2463 let _g = self.ddl_lock.lock();
2464 let commit_lock = Arc::clone(&self.commit_lock);
2465 let _c = commit_lock.lock();
2466 let epoch = self.epoch.bump_assigned();
2467 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2468 {
2469 let mut cat = self.catalog.write();
2470 let before = cat.procedures.len();
2471 cat.procedures.retain(|p| p.procedure.name != name);
2472 if cat.procedures.len() == before {
2473 return Err(MongrelError::NotFound(format!(
2474 "procedure {name:?} not found"
2475 )));
2476 }
2477 cat.db_epoch = epoch.0;
2478 }
2479 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2480 self.epoch.publish_in_order(epoch);
2481 _epoch_guard.disarm();
2482 Ok(())
2483 }
2484
2485 pub fn users(&self) -> Vec<crate::auth::UserEntry> {
2490 self.catalog.read().users.clone()
2491 }
2492
2493 pub fn roles(&self) -> Vec<crate::auth::RoleEntry> {
2495 self.catalog.read().roles.clone()
2496 }
2497
2498 pub fn create_user(&self, username: &str, password: &str) -> Result<crate::auth::UserEntry> {
2500 self.require(&crate::auth::Permission::Admin)?;
2501 let hash = crate::auth::hash_password(password).map_err(MongrelError::Other)?;
2502 let epoch = self.epoch.bump_assigned();
2503 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2504 let id = {
2505 let mut cat = self.catalog.write();
2506 if cat.users.iter().any(|u| u.username == username) {
2507 return Err(MongrelError::InvalidArgument(format!(
2508 "user {username:?} already exists"
2509 )));
2510 }
2511 cat.next_user_id += 1;
2512 let id = cat.next_user_id;
2513 let entry = crate::auth::UserEntry {
2514 id,
2515 username: username.into(),
2516 password_hash: hash,
2517 roles: Vec::new(),
2518 is_admin: false,
2519 created_epoch: epoch.0,
2520 };
2521 cat.users.push(entry.clone());
2522 cat.security_version = cat.security_version.wrapping_add(1);
2523 cat.db_epoch = epoch.0;
2524 entry
2525 };
2526 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2527 self.epoch.publish_in_order(epoch);
2528 _epoch_guard.disarm();
2529 Ok(id)
2530 }
2531
2532 pub fn drop_user(&self, username: &str) -> Result<()> {
2534 self.require(&crate::auth::Permission::Admin)?;
2535 let epoch = self.epoch.bump_assigned();
2536 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2537 {
2538 let mut cat = self.catalog.write();
2539 let before = cat.users.len();
2540 cat.users.retain(|u| u.username != username);
2541 if cat.users.len() == before {
2542 return Err(MongrelError::NotFound(format!(
2543 "user {username:?} not found"
2544 )));
2545 }
2546 cat.security_version = cat.security_version.wrapping_add(1);
2547 cat.db_epoch = epoch.0;
2548 }
2549 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2550 self.epoch.publish_in_order(epoch);
2551 _epoch_guard.disarm();
2552 Ok(())
2553 }
2554
2555 pub fn alter_user_password(&self, username: &str, new_password: &str) -> Result<()> {
2557 self.require(&crate::auth::Permission::Admin)?;
2558 let hash = crate::auth::hash_password(new_password).map_err(MongrelError::Other)?;
2559 let epoch = self.epoch.bump_assigned();
2560 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2561 {
2562 let mut cat = self.catalog.write();
2563 let user = cat
2564 .users
2565 .iter_mut()
2566 .find(|u| u.username == username)
2567 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
2568 user.password_hash = hash;
2569 cat.security_version = cat.security_version.wrapping_add(1);
2570 cat.db_epoch = epoch.0;
2571 }
2572 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2573 self.epoch.publish_in_order(epoch);
2574 _epoch_guard.disarm();
2575 Ok(())
2576 }
2577
2578 pub fn verify_user(
2581 &self,
2582 username: &str,
2583 password: &str,
2584 ) -> Result<Option<crate::auth::UserEntry>> {
2585 let cat = self.catalog.read();
2586 let Some(user) = cat.users.iter().find(|u| u.username == username) else {
2587 return Ok(None);
2588 };
2589 if user.password_hash.is_empty() {
2590 return Ok(None);
2591 }
2592 let ok = crate::auth::verify_password(password, &user.password_hash)
2593 .map_err(MongrelError::Other)?;
2594 if ok {
2595 Ok(Some(user.clone()))
2596 } else {
2597 Ok(None)
2598 }
2599 }
2600
2601 pub fn set_user_admin(&self, username: &str, is_admin: bool) -> Result<()> {
2603 self.require(&crate::auth::Permission::Admin)?;
2604 let epoch = self.epoch.bump_assigned();
2605 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2606 {
2607 let mut cat = self.catalog.write();
2608 let user = cat
2609 .users
2610 .iter_mut()
2611 .find(|u| u.username == username)
2612 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
2613 user.is_admin = is_admin;
2614 cat.security_version = cat.security_version.wrapping_add(1);
2615 cat.db_epoch = epoch.0;
2616 }
2617 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2618 self.epoch.publish_in_order(epoch);
2619 _epoch_guard.disarm();
2620 Ok(())
2621 }
2622
2623 pub fn create_role(&self, name: &str) -> Result<crate::auth::RoleEntry> {
2625 self.require(&crate::auth::Permission::Admin)?;
2626 let epoch = self.epoch.bump_assigned();
2627 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2628 let entry = {
2629 let mut cat = self.catalog.write();
2630 if cat.roles.iter().any(|r| r.name == name) {
2631 return Err(MongrelError::InvalidArgument(format!(
2632 "role {name:?} already exists"
2633 )));
2634 }
2635 let entry = crate::auth::RoleEntry {
2636 name: name.into(),
2637 permissions: Vec::new(),
2638 created_epoch: epoch.0,
2639 };
2640 cat.roles.push(entry.clone());
2641 cat.security_version = cat.security_version.wrapping_add(1);
2642 cat.db_epoch = epoch.0;
2643 entry
2644 };
2645 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2646 self.epoch.publish_in_order(epoch);
2647 _epoch_guard.disarm();
2648 Ok(entry)
2649 }
2650
2651 pub fn drop_role(&self, name: &str) -> Result<()> {
2653 self.require(&crate::auth::Permission::Admin)?;
2654 let epoch = self.epoch.bump_assigned();
2655 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2656 {
2657 let mut cat = self.catalog.write();
2658 let before = cat.roles.len();
2659 cat.roles.retain(|r| r.name != name);
2660 if cat.roles.len() == before {
2661 return Err(MongrelError::NotFound(format!("role {name:?} not found")));
2662 }
2663 for user in &mut cat.users {
2665 user.roles.retain(|r| r != name);
2666 }
2667 cat.security_version = cat.security_version.wrapping_add(1);
2668 cat.db_epoch = epoch.0;
2669 }
2670 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2671 self.epoch.publish_in_order(epoch);
2672 _epoch_guard.disarm();
2673 Ok(())
2674 }
2675
2676 pub fn grant_role(&self, username: &str, role_name: &str) -> Result<()> {
2678 self.require(&crate::auth::Permission::Admin)?;
2679 let epoch = self.epoch.bump_assigned();
2680 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2681 {
2682 let mut cat = self.catalog.write();
2683 if !cat.roles.iter().any(|r| r.name == role_name) {
2684 return Err(MongrelError::NotFound(format!(
2685 "role {role_name:?} not found"
2686 )));
2687 }
2688 let user = cat
2689 .users
2690 .iter_mut()
2691 .find(|u| u.username == username)
2692 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
2693 if !user.roles.contains(&role_name.to_string()) {
2694 user.roles.push(role_name.into());
2695 }
2696 cat.security_version = cat.security_version.wrapping_add(1);
2697 cat.db_epoch = epoch.0;
2698 }
2699 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2700 self.epoch.publish_in_order(epoch);
2701 _epoch_guard.disarm();
2702 Ok(())
2703 }
2704
2705 pub fn revoke_role(&self, username: &str, role_name: &str) -> Result<()> {
2707 self.require(&crate::auth::Permission::Admin)?;
2708 let epoch = self.epoch.bump_assigned();
2709 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2710 {
2711 let mut cat = self.catalog.write();
2712 let user = cat
2713 .users
2714 .iter_mut()
2715 .find(|u| u.username == username)
2716 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
2717 user.roles.retain(|r| r != role_name);
2718 cat.security_version = cat.security_version.wrapping_add(1);
2719 cat.db_epoch = epoch.0;
2720 }
2721 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2722 self.epoch.publish_in_order(epoch);
2723 _epoch_guard.disarm();
2724 Ok(())
2725 }
2726
2727 pub fn grant_permission(
2729 &self,
2730 role_name: &str,
2731 permission: crate::auth::Permission,
2732 ) -> Result<()> {
2733 self.require(&crate::auth::Permission::Admin)?;
2734 let epoch = self.epoch.bump_assigned();
2735 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2736 {
2737 let mut cat = self.catalog.write();
2738 let role = cat
2739 .roles
2740 .iter_mut()
2741 .find(|r| r.name == role_name)
2742 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
2743 merge_permission(&mut role.permissions, permission);
2744 cat.security_version = cat.security_version.wrapping_add(1);
2745 cat.db_epoch = epoch.0;
2746 }
2747 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2748 self.epoch.publish_in_order(epoch);
2749 _epoch_guard.disarm();
2750 Ok(())
2751 }
2752
2753 pub fn revoke_permission(
2755 &self,
2756 role_name: &str,
2757 permission: crate::auth::Permission,
2758 ) -> Result<()> {
2759 self.require(&crate::auth::Permission::Admin)?;
2760 let epoch = self.epoch.bump_assigned();
2761 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2762 {
2763 let mut cat = self.catalog.write();
2764 let role = cat
2765 .roles
2766 .iter_mut()
2767 .find(|r| r.name == role_name)
2768 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
2769 revoke_permission_from(&mut role.permissions, &permission);
2770 cat.security_version = cat.security_version.wrapping_add(1);
2771 cat.db_epoch = epoch.0;
2772 }
2773 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2774 self.epoch.publish_in_order(epoch);
2775 _epoch_guard.disarm();
2776 Ok(())
2777 }
2778
2779 pub fn resolve_principal(&self, username: &str) -> Option<crate::auth::Principal> {
2782 let cat = self.catalog.read();
2783 Self::resolve_principal_from_catalog(&cat, username)
2784 }
2785
2786 fn resolve_principal_from_catalog(
2791 cat: &Catalog,
2792 username: &str,
2793 ) -> Option<crate::auth::Principal> {
2794 let user = cat.users.iter().find(|u| u.username == username)?;
2795 let mut permissions = Vec::new();
2796 for role_name in &user.roles {
2797 if let Some(role) = cat.roles.iter().find(|r| &r.name == role_name) {
2798 permissions.extend(role.permissions.iter().cloned());
2799 }
2800 }
2801 Some(crate::auth::Principal {
2802 username: user.username.clone(),
2803 is_admin: user.is_admin,
2804 roles: user.roles.clone(),
2805 permissions,
2806 })
2807 }
2808
2809 pub fn check_permission(&self, username: &str, permission: &crate::auth::Permission) -> bool {
2811 match self.resolve_principal(username) {
2812 Some(p) => p.has_permission(permission),
2813 None => false,
2814 }
2815 }
2816
2817 pub fn require_auth_enabled(&self) -> bool {
2821 self.catalog.read().require_auth
2822 }
2823
2824 pub fn principal(&self) -> Option<crate::auth::Principal> {
2828 self.principal.read().clone()
2829 }
2830
2831 fn table_auth_checker(&self) -> Option<Arc<dyn crate::auth_state::TableAuthChecker>> {
2837 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
2838 self.auth_state.clone(),
2839 )))
2840 }
2841
2842 pub fn refresh_principal(&self) -> Result<()> {
2855 let username = match self.principal.read().clone() {
2856 Some(p) => p.username,
2857 None => return Ok(()),
2858 };
2859 let cat = catalog::read(&self.root, self.meta_dek.as_ref())?
2863 .ok_or_else(|| MongrelError::NotFound("catalog vanished during refresh".into()))?;
2864 *self.catalog.write() = cat.clone();
2867 match Self::resolve_principal_from_catalog(&cat, &username) {
2868 Some(p) => {
2869 *self.principal.write() = Some(p.clone());
2870 self.auth_state.set_principal(Some(p));
2874 Ok(())
2875 }
2876 None => Err(MongrelError::InvalidCredentials { username }),
2877 }
2878 }
2879
2880 pub fn enable_auth(&self, admin_username: &str, admin_password: &str) -> Result<()> {
2892 let password_hash =
2893 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
2894 let epoch = self.epoch.bump_assigned();
2895 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2896 {
2897 let mut cat = self.catalog.write();
2898 if cat.require_auth {
2899 return Err(MongrelError::InvalidArgument(
2900 "database already has require_auth enabled".into(),
2901 ));
2902 }
2903 if cat.users.iter().any(|u| u.username == admin_username) {
2906 return Err(MongrelError::InvalidArgument(format!(
2907 "user {admin_username:?} already exists"
2908 )));
2909 }
2910 cat.next_user_id = cat.next_user_id.max(1);
2911 let id = cat.next_user_id;
2912 cat.next_user_id += 1;
2913 cat.users.push(crate::auth::UserEntry {
2914 id,
2915 username: admin_username.to_string(),
2916 password_hash,
2917 roles: Vec::new(),
2918 is_admin: true,
2919 created_epoch: epoch.0,
2920 });
2921 cat.require_auth = true;
2922 cat.security_version = cat.security_version.wrapping_add(1);
2923 cat.db_epoch = epoch.0;
2924 }
2925 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2926 *self.principal.write() = Some(crate::auth::Principal {
2929 username: admin_username.to_string(),
2930 is_admin: true,
2931 roles: Vec::new(),
2932 permissions: Vec::new(),
2933 });
2934 self.auth_state.set_require_auth(true);
2935 self.epoch.publish_in_order(epoch);
2936 _epoch_guard.disarm();
2937 Ok(())
2938 }
2939
2940 pub fn disable_auth(&self) -> Result<()> {
2957 let epoch = self.epoch.bump_assigned();
2958 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2959 {
2960 let mut cat = self.catalog.write();
2961 if !cat.require_auth {
2962 return Err(MongrelError::InvalidArgument(
2963 "database does not have require_auth enabled".into(),
2964 ));
2965 }
2966 cat.require_auth = false;
2967 cat.security_version = cat.security_version.wrapping_add(1);
2968 cat.db_epoch = epoch.0;
2969 }
2970 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2971 *self.principal.write() = None;
2973 self.auth_state.set_require_auth(false);
2975 self.epoch.publish_in_order(epoch);
2976 _epoch_guard.disarm();
2977 Ok(())
2978 }
2979
2980 pub fn require(&self, perm: &crate::auth::Permission) -> Result<()> {
2987 if self.read_only && !matches!(perm, crate::auth::Permission::Select { .. }) {
2988 return Err(MongrelError::ReadOnlyReplica);
2989 }
2990 if !self.catalog.read().require_auth {
2991 return Ok(());
2992 }
2993 let guard = self.principal.read();
2994 let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
2995 if p.has_permission(perm) {
2996 Ok(())
2997 } else {
2998 Err(MongrelError::PermissionDenied {
2999 required: perm.clone(),
3000 principal: p.username.clone(),
3001 })
3002 }
3003 }
3004
3005 pub fn require_table(
3010 &self,
3011 table: &str,
3012 perm: crate::auth_state::RequiredPermission,
3013 ) -> Result<()> {
3014 self.require(&perm.into_permission(table))
3015 }
3016
3017 pub fn triggers(&self) -> Vec<StoredTrigger> {
3018 self.catalog
3019 .read()
3020 .triggers
3021 .iter()
3022 .map(|t| t.trigger.clone())
3023 .collect()
3024 }
3025
3026 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
3027 self.catalog
3028 .read()
3029 .triggers
3030 .iter()
3031 .find(|t| t.trigger.name == name)
3032 .map(|t| t.trigger.clone())
3033 }
3034
3035 pub fn create_trigger(&self, mut trigger: StoredTrigger) -> Result<StoredTrigger> {
3036 self.require(&crate::auth::Permission::Ddl)?;
3037 let _g = self.ddl_lock.lock();
3038 trigger.validate()?;
3039 self.validate_trigger_references(&trigger)?;
3040 {
3041 let cat = self.catalog.read();
3042 if cat.triggers.iter().any(|t| t.trigger.name == trigger.name) {
3043 return Err(MongrelError::InvalidArgument(format!(
3044 "trigger {:?} already exists",
3045 trigger.name
3046 )));
3047 }
3048 }
3049 let commit_lock = Arc::clone(&self.commit_lock);
3050 let _c = commit_lock.lock();
3051 let epoch = self.epoch.bump_assigned();
3052 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3053 trigger.created_epoch = epoch.0;
3054 trigger.updated_epoch = epoch.0;
3055 {
3056 let mut cat = self.catalog.write();
3057 cat.triggers.push(TriggerEntry::from(trigger.clone()));
3058 cat.db_epoch = epoch.0;
3059 }
3060 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3061 self.epoch.publish_in_order(epoch);
3062 _epoch_guard.disarm();
3063 Ok(trigger)
3064 }
3065
3066 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
3067 let _g = self.ddl_lock.lock();
3068 trigger.validate()?;
3069 self.validate_trigger_references(&trigger)?;
3070 let commit_lock = Arc::clone(&self.commit_lock);
3071 let _c = commit_lock.lock();
3072 let epoch = self.epoch.bump_assigned();
3073 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3074 let replaced = {
3075 let mut cat = self.catalog.write();
3076 let next = match cat
3077 .triggers
3078 .iter()
3079 .position(|t| t.trigger.name == trigger.name)
3080 {
3081 Some(idx) => {
3082 let next = cat.triggers[idx]
3083 .trigger
3084 .replaced(trigger.clone(), epoch.0)?;
3085 cat.triggers[idx] = TriggerEntry::from(next.clone());
3086 next
3087 }
3088 None => {
3089 let mut next = trigger;
3090 next.created_epoch = epoch.0;
3091 next.updated_epoch = epoch.0;
3092 cat.triggers.push(TriggerEntry::from(next.clone()));
3093 next
3094 }
3095 };
3096 cat.db_epoch = epoch.0;
3097 next
3098 };
3099 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3100 self.epoch.publish_in_order(epoch);
3101 _epoch_guard.disarm();
3102 Ok(replaced)
3103 }
3104
3105 pub fn drop_trigger(&self, name: &str) -> Result<()> {
3106 self.require(&crate::auth::Permission::Ddl)?;
3107 let _g = self.ddl_lock.lock();
3108 let commit_lock = Arc::clone(&self.commit_lock);
3109 let _c = commit_lock.lock();
3110 let epoch = self.epoch.bump_assigned();
3111 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3112 {
3113 let mut cat = self.catalog.write();
3114 let before = cat.triggers.len();
3115 cat.triggers.retain(|t| t.trigger.name != name);
3116 if cat.triggers.len() == before {
3117 return Err(MongrelError::NotFound(format!(
3118 "trigger {name:?} not found"
3119 )));
3120 }
3121 cat.db_epoch = epoch.0;
3122 }
3123 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3124 self.epoch.publish_in_order(epoch);
3125 _epoch_guard.disarm();
3126 Ok(())
3127 }
3128
3129 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
3130 self.catalog.read().external_tables.clone()
3131 }
3132
3133 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
3134 self.catalog
3135 .read()
3136 .external_tables
3137 .iter()
3138 .find(|t| t.name == name)
3139 .cloned()
3140 }
3141
3142 pub fn create_external_table(
3143 &self,
3144 mut entry: ExternalTableEntry,
3145 ) -> Result<ExternalTableEntry> {
3146 self.require(&crate::auth::Permission::Ddl)?;
3147 let _g = self.ddl_lock.lock();
3148 entry.validate()?;
3149 {
3150 let cat = self.catalog.read();
3151 if cat.live(&entry.name).is_some()
3152 || cat.external_tables.iter().any(|t| t.name == entry.name)
3153 {
3154 return Err(MongrelError::InvalidArgument(format!(
3155 "table {:?} already exists",
3156 entry.name
3157 )));
3158 }
3159 }
3160 let commit_lock = Arc::clone(&self.commit_lock);
3161 let _c = commit_lock.lock();
3162 let epoch = self.epoch.bump_assigned();
3163 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3164 entry.created_epoch = epoch.0;
3165 {
3166 let mut cat = self.catalog.write();
3167 cat.external_tables.push(entry.clone());
3168 cat.db_epoch = epoch.0;
3169 }
3170 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3171 self.epoch.publish_in_order(epoch);
3172 _epoch_guard.disarm();
3173 Ok(entry)
3174 }
3175
3176 pub fn drop_external_table(&self, name: &str) -> Result<()> {
3177 self.require(&crate::auth::Permission::Ddl)?;
3178 let _g = self.ddl_lock.lock();
3179 let commit_lock = Arc::clone(&self.commit_lock);
3180 let _c = commit_lock.lock();
3181 let epoch = self.epoch.bump_assigned();
3182 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3183 {
3184 let mut cat = self.catalog.write();
3185 let before = cat.external_tables.len();
3186 cat.external_tables.retain(|t| t.name != name);
3187 if cat.external_tables.len() == before {
3188 return Err(MongrelError::NotFound(format!(
3189 "external table {name:?} not found"
3190 )));
3191 }
3192 cat.db_epoch = epoch.0;
3193 }
3194 let state_dir = self.root.join(VTAB_DIR).join(name);
3195 if state_dir.exists() {
3196 std::fs::remove_dir_all(state_dir)?;
3197 }
3198 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3199 self.epoch.publish_in_order(epoch);
3200 _epoch_guard.disarm();
3201 Ok(())
3202 }
3203
3204 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
3205 let txn_id = self.alloc_txn_id();
3206 self.commit_transaction_with_external_states(
3207 txn_id,
3208 self.epoch.visible(),
3209 Vec::new(),
3210 vec![(name.to_string(), state.to_vec())],
3211 Vec::new(),
3212 None,
3213 None,
3214 )
3215 }
3216
3217 pub fn trigger_config(&self) -> TriggerConfig {
3218 use std::sync::atomic::Ordering;
3219 TriggerConfig {
3220 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
3221 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
3222 max_loop_iterations: self.trigger_max_loop_iterations.load(Ordering::Relaxed),
3223 }
3224 }
3225
3226 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
3227 use std::sync::atomic::Ordering;
3228 if config.max_depth == 0 {
3229 return Err(MongrelError::InvalidArgument(
3230 "trigger max_depth must be greater than 0".into(),
3231 ));
3232 }
3233 self.trigger_recursive
3234 .store(config.recursive_triggers, Ordering::Relaxed);
3235 self.trigger_max_depth
3236 .store(config.max_depth, Ordering::Relaxed);
3237 self.trigger_max_loop_iterations
3238 .store(config.max_loop_iterations, Ordering::Relaxed);
3239 Ok(())
3240 }
3241
3242 pub fn set_recursive_triggers(&self, recursive: bool) {
3243 use std::sync::atomic::Ordering;
3244 self.trigger_recursive.store(recursive, Ordering::Relaxed);
3245 }
3246
3247 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent> {
3251 self.notify.subscribe()
3252 }
3253
3254 pub fn subscribe_change_commits(&self) -> tokio::sync::broadcast::Receiver<()> {
3255 self.change_wake.subscribe()
3256 }
3257
3258 pub fn change_events_since(&self, last_event_id: Option<&str>) -> Result<CdcBatch> {
3263 use crate::wal::Op;
3264
3265 let resume = match last_event_id {
3266 Some(id) => {
3267 let (epoch, index) = id.split_once(':').ok_or_else(|| {
3268 MongrelError::InvalidArgument(format!(
3269 "invalid CDC event id {id:?}; expected <epoch>:<index>"
3270 ))
3271 })?;
3272 Some((
3273 epoch.parse::<u64>().map_err(|error| {
3274 MongrelError::InvalidArgument(format!("invalid CDC epoch: {error}"))
3275 })?,
3276 index.parse::<u32>().map_err(|error| {
3277 MongrelError::InvalidArgument(format!("invalid CDC index: {error}"))
3278 })?,
3279 ))
3280 }
3281 None => None,
3282 };
3283
3284 let mut wal = self.shared_wal.lock();
3285 wal.group_sync()?;
3286 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
3287 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
3288 drop(wal);
3289
3290 let commits: HashMap<u64, (u64, Vec<crate::wal::AddedRun>)> = records
3291 .iter()
3292 .filter_map(|record| match &record.op {
3293 Op::TxnCommit { epoch, added_runs } => {
3294 Some((record.txn_id, (*epoch, added_runs.clone())))
3295 }
3296 _ => None,
3297 })
3298 .collect();
3299 let earliest_epoch = commits.values().map(|(epoch, _)| *epoch).min();
3300 let current_epoch = self.epoch.visible().0;
3301 let gap = resume.is_some_and(|(epoch, _)| {
3302 epoch < current_epoch
3303 && earliest_epoch.map_or(true, |earliest| earliest > epoch.saturating_add(1))
3304 });
3305 if gap {
3306 return Ok(CdcBatch {
3307 events: Vec::new(),
3308 current_epoch,
3309 earliest_epoch,
3310 gap: true,
3311 });
3312 }
3313
3314 let table_names: HashMap<u64, String> = self
3315 .catalog
3316 .read()
3317 .tables
3318 .iter()
3319 .map(|entry| (entry.table_id, entry.name.clone()))
3320 .collect();
3321 let before_images: HashMap<(u64, u64, u64), crate::memtable::Row> = records
3322 .iter()
3323 .filter_map(|record| {
3324 if !commits.contains_key(&record.txn_id) {
3325 return None;
3326 }
3327 let Op::BeforeImage {
3328 table_id,
3329 row_id,
3330 row,
3331 } = &record.op
3332 else {
3333 return None;
3334 };
3335 bincode::deserialize(row)
3336 .ok()
3337 .map(|before| ((record.txn_id, *table_id, row_id.0), before))
3338 })
3339 .collect();
3340 let mut operation_indices: HashMap<u64, u32> = HashMap::new();
3341 let mut events = Vec::new();
3342 for record in &records {
3343 let Some((commit_epoch, _)) = commits.get(&record.txn_id) else {
3344 continue;
3345 };
3346 let event = match &record.op {
3347 Op::Put { table_id, rows } => {
3348 let rows: Vec<crate::memtable::Row> = bincode::deserialize(rows)?;
3349 let data = serde_json::to_value(rows)
3350 .map_err(|error| MongrelError::Other(format!("CDC JSON: {error}")))?;
3351 Some((*table_id, "put", data))
3352 }
3353 Op::Delete { table_id, row_ids } => {
3354 let before = row_ids
3355 .iter()
3356 .filter_map(|row_id| {
3357 before_images
3358 .get(&(record.txn_id, *table_id, row_id.0))
3359 .cloned()
3360 })
3361 .collect::<Vec<_>>();
3362 Some((
3363 *table_id,
3364 "delete",
3365 serde_json::json!({
3366 "row_ids": row_ids.iter().map(|row_id| row_id.0).collect::<Vec<_>>(),
3367 "before": before,
3368 }),
3369 ))
3370 }
3371 Op::TruncateTable { table_id } => {
3372 Some((*table_id, "truncate", serde_json::Value::Null))
3373 }
3374 _ => None,
3375 };
3376 if let Some((table_id, op, data)) = event {
3377 let index = operation_indices.entry(record.txn_id).or_insert(0);
3378 let event_position = (*commit_epoch, *index);
3379 *index = index.saturating_add(1);
3380 if resume.is_some_and(|position| event_position <= position) {
3381 continue;
3382 }
3383 events.push(ChangeEvent {
3384 id: Some(format!("{}:{}", event_position.0, event_position.1)),
3385 channel: "changes".into(),
3386 table_id: Some(table_id),
3387 table: table_names.get(&table_id).cloned().unwrap_or_default(),
3388 op: op.into(),
3389 epoch: *commit_epoch,
3390 txn_id: Some(record.txn_id),
3391 message: None,
3392 data: Some(data),
3393 });
3394 }
3395 if let Op::TxnCommit { added_runs, .. } = &record.op {
3396 for run in added_runs {
3397 let index = operation_indices.entry(record.txn_id).or_insert(0);
3398 let event_position = (*commit_epoch, *index);
3399 *index = index.saturating_add(1);
3400 if resume.is_some_and(|position| event_position <= position) {
3401 continue;
3402 }
3403 let handle = self.tables.read().get(&run.table_id).cloned();
3404 let rows = handle.and_then(|handle| {
3405 let table = handle.lock();
3406 let mut reader = table.open_reader(run.run_id).ok()?;
3407 let mut rows = reader.all_rows().ok()?;
3408 for row in &mut rows {
3409 row.committed_epoch = Epoch(*commit_epoch);
3410 }
3411 Some(rows)
3412 });
3413 let Some(rows) = rows else {
3414 return Ok(CdcBatch {
3419 events: Vec::new(),
3420 current_epoch,
3421 earliest_epoch,
3422 gap: true,
3423 });
3424 };
3425 events.push(ChangeEvent {
3426 id: Some(format!("{}:{}", event_position.0, event_position.1)),
3427 channel: "changes".into(),
3428 table_id: Some(run.table_id),
3429 table: table_names.get(&run.table_id).cloned().unwrap_or_default(),
3430 op: "put_run".into(),
3431 epoch: *commit_epoch,
3432 txn_id: Some(record.txn_id),
3433 message: None,
3434 data: Some(serde_json::json!({
3435 "run_id": run.run_id.to_string(),
3436 "row_count": run.row_count,
3437 "min_row_id": run.min_row_id,
3438 "max_row_id": run.max_row_id,
3439 "rows": rows,
3440 })),
3441 });
3442 }
3443 }
3444 }
3445 Ok(CdcBatch {
3446 events,
3447 current_epoch,
3448 earliest_epoch,
3449 gap: false,
3450 })
3451 }
3452
3453 pub fn notify(&self, channel: &str, message: Option<String>) {
3456 let _ = self.notify.send(ChangeEvent {
3457 id: None,
3458 channel: channel.to_string(),
3459 table_id: None,
3460 table: String::new(),
3461 op: "notify".into(),
3462 epoch: self.epoch.visible().0,
3463 txn_id: None,
3464 message,
3465 data: None,
3466 });
3467 }
3468
3469 pub fn call_procedure(
3470 &self,
3471 name: &str,
3472 args: HashMap<String, crate::Value>,
3473 ) -> Result<ProcedureCallResult> {
3474 self.call_procedure_as(name, args, None)
3475 }
3476
3477 pub fn call_procedure_as(
3478 &self,
3479 name: &str,
3480 args: HashMap<String, crate::Value>,
3481 principal: Option<&crate::auth::Principal>,
3482 ) -> Result<ProcedureCallResult> {
3483 self.require_for(principal, &crate::auth::Permission::All)?;
3487 let procedure = self
3488 .procedure(name)
3489 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
3490 let args = bind_procedure_args(&procedure, args)?;
3491 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
3492 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
3493 if has_writes {
3494 let mut tx = self.begin_as(principal.cloned());
3495 let run = (|| {
3496 for step in &procedure.body.steps {
3497 let output = self.execute_procedure_step(
3498 step,
3499 &args,
3500 &outputs,
3501 Some(&mut tx),
3502 principal,
3503 )?;
3504 outputs.insert(step.id().to_string(), output);
3505 }
3506 eval_return_output(&procedure.body.return_value, &args, &outputs)
3507 })();
3508 match run {
3509 Ok(output) => {
3510 let epoch = tx.commit()?.0;
3511 Ok(ProcedureCallResult {
3512 epoch: Some(epoch),
3513 output,
3514 })
3515 }
3516 Err(e) => {
3517 tx.rollback();
3518 Err(e)
3519 }
3520 }
3521 } else {
3522 for step in &procedure.body.steps {
3523 let output = self.execute_procedure_step(step, &args, &outputs, None, principal)?;
3524 outputs.insert(step.id().to_string(), output);
3525 }
3526 Ok(ProcedureCallResult {
3527 epoch: None,
3528 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
3529 })
3530 }
3531 }
3532
3533 fn execute_procedure_step(
3534 &self,
3535 step: &ProcedureStep,
3536 args: &HashMap<String, crate::Value>,
3537 outputs: &HashMap<String, ProcedureCallOutput>,
3538 tx: Option<&mut crate::txn::Transaction<'_>>,
3539 principal: Option<&crate::auth::Principal>,
3540 ) -> Result<ProcedureCallOutput> {
3541 match step {
3542 ProcedureStep::NativeQuery {
3543 table,
3544 conditions,
3545 projection,
3546 limit,
3547 ..
3548 } => {
3549 let mut q = crate::Query::new();
3550 for condition in conditions {
3551 q = q.and(eval_condition(condition, args, outputs)?);
3552 }
3553 let handle = self.table(table)?;
3554 let rows = handle.lock().query(&q)?;
3555 let mut rows = self.secure_rows_for(table, rows, principal)?;
3556 if let Some(limit) = limit {
3557 rows.truncate(*limit);
3558 }
3559 let projection = projection.as_ref();
3560 Ok(ProcedureCallOutput::Rows(
3561 rows.into_iter()
3562 .map(|row| ProcedureCallRow {
3563 row_id: Some(row.row_id),
3564 columns: match projection {
3565 Some(ids) => row
3566 .columns
3567 .into_iter()
3568 .filter(|(id, _)| ids.contains(id))
3569 .collect(),
3570 None => row.columns,
3571 },
3572 })
3573 .collect(),
3574 ))
3575 }
3576 ProcedureStep::Put {
3577 table,
3578 cells,
3579 returning,
3580 ..
3581 } => {
3582 let tx = tx.ok_or_else(|| {
3583 MongrelError::InvalidArgument(
3584 "write procedure step requires a transaction".into(),
3585 )
3586 })?;
3587 let cells = eval_cells(cells, args, outputs)?;
3588 if *returning {
3589 let out = tx.put_returning(table, cells)?;
3590 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
3591 row_id: None,
3592 columns: out.row.columns.into_iter().collect(),
3593 }))
3594 } else {
3595 tx.put(table, cells)?;
3596 Ok(ProcedureCallOutput::Null)
3597 }
3598 }
3599 ProcedureStep::Upsert {
3600 table,
3601 cells,
3602 update_cells,
3603 returning,
3604 ..
3605 } => {
3606 let tx = tx.ok_or_else(|| {
3607 MongrelError::InvalidArgument(
3608 "write procedure step requires a transaction".into(),
3609 )
3610 })?;
3611 let cells = eval_cells(cells, args, outputs)?;
3612 let action = match update_cells {
3613 Some(update_cells) => {
3614 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
3615 }
3616 None => crate::UpsertAction::DoNothing,
3617 };
3618 let out = tx.upsert(table, cells, action)?;
3619 if *returning {
3620 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
3621 row_id: None,
3622 columns: out.row.columns.into_iter().collect(),
3623 }))
3624 } else {
3625 Ok(ProcedureCallOutput::Null)
3626 }
3627 }
3628 ProcedureStep::DeleteByPk { table, pk, .. } => {
3629 let tx = tx.ok_or_else(|| {
3630 MongrelError::InvalidArgument(
3631 "write procedure step requires a transaction".into(),
3632 )
3633 })?;
3634 let pk = eval_value(pk, args, outputs)?;
3635 let handle = self.table(table)?;
3636 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
3637 MongrelError::NotFound("procedure delete_by_pk target not found".into())
3638 })?;
3639 tx.delete(table, row_id)?;
3640 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
3641 }
3642 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
3643 "DeleteRows procedure step is not supported by the core executor yet".into(),
3644 )),
3645 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
3646 "SqlQuery procedure step must be executed by mongreldb-query".into(),
3647 )),
3648 }
3649 }
3650
3651 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
3652 let cat = self.catalog.read();
3653 for step in &procedure.body.steps {
3654 let Some(table_name) = step.table() else {
3655 continue;
3656 };
3657 let schema = &cat
3658 .live(table_name)
3659 .ok_or_else(|| {
3660 MongrelError::InvalidArgument(format!(
3661 "procedure {:?} references unknown table {table_name:?}",
3662 procedure.name
3663 ))
3664 })?
3665 .schema;
3666 match step {
3667 ProcedureStep::NativeQuery {
3668 conditions,
3669 projection,
3670 ..
3671 } => {
3672 for condition in conditions {
3673 validate_condition_columns(condition, schema)?;
3674 }
3675 if let Some(projection) = projection {
3676 for id in projection {
3677 validate_column_id(*id, schema)?;
3678 }
3679 }
3680 }
3681 ProcedureStep::Put { cells, .. } => {
3682 for cell in cells {
3683 validate_column_id(cell.column_id, schema)?;
3684 }
3685 }
3686 ProcedureStep::Upsert {
3687 cells,
3688 update_cells,
3689 ..
3690 } => {
3691 for cell in cells {
3692 validate_column_id(cell.column_id, schema)?;
3693 }
3694 if let Some(update_cells) = update_cells {
3695 for cell in update_cells {
3696 validate_column_id(cell.column_id, schema)?;
3697 }
3698 }
3699 }
3700 ProcedureStep::DeleteByPk { .. } => {
3701 if schema.primary_key().is_none() {
3702 return Err(MongrelError::InvalidArgument(format!(
3703 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
3704 procedure.name
3705 )));
3706 }
3707 }
3708 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
3709 }
3710 }
3711 Ok(())
3712 }
3713
3714 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
3715 let cat = self.catalog.read();
3716 let target_schema = match &trigger.target {
3717 TriggerTarget::Table(target_name) => cat
3718 .live(target_name)
3719 .ok_or_else(|| {
3720 MongrelError::InvalidArgument(format!(
3721 "trigger {:?} references unknown target table {target_name:?}",
3722 trigger.name
3723 ))
3724 })?
3725 .schema
3726 .clone(),
3727 TriggerTarget::View(_) => Schema {
3728 columns: trigger.target_columns.clone(),
3729 ..Schema::default()
3730 },
3731 };
3732 for col in &trigger.update_of {
3733 if target_schema.column(col).is_none() {
3734 return Err(MongrelError::InvalidArgument(format!(
3735 "trigger {:?} UPDATE OF references unknown column {col:?}",
3736 trigger.name
3737 )));
3738 }
3739 }
3740 if let Some(expr) = &trigger.when {
3741 validate_trigger_expr(expr, &target_schema, trigger.event)?;
3742 }
3743 let mut select_schemas: HashMap<String, &Schema> = HashMap::new();
3744 for step in &trigger.program.steps {
3745 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
3746 {
3747 return Err(MongrelError::InvalidArgument(
3748 "SetNew trigger steps are only valid in BEFORE triggers".into(),
3749 ));
3750 }
3751 validate_trigger_step(
3752 step,
3753 &cat,
3754 &target_schema,
3755 trigger.event,
3756 &mut select_schemas,
3757 )?;
3758 }
3759 Ok(())
3760 }
3761
3762 pub fn begin(&self) -> crate::txn::Transaction<'_> {
3764 self.begin_with_isolation(crate::txn::IsolationLevel::default())
3765 }
3766
3767 pub fn begin_as(
3768 &self,
3769 principal: Option<crate::auth::Principal>,
3770 ) -> crate::txn::Transaction<'_> {
3771 let txn_id = self.alloc_txn_id();
3772 let read = Snapshot::at(self.epoch.visible());
3773 crate::txn::Transaction::new(self, txn_id, read).with_principal(principal)
3774 }
3775
3776 pub fn begin_with_isolation(
3778 &self,
3779 level: crate::txn::IsolationLevel,
3780 ) -> crate::txn::Transaction<'_> {
3781 let txn_id = self.alloc_txn_id();
3782 let epoch = match level {
3783 crate::txn::IsolationLevel::ReadCommitted => self.epoch.visible(),
3784 _ => self.epoch.visible(),
3785 };
3786 let read = Snapshot::at(epoch);
3787 crate::txn::Transaction::new(self, txn_id, read)
3788 }
3789
3790 pub fn begin_with_external_trigger_bridge<'a>(
3793 &'a self,
3794 bridge: &'a dyn ExternalTriggerBridge,
3795 ) -> crate::txn::Transaction<'a> {
3796 let txn_id = self.alloc_txn_id();
3797 let read = Snapshot::at(self.epoch.visible());
3798 crate::txn::Transaction::new(self, txn_id, read).with_external_trigger_bridge(bridge)
3799 }
3800
3801 pub fn begin_with_external_trigger_bridge_as<'a>(
3802 &'a self,
3803 bridge: &'a dyn ExternalTriggerBridge,
3804 principal: Option<crate::auth::Principal>,
3805 ) -> crate::txn::Transaction<'a> {
3806 let txn_id = self.alloc_txn_id();
3807 let read = Snapshot::at(self.epoch.visible());
3808 crate::txn::Transaction::new(self, txn_id, read)
3809 .with_external_trigger_bridge(bridge)
3810 .with_principal(principal)
3811 }
3812
3813 pub fn transaction<T>(
3815 &self,
3816 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
3817 ) -> Result<T> {
3818 let mut tx = self.begin();
3819 match f(&mut tx) {
3820 Ok(out) => {
3821 tx.commit()?;
3822 Ok(out)
3823 }
3824 Err(e) => {
3825 tx.rollback();
3826 Err(e)
3827 }
3828 }
3829 }
3830
3831 pub fn transaction_with_external_trigger_bridge<'a, T>(
3834 &'a self,
3835 bridge: &'a dyn ExternalTriggerBridge,
3836 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
3837 ) -> Result<T> {
3838 let mut tx = self.begin_with_external_trigger_bridge(bridge);
3839 match f(&mut tx) {
3840 Ok(out) => {
3841 tx.commit()?;
3842 Ok(out)
3843 }
3844 Err(e) => {
3845 tx.rollback();
3846 Err(e)
3847 }
3848 }
3849 }
3850
3851 pub fn transaction_with_external_trigger_bridge_as<'a, T>(
3852 &'a self,
3853 bridge: &'a dyn ExternalTriggerBridge,
3854 principal: Option<crate::auth::Principal>,
3855 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
3856 ) -> Result<T> {
3857 let mut tx = self.begin_with_external_trigger_bridge_as(bridge, principal);
3858 match f(&mut tx) {
3859 Ok(output) => {
3860 tx.commit()?;
3861 Ok(output)
3862 }
3863 Err(error) => {
3864 tx.rollback();
3865 Err(error)
3866 }
3867 }
3868 }
3869
3870 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
3873 self.active_txns.register(epoch)
3874 }
3875
3876 fn fill_auto_increment_for_staging(
3877 &self,
3878 staging: &mut [(u64, crate::txn::Staged)],
3879 ) -> Result<()> {
3880 let tables = self.tables.read();
3881 for (table_id, staged) in staging {
3882 if let crate::txn::Staged::Put(cells) = staged {
3883 if let Some(handle) = tables.get(table_id) {
3884 let mut t = handle.lock();
3885 t.fill_auto_inc(cells)?;
3886 }
3887 }
3888 }
3889 Ok(())
3890 }
3891
3892 fn expand_table_triggers(
3893 &self,
3894 staging: &mut Vec<(u64, crate::txn::Staged)>,
3895 read_epoch: Epoch,
3896 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
3897 external_states: &mut Vec<(String, Vec<u8>)>,
3898 ) -> Result<()> {
3899 let mut external_writes = Vec::new();
3900 let config = self.trigger_config();
3901 if config.recursive_triggers {
3902 let chunk = std::mem::take(staging);
3903 let stacks = vec![Vec::new(); chunk.len()];
3904 *staging = self.expand_trigger_chunk(
3905 chunk,
3906 stacks,
3907 read_epoch,
3908 0,
3909 config.max_depth,
3910 &mut external_writes,
3911 &config,
3912 )?;
3913 self.apply_external_trigger_writes(
3914 external_writes,
3915 external_trigger_bridge,
3916 external_states,
3917 staging,
3918 )?;
3919 return Ok(());
3920 }
3921
3922 let mut expansion = self.expand_table_triggers_once(staging, read_epoch, None, &config)?;
3923 if !expansion.before.is_empty() {
3924 let mut final_staging = expansion.before;
3925 final_staging.extend(filter_ignored_staging(
3926 std::mem::take(staging),
3927 &expansion.ignored_indices,
3928 ));
3929 *staging = final_staging;
3930 } else if !expansion.ignored_indices.is_empty() {
3931 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
3932 }
3933 staging.append(&mut expansion.after);
3934 external_writes.append(&mut expansion.before_external);
3935 external_writes.append(&mut expansion.after_external);
3936 self.apply_external_trigger_writes(
3937 external_writes,
3938 external_trigger_bridge,
3939 external_states,
3940 staging,
3941 )?;
3942 Ok(())
3943 }
3944
3945 #[allow(clippy::too_many_arguments)]
3946 fn expand_trigger_chunk(
3947 &self,
3948 mut chunk: Vec<(u64, crate::txn::Staged)>,
3949 stacks: Vec<Vec<String>>,
3950 read_epoch: Epoch,
3951 depth: u32,
3952 max_depth: u32,
3953 external_writes: &mut Vec<ExternalTriggerWrite>,
3954 config: &TriggerConfig,
3955 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
3956 if chunk.is_empty() {
3957 return Ok(Vec::new());
3958 }
3959 self.fill_auto_increment_for_staging(&mut chunk)?;
3960 let expansion =
3961 self.expand_table_triggers_once(&mut chunk, read_epoch, Some(&stacks), config)?;
3962 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
3963 let stack = expansion
3964 .before_stacks
3965 .first()
3966 .or_else(|| expansion.after_stacks.first())
3967 .cloned()
3968 .unwrap_or_default();
3969 return Err(MongrelError::Conflict(format!(
3970 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
3971 Self::format_trigger_stack(&stack)
3972 )));
3973 }
3974
3975 let mut out = Vec::new();
3976 external_writes.extend(expansion.before_external);
3977 out.extend(self.expand_trigger_chunk(
3978 expansion.before,
3979 expansion.before_stacks,
3980 read_epoch,
3981 depth + 1,
3982 max_depth,
3983 external_writes,
3984 config,
3985 )?);
3986 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
3987 external_writes.extend(expansion.after_external);
3988 out.extend(self.expand_trigger_chunk(
3989 expansion.after,
3990 expansion.after_stacks,
3991 read_epoch,
3992 depth + 1,
3993 max_depth,
3994 external_writes,
3995 config,
3996 )?);
3997 Ok(out)
3998 }
3999
4000 fn apply_external_trigger_writes(
4001 &self,
4002 writes: Vec<ExternalTriggerWrite>,
4003 bridge: Option<&dyn ExternalTriggerBridge>,
4004 external_states: &mut Vec<(String, Vec<u8>)>,
4005 staging: &mut Vec<(u64, crate::txn::Staged)>,
4006 ) -> Result<()> {
4007 if writes.is_empty() {
4008 return Ok(());
4009 }
4010 let bridge = bridge.ok_or_else(|| {
4011 MongrelError::InvalidArgument(
4012 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
4013 )
4014 })?;
4015 for write in writes {
4016 let table = write.table().to_string();
4017 let entry = self.external_table(&table).ok_or_else(|| {
4018 MongrelError::NotFound(format!("external table {table:?} not found"))
4019 })?;
4020 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
4021 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
4022 external_states.push((table, result.state));
4023 for base_write in result.base_writes {
4024 match base_write {
4025 ExternalTriggerBaseWrite::Put { table, cells } => {
4026 let table_id = self.table_id(&table)?;
4027 staging.push((table_id, crate::txn::Staged::Put(cells)));
4028 }
4029 ExternalTriggerBaseWrite::Delete { table, row_id } => {
4030 let table_id = self.table_id(&table)?;
4031 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
4032 }
4033 }
4034 }
4035 }
4036 dedup_external_states_in_place(external_states);
4037 Ok(())
4038 }
4039
4040 fn expand_table_triggers_once(
4041 &self,
4042 staging: &mut Vec<(u64, crate::txn::Staged)>,
4043 read_epoch: Epoch,
4044 trigger_stacks: Option<&[Vec<String>]>,
4045 config: &TriggerConfig,
4046 ) -> Result<TriggerExpansion> {
4047 let triggers: Vec<StoredTrigger> = self
4048 .catalog
4049 .read()
4050 .triggers
4051 .iter()
4052 .filter(|entry| {
4053 entry.trigger.enabled
4054 && matches!(
4055 entry.trigger.timing,
4056 TriggerTiming::Before | TriggerTiming::After
4057 )
4058 && matches!(entry.trigger.target, TriggerTarget::Table(_))
4059 })
4060 .map(|entry| entry.trigger.clone())
4061 .collect();
4062 if triggers.is_empty() || staging.is_empty() {
4063 return Ok(TriggerExpansion::default());
4064 }
4065
4066 let before_triggers = triggers
4067 .iter()
4068 .filter(|trigger| trigger.timing == TriggerTiming::Before)
4069 .cloned()
4070 .collect::<Vec<_>>();
4071 let after_triggers = triggers
4072 .iter()
4073 .filter(|trigger| trigger.timing == TriggerTiming::After)
4074 .cloned()
4075 .collect::<Vec<_>>();
4076
4077 let mut before_added = Vec::new();
4078 let mut before_stacks = Vec::new();
4079 let mut before_external = Vec::new();
4080 let mut ignored_indices = std::collections::BTreeSet::new();
4081 if !before_triggers.is_empty() {
4082 let before_events =
4083 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks)?;
4084 let mut out = TriggerProgramOutput {
4085 added: &mut before_added,
4086 added_stacks: &mut before_stacks,
4087 added_external: &mut before_external,
4088 ignored_indices: &mut ignored_indices,
4089 };
4090 self.execute_triggers_for_events(
4091 &before_triggers,
4092 &before_events,
4093 Some(staging),
4094 &mut out,
4095 config,
4096 read_epoch,
4097 )?;
4098 }
4099
4100 let after_events = if after_triggers.is_empty() {
4101 Vec::new()
4102 } else {
4103 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks)?
4104 .into_iter()
4105 .filter(|event| {
4106 !event
4107 .op_indices
4108 .iter()
4109 .any(|idx| ignored_indices.contains(idx))
4110 })
4111 .collect()
4112 };
4113
4114 let mut after_added = Vec::new();
4115 let mut after_stacks = Vec::new();
4116 let mut after_external = Vec::new();
4117 let mut out = TriggerProgramOutput {
4118 added: &mut after_added,
4119 added_stacks: &mut after_stacks,
4120 added_external: &mut after_external,
4121 ignored_indices: &mut ignored_indices,
4122 };
4123 self.execute_triggers_for_events(
4124 &after_triggers,
4125 &after_events,
4126 None,
4127 &mut out,
4128 config,
4129 read_epoch,
4130 )?;
4131 Ok(TriggerExpansion {
4132 before: before_added,
4133 before_stacks,
4134 before_external,
4135 after: after_added,
4136 after_stacks,
4137 after_external,
4138 ignored_indices,
4139 })
4140 }
4141
4142 fn execute_triggers_for_events(
4143 &self,
4144 triggers: &[StoredTrigger],
4145 events: &[WriteEvent],
4146 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
4147 out: &mut TriggerProgramOutput<'_>,
4148 config: &TriggerConfig,
4149 read_epoch: Epoch,
4150 ) -> Result<()> {
4151 for event in events {
4152 for trigger in triggers {
4153 if event
4154 .op_indices
4155 .iter()
4156 .any(|idx| out.ignored_indices.contains(idx))
4157 {
4158 break;
4159 }
4160 let matches = {
4161 let cat = self.catalog.read();
4162 trigger_matches_event(trigger, event, &cat)?
4163 };
4164 if !matches {
4165 continue;
4166 }
4167 if let Some(when) = &trigger.when {
4168 if !eval_trigger_expr(when, event)? {
4169 continue;
4170 }
4171 }
4172 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
4173 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
4174 return Err(MongrelError::Conflict(format!(
4175 "trigger recursion cycle detected; trigger stack: {}",
4176 Self::format_trigger_stack(&trigger_stack)
4177 )));
4178 }
4179 let outcome = match staging.as_mut() {
4180 Some(staging) => self.execute_trigger_program(
4181 trigger,
4182 event,
4183 Some(&mut **staging),
4184 out,
4185 &trigger_stack,
4186 config,
4187 read_epoch,
4188 )?,
4189 None => self.execute_trigger_program(
4190 trigger,
4191 event,
4192 None,
4193 out,
4194 &trigger_stack,
4195 config,
4196 read_epoch,
4197 )?,
4198 };
4199 if outcome == TriggerProgramOutcome::Ignore {
4200 out.ignored_indices.extend(event.op_indices.iter().copied());
4201 break;
4202 }
4203 }
4204 }
4205 Ok(())
4206 }
4207
4208 fn trigger_events_for_staging(
4209 &self,
4210 staging: &[(u64, crate::txn::Staged)],
4211 read_epoch: Epoch,
4212 trigger_stacks: Option<&[Vec<String>]>,
4213 ) -> Result<Vec<WriteEvent>> {
4214 use crate::txn::Staged;
4215 use std::collections::{HashMap, VecDeque};
4216
4217 let snapshot = Snapshot::at(read_epoch);
4218 let cat = self.catalog.read();
4219 let mut table_names = HashMap::new();
4220 let mut table_schemas = HashMap::new();
4221 for entry in cat
4222 .tables
4223 .iter()
4224 .filter(|entry| matches!(entry.state, TableState::Live))
4225 {
4226 table_names.insert(entry.table_id, entry.name.clone());
4227 table_schemas.insert(entry.table_id, entry.schema.clone());
4228 }
4229 drop(cat);
4230
4231 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
4232 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
4233 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
4234
4235 for (idx, (table_id, staged)) in staging.iter().enumerate() {
4236 let Some(schema) = table_schemas.get(table_id) else {
4237 continue;
4238 };
4239 let Some(pk) = schema.primary_key() else {
4240 continue;
4241 };
4242 match staged {
4243 Staged::Delete(row_id) => {
4244 let handle = self.table_by_id(*table_id)?;
4245 let Some(row) = handle.lock().get(*row_id, snapshot) else {
4246 continue;
4247 };
4248 let Some(pk_value) = row.columns.get(&pk.id) else {
4249 continue;
4250 };
4251 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
4252 delete_by_key
4253 .entry((*table_id, pk_value.encode_key()))
4254 .or_default()
4255 .push_back(idx);
4256 }
4257 Staged::Put(cells) => {
4258 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
4259 put_by_key
4260 .entry((*table_id, value.encode_key()))
4261 .or_default()
4262 .push_back(idx);
4263 }
4264 }
4265 Staged::Update(row_id, _) => {
4266 let handle = self.table_by_id(*table_id)?;
4267 let row = handle.lock().get(*row_id, snapshot);
4268 if let Some(row) = row {
4269 old_rows.insert(idx, TriggerRowImage::from_row(row));
4270 }
4271 }
4272 Staged::Truncate => {}
4273 }
4274 }
4275
4276 let mut paired_delete = std::collections::HashSet::new();
4277 let mut paired_put = std::collections::HashSet::new();
4278 let mut events = Vec::new();
4279
4280 for (key, deletes) in delete_by_key.iter_mut() {
4281 let Some(puts) = put_by_key.get_mut(key) else {
4282 continue;
4283 };
4284 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
4285 paired_delete.insert(delete_idx);
4286 paired_put.insert(put_idx);
4287 let (table_id, _) = &staging[put_idx];
4288 let Some(table_name) = table_names.get(table_id).cloned() else {
4289 continue;
4290 };
4291 let old = old_rows.get(&delete_idx).cloned();
4292 let new = match &staging[put_idx].1 {
4293 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
4294 _ => None,
4295 };
4296 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
4297 events.push(WriteEvent {
4298 table: table_name,
4299 kind: TriggerEvent::Update,
4300 old,
4301 new,
4302 changed_columns,
4303 op_indices: vec![delete_idx, put_idx],
4304 put_idx: Some(put_idx),
4305 trigger_stack: Self::trigger_stack_for_indices(
4306 trigger_stacks,
4307 &[delete_idx, put_idx],
4308 ),
4309 });
4310 }
4311 }
4312
4313 for (idx, (table_id, staged)) in staging.iter().enumerate() {
4314 let Some(table_name) = table_names.get(table_id).cloned() else {
4315 continue;
4316 };
4317 match staged {
4318 Staged::Put(cells) if !paired_put.contains(&idx) => {
4319 let new = Some(TriggerRowImage::from_cells(cells));
4320 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
4321 events.push(WriteEvent {
4322 table: table_name,
4323 kind: TriggerEvent::Insert,
4324 old: None,
4325 new,
4326 changed_columns,
4327 op_indices: vec![idx],
4328 put_idx: Some(idx),
4329 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
4330 });
4331 }
4332 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
4333 let old = match old_rows.get(&idx).cloned() {
4334 Some(old) => Some(old),
4335 None => {
4336 let handle = self.table_by_id(*table_id)?;
4337 let row = handle.lock().get(*row_id, snapshot);
4338 row.map(TriggerRowImage::from_row)
4339 }
4340 };
4341 let Some(old) = old else {
4342 continue;
4343 };
4344 let changed_columns = old.columns.keys().copied().collect();
4345 events.push(WriteEvent {
4346 table: table_name,
4347 kind: TriggerEvent::Delete,
4348 old: Some(old),
4349 new: None,
4350 changed_columns,
4351 op_indices: vec![idx],
4352 put_idx: None,
4353 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
4354 });
4355 }
4356 Staged::Update(_, cells) => {
4357 let old = old_rows.get(&idx).cloned();
4358 let new = Some(TriggerRowImage::from_cells(cells));
4359 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
4360 events.push(WriteEvent {
4361 table: table_name,
4362 kind: TriggerEvent::Update,
4363 old,
4364 new,
4365 changed_columns,
4366 op_indices: vec![idx],
4367 put_idx: Some(idx),
4368 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
4369 });
4370 }
4371 Staged::Truncate => {}
4372 _ => {}
4373 }
4374 }
4375
4376 Ok(events)
4377 }
4378
4379 #[allow(clippy::too_many_arguments)]
4380 fn execute_trigger_program(
4381 &self,
4382 trigger: &StoredTrigger,
4383 event: &WriteEvent,
4384 staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
4385 out: &mut TriggerProgramOutput<'_>,
4386 trigger_stack: &[String],
4387 config: &TriggerConfig,
4388 read_epoch: Epoch,
4389 ) -> Result<TriggerProgramOutcome> {
4390 let mut event = event.clone();
4391 let mut select_results: HashMap<String, Vec<TriggerRowImage>> = HashMap::new();
4392 self.execute_trigger_steps(
4393 trigger,
4394 &trigger.program.steps,
4395 &mut event,
4396 staging,
4397 out,
4398 trigger_stack,
4399 config,
4400 &mut select_results,
4401 0,
4402 None,
4403 read_epoch,
4404 )
4405 }
4406
4407 #[allow(clippy::too_many_arguments)]
4408 fn execute_trigger_steps(
4409 &self,
4410 trigger: &StoredTrigger,
4411 steps: &[TriggerStep],
4412 event: &mut WriteEvent,
4413 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
4414 out: &mut TriggerProgramOutput<'_>,
4415 trigger_stack: &[String],
4416 config: &TriggerConfig,
4417 select_results: &mut HashMap<String, Vec<TriggerRowImage>>,
4418 depth: u32,
4419 selected: Option<&TriggerRowImage>,
4420 read_epoch: Epoch,
4421 ) -> Result<TriggerProgramOutcome> {
4422 let _ = depth;
4423 for step in steps {
4424 match step {
4425 TriggerStep::SetNew { cells } => {
4426 if trigger.timing != TriggerTiming::Before {
4427 return Err(MongrelError::InvalidArgument(
4428 "SetNew trigger step is only valid in BEFORE triggers".into(),
4429 ));
4430 }
4431 let put_idx = event.put_idx.ok_or_else(|| {
4432 MongrelError::InvalidArgument(
4433 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
4434 )
4435 })?;
4436 let staging = staging.as_deref_mut().ok_or_else(|| {
4437 MongrelError::InvalidArgument(
4438 "SetNew trigger step requires mutable trigger staging".into(),
4439 )
4440 })?;
4441 let row_cells = match staging.get_mut(put_idx).map(|(_, op)| op) {
4442 Some(crate::txn::Staged::Put(cells))
4443 | Some(crate::txn::Staged::Update(_, cells)) => cells,
4444 _ => {
4445 return Err(MongrelError::InvalidArgument(
4446 "SetNew trigger step target row is not mutable".into(),
4447 ))
4448 }
4449 };
4450 for (column_id, value) in eval_trigger_cells(cells, event, selected)? {
4451 row_cells.retain(|(id, _)| *id != column_id);
4452 row_cells.push((column_id, value.clone()));
4453 if let Some(new) = &mut event.new {
4454 new.columns.insert(column_id, value);
4455 }
4456 }
4457 row_cells.sort_by_key(|(id, _)| *id);
4458 }
4459 TriggerStep::Insert { table, cells } => {
4460 let cells = eval_trigger_cells(cells, event, selected)?;
4461 if let Ok(table_id) = self.table_id(table) {
4462 out.added.push((table_id, crate::txn::Staged::Put(cells)));
4463 out.added_stacks.push(trigger_stack.to_vec());
4464 } else if self.external_table(table).is_some() {
4465 out.added_external.push(ExternalTriggerWrite::Insert {
4466 table: table.clone(),
4467 cells,
4468 });
4469 } else {
4470 return Err(MongrelError::NotFound(format!(
4471 "trigger {:?} insert target {table:?} not found",
4472 trigger.name
4473 )));
4474 }
4475 }
4476 TriggerStep::UpdateByPk { table, pk, cells } => {
4477 let pk = eval_trigger_value(pk, event, selected)?;
4478 let cells = eval_trigger_cells(cells, event, selected)?;
4479 if self.external_table(table).is_some() {
4480 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
4481 table: table.clone(),
4482 pk,
4483 cells,
4484 });
4485 } else {
4486 let row_id = self
4487 .table(table)?
4488 .lock()
4489 .lookup_pk(&pk.encode_key())
4490 .ok_or_else(|| {
4491 MongrelError::NotFound(format!(
4492 "trigger {:?} update target not found",
4493 trigger.name
4494 ))
4495 })?;
4496 let handle = self.table(table)?;
4497 let snapshot = Snapshot::at(self.epoch.visible());
4498 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
4499 MongrelError::NotFound(format!(
4500 "trigger {:?} update target not visible",
4501 trigger.name
4502 ))
4503 })?;
4504 let mut merged = old.columns;
4505 for (column_id, value) in cells {
4506 merged.insert(column_id, value);
4507 }
4508 out.added.push((
4509 self.table_id(table)?,
4510 crate::txn::Staged::Update(row_id, merged.into_iter().collect()),
4511 ));
4512 out.added_stacks.push(trigger_stack.to_vec());
4513 }
4514 }
4515 TriggerStep::DeleteByPk { table, pk } => {
4516 let pk = eval_trigger_value(pk, event, selected)?;
4517 if self.external_table(table).is_some() {
4518 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
4519 table: table.clone(),
4520 pk,
4521 });
4522 } else {
4523 let row_id = self
4524 .table(table)?
4525 .lock()
4526 .lookup_pk(&pk.encode_key())
4527 .ok_or_else(|| {
4528 MongrelError::NotFound(format!(
4529 "trigger {:?} delete target not found",
4530 trigger.name
4531 ))
4532 })?;
4533 out.added
4534 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
4535 out.added_stacks.push(trigger_stack.to_vec());
4536 }
4537 }
4538 TriggerStep::Select {
4539 id,
4540 table,
4541 conditions,
4542 } => {
4543 let schema = self.table(table)?.lock().schema().clone();
4544 let snapshot = Snapshot::at(read_epoch);
4545 let rows = self.table(table)?.lock().visible_rows(snapshot)?;
4546 let mut matched = Vec::new();
4547 for row in rows {
4548 let image = TriggerRowImage::from_row(row);
4549 let passes = conditions
4550 .iter()
4551 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
4552 .collect::<Result<Vec<_>>>()?
4553 .into_iter()
4554 .all(|b| b);
4555 if passes {
4556 matched.push(image);
4557 }
4558 }
4559 if let Some(pk) = schema.primary_key() {
4560 matched.sort_by(|a, b| {
4561 let av = a.columns.get(&pk.id).unwrap_or(&Value::Null);
4562 let bv = b.columns.get(&pk.id).unwrap_or(&Value::Null);
4563 value_order(av, bv).unwrap_or(std::cmp::Ordering::Equal)
4564 });
4565 }
4566 select_results.insert(id.clone(), matched);
4567 }
4568 TriggerStep::Foreach { id, steps } => {
4569 let rows = select_results.get(id).ok_or_else(|| {
4570 MongrelError::InvalidArgument(format!(
4571 "trigger {:?} foreach references unknown select id {id:?}",
4572 trigger.name
4573 ))
4574 })?;
4575 if rows.len() > config.max_loop_iterations as usize {
4576 return Err(MongrelError::InvalidArgument(format!(
4577 "trigger {:?} foreach exceeded max_loop_iterations ({})",
4578 trigger.name, config.max_loop_iterations
4579 )));
4580 }
4581 for row in rows.clone() {
4582 let result = self.execute_trigger_steps(
4583 trigger,
4584 steps,
4585 event,
4586 staging.as_deref_mut(),
4587 out,
4588 trigger_stack,
4589 config,
4590 select_results,
4591 depth + 1,
4592 Some(&row),
4593 read_epoch,
4594 )?;
4595 if result == TriggerProgramOutcome::Ignore {
4596 return Ok(TriggerProgramOutcome::Ignore);
4597 }
4598 }
4599 }
4600 TriggerStep::DeleteWhere { table, conditions } => {
4601 let schema = self.table(table)?.lock().schema().clone();
4602 let snapshot = Snapshot::at(read_epoch);
4603 let rows = self.table(table)?.lock().visible_rows(snapshot)?;
4604 let table_id = self.table_id(table)?;
4605 let mut to_delete = Vec::new();
4606 for row in rows {
4607 let image = TriggerRowImage::from_row(row.clone());
4608 let passes = conditions
4609 .iter()
4610 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
4611 .collect::<Result<Vec<_>>>()?
4612 .into_iter()
4613 .all(|b| b);
4614 if passes {
4615 to_delete.push((table_id, row.row_id));
4616 }
4617 }
4618 for (table_id, row_id) in to_delete {
4619 out.added
4620 .push((table_id, crate::txn::Staged::Delete(row_id)));
4621 out.added_stacks.push(trigger_stack.to_vec());
4622 }
4623 }
4624 TriggerStep::UpdateWhere {
4625 table,
4626 conditions,
4627 cells,
4628 } => {
4629 let schema = self.table(table)?.lock().schema().clone();
4630 let snapshot = Snapshot::at(read_epoch);
4631 let rows = self.table(table)?.lock().visible_rows(snapshot)?;
4632 let table_id = self.table_id(table)?;
4633 let mut to_update = Vec::new();
4634 for row in rows {
4635 let image = TriggerRowImage::from_row(row.clone());
4636 let passes = conditions
4637 .iter()
4638 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
4639 .collect::<Result<Vec<_>>>()?
4640 .into_iter()
4641 .all(|b| b);
4642 if passes {
4643 let new_cells = cells
4644 .iter()
4645 .map(|cell| {
4646 Ok((
4647 cell.column_id,
4648 eval_trigger_value(&cell.value, event, Some(&image))?,
4649 ))
4650 })
4651 .collect::<Result<Vec<_>>>()?;
4652 let mut merged = row.columns.clone();
4653 for (column_id, value) in new_cells {
4654 merged.insert(column_id, value);
4655 }
4656 to_update.push((table_id, row.row_id, merged));
4657 }
4658 }
4659 for (table_id, row_id, merged) in to_update {
4660 out.added.push((
4661 table_id,
4662 crate::txn::Staged::Update(row_id, merged.into_iter().collect()),
4663 ));
4664 out.added_stacks.push(trigger_stack.to_vec());
4665 }
4666 }
4667 TriggerStep::Raise { action, message } => match action {
4668 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
4669 TriggerRaiseAction::Abort
4670 | TriggerRaiseAction::Fail
4671 | TriggerRaiseAction::Rollback => {
4672 let message = eval_trigger_value(message, event, selected)?;
4673 return Err(MongrelError::Conflict(format!(
4674 "trigger {:?} raised: {}; trigger stack: {}",
4675 trigger.name,
4676 trigger_message(message),
4677 Self::format_trigger_stack(trigger_stack)
4678 )));
4679 }
4680 },
4681 }
4682 }
4683 Ok(TriggerProgramOutcome::Continue)
4684 }
4685
4686 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
4687 let Some(stacks) = stacks else {
4688 return Vec::new();
4689 };
4690 let mut out = Vec::new();
4691 for idx in indices {
4692 let Some(stack) = stacks.get(*idx) else {
4693 continue;
4694 };
4695 for name in stack {
4696 if !out.iter().any(|existing| existing == name) {
4697 out.push(name.clone());
4698 }
4699 }
4700 }
4701 out
4702 }
4703
4704 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
4705 let mut out = stack.to_vec();
4706 out.push(trigger_name.to_string());
4707 out
4708 }
4709
4710 fn format_trigger_stack(stack: &[String]) -> String {
4711 if stack.is_empty() {
4712 "<root>".into()
4713 } else {
4714 stack.join(" -> ")
4715 }
4716 }
4717
4718 fn validate_constraints(
4733 &self,
4734 staging: &mut Vec<(u64, crate::txn::Staged)>,
4735 read_epoch: Epoch,
4736 ) -> Result<()> {
4737 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
4738 use crate::memtable::Row;
4739 use crate::txn::Staged;
4740 use std::collections::HashSet;
4741
4742 let snapshot = Snapshot::at(read_epoch);
4743 let cat = self.catalog.read();
4744
4745 let live: Vec<(u64, &str, &crate::schema::Schema)> = cat
4747 .tables
4748 .iter()
4749 .filter(|e| matches!(e.state, TableState::Live))
4750 .map(|e| (e.table_id, e.name.as_str(), &e.schema))
4751 .collect();
4752
4753 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
4755 if !any_constraints {
4756 return Ok(());
4757 }
4758
4759 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
4761 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
4762 if let Some(r) = rows_cache.get(&table_id) {
4763 return Ok(r.clone());
4764 }
4765 let handle = self.table_by_id(table_id)?;
4766 let rows = handle.lock().visible_rows(snapshot)?;
4767 rows_cache.insert(table_id, rows.clone());
4768 Ok(rows)
4769 };
4770
4771 let mut processed_updates = HashSet::new();
4776 type PendingUpdate = (usize, u64, crate::rowid::RowId, Vec<(u16, Value)>);
4777 loop {
4778 let updates: Vec<PendingUpdate> = staging
4779 .iter()
4780 .enumerate()
4781 .filter_map(|(index, (table_id, op))| match op {
4782 Staged::Update(row_id, cells) if !processed_updates.contains(&index) => {
4783 Some((index, *table_id, *row_id, cells.clone()))
4784 }
4785 _ => None,
4786 })
4787 .collect();
4788 if updates.is_empty() {
4789 break;
4790 }
4791 let mut new_ops = Vec::new();
4792 for (index, table_id, row_id, new_cells) in updates {
4793 processed_updates.insert(index);
4794 let Some(tname) = live
4795 .iter()
4796 .find(|(id, _, _)| *id == table_id)
4797 .map(|(_, name, _)| *name)
4798 else {
4799 continue;
4800 };
4801 let Some(old_row) = self.table_by_id(table_id)?.lock().get(row_id, snapshot) else {
4802 continue;
4803 };
4804 let new_map: HashMap<u16, Value> = new_cells.iter().cloned().collect();
4805 for (child_id, _child_name, child_schema) in &live {
4806 for fk in &child_schema.constraints.foreign_keys {
4807 if fk.ref_table != tname {
4808 continue;
4809 }
4810 let Some(old_key) = encode_composite_key(&fk.ref_columns, &old_row.columns)
4811 else {
4812 continue;
4813 };
4814 if encode_composite_key(&fk.ref_columns, &new_map).as_deref()
4815 == Some(old_key.as_slice())
4816 {
4817 continue;
4818 }
4819 if fk.on_update == FkAction::Restrict {
4820 continue;
4821 }
4822 let child_rows = load_rows(*child_id)?;
4823 for child in child_rows {
4824 if encode_composite_key(&fk.columns, &child.columns).as_deref()
4825 != Some(old_key.as_slice())
4826 {
4827 continue;
4828 }
4829 if staging.iter().any(|(id, op)| {
4830 *id == *child_id
4831 && matches!(op, Staged::Delete(id) if *id == child.row_id)
4832 }) {
4833 continue;
4834 }
4835 let mut cells: Vec<(u16, Value)> = child
4836 .columns
4837 .iter()
4838 .map(|(column_id, value)| (*column_id, value.clone()))
4839 .collect();
4840 for (child_column, parent_column) in
4841 fk.columns.iter().zip(&fk.ref_columns)
4842 {
4843 cells.retain(|(column_id, _)| column_id != child_column);
4844 let value = match fk.on_update {
4845 FkAction::Cascade => {
4846 new_map.get(parent_column).cloned().unwrap_or(Value::Null)
4847 }
4848 FkAction::SetNull => Value::Null,
4849 FkAction::Restrict => unreachable!(),
4850 };
4851 cells.push((*child_column, value));
4852 }
4853 cells.sort_by_key(|(column_id, _)| *column_id);
4854 if let Some(existing_index) = staging.iter().position(|(id, op)| {
4855 *id == *child_id
4856 && matches!(op, Staged::Update(id, _) if *id == child.row_id)
4857 }) {
4858 if let Staged::Update(_, existing) = &mut staging[existing_index].1
4859 {
4860 if *existing != cells {
4861 *existing = cells;
4862 processed_updates.remove(&existing_index);
4863 }
4864 }
4865 } else {
4866 new_ops.push((*child_id, Staged::Update(child.row_id, cells)));
4867 }
4868 }
4869 }
4870 }
4871 }
4872 staging.extend(new_ops);
4873 }
4874
4875 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
4880 loop {
4881 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
4882 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
4883 .iter()
4884 .filter_map(|(t, op)| match op {
4885 Staged::Delete(rid) => Some((*t, *rid)),
4886 _ => None,
4887 })
4888 .collect();
4889 for (table_id, rid) in deletes {
4890 if !cascaded.insert((table_id, rid.0)) {
4891 continue;
4892 }
4893 let Some(tname) = live
4894 .iter()
4895 .find(|(t, _, _)| *t == table_id)
4896 .map(|(_, n, _)| *n)
4897 else {
4898 continue;
4899 };
4900 let parent_handle = self.table_by_id(table_id)?;
4901 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
4902 continue;
4903 };
4904 for (child_id, _child_name, child_schema) in &live {
4905 for fk in &child_schema.constraints.foreign_keys {
4906 if fk.ref_table != tname {
4907 continue;
4908 }
4909 let Some(parent_key) =
4910 encode_composite_key(&fk.ref_columns, &parent_row.columns)
4911 else {
4912 continue;
4913 };
4914 let key_preserved = staging.iter().any(|(t, op)| {
4923 if *t != table_id {
4924 return false;
4925 }
4926 let Staged::Put(cells) = op else {
4927 return false;
4928 };
4929 let map: HashMap<u16, crate::memtable::Value> =
4930 cells.iter().cloned().collect();
4931 encode_composite_key(&fk.ref_columns, &map).as_deref()
4932 == Some(parent_key.as_slice())
4933 });
4934 if key_preserved {
4935 continue;
4936 }
4937 match fk.on_delete {
4938 FkAction::Restrict => continue,
4939 FkAction::Cascade => {
4940 let child_rows = load_rows(*child_id)?;
4941 for cr in &child_rows {
4942 if !cascaded.contains(&(*child_id, cr.row_id.0))
4943 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
4944 == Some(parent_key.as_slice())
4945 {
4946 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
4947 }
4948 }
4949 }
4950 FkAction::SetNull => {
4951 let child_rows = load_rows(*child_id)?;
4952 for cr in &child_rows {
4953 if !cascaded.contains(&(*child_id, cr.row_id.0))
4954 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
4955 == Some(parent_key.as_slice())
4956 {
4957 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
4960 .columns
4961 .iter()
4962 .map(|(k, v)| (*k, v.clone()))
4963 .collect();
4964 for cid in &fk.columns {
4965 cells.retain(|(k, _)| k != cid);
4966 cells.push((*cid, crate::memtable::Value::Null));
4967 }
4968 new_ops.push((*child_id, Staged::Update(cr.row_id, cells)));
4969 }
4970 }
4971 }
4972 }
4973 }
4974 }
4975 }
4976 if new_ops.is_empty() {
4977 break;
4978 }
4979 staging.extend(new_ops);
4980 }
4981
4982 let staged_deletes: HashSet<(u64, u64)> = staging
4986 .iter()
4987 .filter_map(|(t, op)| match op {
4988 Staged::Delete(rid) | Staged::Update(rid, _) => Some((*t, rid.0)),
4989 _ => None,
4990 })
4991 .collect();
4992
4993 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
4995
4996 for (table_id, op) in staging.iter() {
4998 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id).copied()
4999 else {
5000 continue;
5001 };
5002 let cells_map: HashMap<u16, crate::memtable::Value>;
5003 match op {
5004 Staged::Put(cells) | Staged::Update(_, cells) => {
5005 cells_map = cells.iter().cloned().collect();
5006
5007 if !schema.constraints.checks.is_empty() {
5009 validate_checks(&schema.constraints.checks, &cells_map)?;
5010 }
5011
5012 for uc in &schema.constraints.uniques {
5014 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
5015 continue; };
5017 let marker = (*table_id, uc.id, key.clone());
5018 if !seen_unique.insert(marker) {
5019 return Err(MongrelError::Conflict(format!(
5020 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
5021 uc.name
5022 )));
5023 }
5024 let rows = load_rows(*table_id)?;
5025 for r in &rows {
5026 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
5029 continue;
5030 }
5031 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
5032 if theirs == key {
5033 return Err(MongrelError::Conflict(format!(
5034 "UNIQUE constraint '{}' on table '{tname}' violated",
5035 uc.name
5036 )));
5037 }
5038 }
5039 }
5040 }
5041
5042 for fk in &schema.constraints.foreign_keys {
5044 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
5045 continue; };
5047 let Some(parent_id) = cat
5048 .tables
5049 .iter()
5050 .find(|t| t.name == fk.ref_table)
5051 .map(|t| t.table_id)
5052 else {
5053 return Err(MongrelError::InvalidArgument(format!(
5054 "FOREIGN KEY '{}' references unknown table '{}'",
5055 fk.name, fk.ref_table
5056 )));
5057 };
5058 let parent_rows = load_rows(parent_id)?;
5059 let mut found = false;
5060 for r in &parent_rows {
5061 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
5062 continue;
5063 }
5064 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
5065 if pkey == child_key {
5066 found = true;
5067 break;
5068 }
5069 }
5070 }
5071 if !found {
5078 for (st_table, st_op) in staging.iter() {
5079 if *st_table != parent_id {
5080 continue;
5081 }
5082 if let Staged::Put(pcells) | Staged::Update(_, pcells) = st_op {
5083 let pmap: HashMap<u16, crate::memtable::Value> =
5084 pcells.iter().cloned().collect();
5085 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &pmap)
5086 {
5087 if pkey == child_key {
5088 found = true;
5089 break;
5090 }
5091 }
5092 }
5093 }
5094 }
5095 if !found {
5096 return Err(MongrelError::Conflict(format!(
5097 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
5098 fk.name, fk.ref_table
5099 )));
5100 }
5101 }
5102
5103 if let Staged::Update(row_id, _) = op {
5108 let parent_handle = self.table_by_id(*table_id)?;
5109 let Some(old_parent) = parent_handle.lock().get(*row_id, snapshot) else {
5110 continue;
5111 };
5112 for (child_id, child_name, child_schema) in &live {
5113 for fk in &child_schema.constraints.foreign_keys {
5114 if fk.ref_table != tname || fk.on_update != FkAction::Restrict {
5115 continue;
5116 }
5117 let Some(old_key) =
5118 encode_composite_key(&fk.ref_columns, &old_parent.columns)
5119 else {
5120 continue;
5121 };
5122 if encode_composite_key(&fk.ref_columns, &cells_map).as_deref()
5123 == Some(old_key.as_slice())
5124 {
5125 continue;
5126 }
5127 for child in load_rows(*child_id)? {
5128 if encode_composite_key(&fk.columns, &child.columns).as_deref()
5129 != Some(old_key.as_slice())
5130 {
5131 continue;
5132 }
5133 let replacement = staging.iter().find_map(|(id, op)| {
5134 if *id != *child_id {
5135 return None;
5136 }
5137 match op {
5138 Staged::Delete(id) if *id == child.row_id => Some(None),
5139 Staged::Update(id, cells) if *id == child.row_id => {
5140 let map: HashMap<u16, Value> =
5141 cells.iter().cloned().collect();
5142 Some(encode_composite_key(&fk.columns, &map))
5143 }
5144 _ => None,
5145 }
5146 });
5147 if replacement.is_some_and(|key| {
5148 key.as_deref() != Some(old_key.as_slice())
5149 }) {
5150 continue;
5151 }
5152 return Err(MongrelError::Conflict(format!(
5153 "FOREIGN KEY '{}' on table '{child_name}' restricts update (parent key referenced)",
5154 fk.name
5155 )));
5156 }
5157 }
5158 }
5159 }
5160 }
5161 Staged::Delete(rid) => {
5162 let parent_handle = self.table_by_id(*table_id)?;
5166 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
5167 continue;
5168 };
5169 for (child_id, child_name, child_schema) in &live {
5170 for fk in &child_schema.constraints.foreign_keys {
5171 if fk.ref_table != tname || fk.on_delete != FkAction::Restrict {
5172 continue;
5173 }
5174 let Some(parent_key) =
5175 encode_composite_key(&fk.ref_columns, &parent_row.columns)
5176 else {
5177 continue;
5178 };
5179 let child_rows = load_rows(*child_id)?;
5180 for r in &child_rows {
5181 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
5184 continue;
5185 }
5186 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
5187 if ck == parent_key {
5188 return Err(MongrelError::Conflict(format!(
5189 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
5190 fk.name
5191 )));
5192 }
5193 }
5194 }
5195 }
5196 }
5197 }
5198 Staged::Truncate => {
5199 for (child_id, child_name, child_schema) in &live {
5203 for fk in &child_schema.constraints.foreign_keys {
5204 if fk.ref_table != tname {
5205 continue;
5206 }
5207 let child_rows = load_rows(*child_id)?;
5208 if child_rows
5209 .iter()
5210 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
5211 {
5212 return Err(MongrelError::Conflict(format!(
5213 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
5214 fk.name
5215 )));
5216 }
5217 }
5218 }
5219 }
5220 }
5221 }
5222 Ok(())
5223 }
5224
5225 fn validate_security_writes(
5226 &self,
5227 staging: &[(u64, crate::txn::Staged)],
5228 read_epoch: Epoch,
5229 explicit_principal: Option<&crate::auth::Principal>,
5230 ) -> Result<()> {
5231 use crate::security::PolicyCommand;
5232 use crate::txn::Staged;
5233
5234 let catalog = self.catalog.read();
5235 if catalog.security.rls_tables.is_empty() {
5236 return Ok(());
5237 }
5238 let security = catalog.security.clone();
5239 let table_names = catalog
5240 .tables
5241 .iter()
5242 .filter(|entry| matches!(entry.state, TableState::Live))
5243 .map(|entry| (entry.table_id, entry.name.clone()))
5244 .collect::<HashMap<_, _>>();
5245 drop(catalog);
5246 if !staging.iter().any(|(table_id, _)| {
5247 table_names
5248 .get(table_id)
5249 .is_some_and(|table| security.rls_enabled(table))
5250 }) {
5251 return Ok(());
5252 }
5253 let cached = self.principal.read().clone();
5254 let principal = explicit_principal
5255 .or(cached.as_ref())
5256 .ok_or(MongrelError::AuthRequired)?;
5257
5258 for (table_id, operation) in staging {
5259 let Some(table) = table_names.get(table_id) else {
5260 continue;
5261 };
5262 if !security.rls_enabled(table) || principal.is_admin {
5263 continue;
5264 }
5265 let denied = |command| MongrelError::PermissionDenied {
5266 required: match command {
5267 PolicyCommand::Insert => crate::auth::Permission::Insert {
5268 table: table.clone(),
5269 },
5270 PolicyCommand::Update => crate::auth::Permission::Update {
5271 table: table.clone(),
5272 },
5273 PolicyCommand::Delete | PolicyCommand::All | PolicyCommand::Select => {
5274 crate::auth::Permission::Delete {
5275 table: table.clone(),
5276 }
5277 }
5278 },
5279 principal: principal.username.clone(),
5280 };
5281 match operation {
5282 Staged::Put(cells) => {
5283 let mut row = crate::memtable::Row::new(RowId(0), Epoch(read_epoch.0));
5284 row.columns.extend(cells.iter().cloned());
5285 if !security.row_allowed(table, PolicyCommand::Insert, &row, principal, true) {
5286 return Err(denied(PolicyCommand::Insert));
5287 }
5288 }
5289 Staged::Update(row_id, cells) => {
5290 let old = self
5291 .table_by_id(*table_id)?
5292 .lock()
5293 .get(*row_id, Snapshot::at(read_epoch))
5294 .ok_or_else(|| {
5295 MongrelError::NotFound(format!("row {} not found", row_id.0))
5296 })?;
5297 if !security.row_allowed(table, PolicyCommand::Update, &old, principal, false) {
5298 return Err(denied(PolicyCommand::Update));
5299 }
5300 let mut new = crate::memtable::Row::new(*row_id, Epoch(read_epoch.0));
5301 new.columns.extend(cells.iter().cloned());
5302 if !security.row_allowed(table, PolicyCommand::Update, &new, principal, true) {
5303 return Err(denied(PolicyCommand::Update));
5304 }
5305 }
5306 Staged::Delete(row_id) => {
5307 let old = self
5308 .table_by_id(*table_id)?
5309 .lock()
5310 .get(*row_id, Snapshot::at(read_epoch))
5311 .ok_or_else(|| {
5312 MongrelError::NotFound(format!("row {} not found", row_id.0))
5313 })?;
5314 if !security.row_allowed(table, PolicyCommand::Delete, &old, principal, false) {
5315 return Err(denied(PolicyCommand::Delete));
5316 }
5317 }
5318 Staged::Truncate => return Err(denied(PolicyCommand::Delete)),
5319 }
5320 }
5321 Ok(())
5322 }
5323
5324 #[allow(clippy::too_many_arguments)]
5331 pub(crate) fn commit_transaction_with_external_states(
5332 &self,
5333 txn_id: u64,
5334 read_epoch: Epoch,
5335 mut staging: Vec<(u64, crate::txn::Staged)>,
5336 external_states: Vec<(String, Vec<u8>)>,
5337 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
5338 security_principal: Option<crate::auth::Principal>,
5339 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
5340 ) -> Result<Epoch> {
5341 use crate::memtable::Row;
5342 use crate::txn::{Staged, StagedOp, WriteKey};
5343 use crate::wal::Op;
5344 use std::collections::hash_map::DefaultHasher;
5345 use std::hash::{Hash, Hasher};
5346 use std::sync::atomic::Ordering;
5347
5348 if self.read_only {
5349 return Err(MongrelError::ReadOnlyReplica);
5350 }
5351 let _replication_guard = self.replication_barrier.read();
5352 if self.poisoned.load(Ordering::Relaxed) {
5353 return Err(MongrelError::Other(
5354 "database poisoned by fsync error".into(),
5355 ));
5356 }
5357 let mut external_states = dedup_external_states(external_states);
5358 if !external_states.is_empty() {
5359 let cat = self.catalog.read();
5360 for (name, _) in &external_states {
5361 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
5362 return Err(MongrelError::NotFound(format!(
5363 "external table {name:?} not found"
5364 )));
5365 }
5366 }
5367 }
5368 let prepared_materialized_views = {
5369 let mut deduplicated = HashMap::new();
5370 for definition in materialized_view_updates {
5371 if definition.name.is_empty() || definition.query.trim().is_empty() {
5372 return Err(MongrelError::InvalidArgument(
5373 "materialized view name and query must not be empty".into(),
5374 ));
5375 }
5376 deduplicated.insert(definition.name.clone(), definition);
5377 }
5378 let catalog = self.catalog.read();
5379 let mut prepared = Vec::with_capacity(deduplicated.len());
5380 for definition in deduplicated.into_values() {
5381 let table_id = catalog
5382 .live(&definition.name)
5383 .ok_or_else(|| {
5384 MongrelError::NotFound(format!(
5385 "materialized view table {:?} not found",
5386 definition.name
5387 ))
5388 })?
5389 .table_id;
5390 prepared.push((table_id, definition));
5391 }
5392 prepared.sort_by(|left, right| left.1.name.cmp(&right.1.name));
5393 prepared
5394 };
5395
5396 self.fill_auto_increment_for_staging(&mut staging)?;
5399 self.expand_table_triggers(
5400 &mut staging,
5401 read_epoch,
5402 external_trigger_bridge,
5403 &mut external_states,
5404 )?;
5405 self.fill_auto_increment_for_staging(&mut staging)?;
5406 external_states = dedup_external_states(external_states);
5407
5408 self.validate_constraints(&mut staging, read_epoch)?;
5413 self.validate_security_writes(&staging, read_epoch, security_principal.as_ref())?;
5414 let mut normalized = Vec::with_capacity(staging.len() * 2);
5415 for (table_id, op) in staging {
5416 match op {
5417 crate::txn::Staged::Update(row_id, cells) => {
5418 normalized.push((table_id, crate::txn::Staged::Delete(row_id)));
5419 normalized.push((table_id, crate::txn::Staged::Put(cells)));
5420 }
5421 op => normalized.push((table_id, op)),
5422 }
5423 }
5424 staging = normalized;
5425 let has_changes = !staging.is_empty()
5426 || !external_states.is_empty()
5427 || !prepared_materialized_views.is_empty();
5428 let truncated_tables: HashSet<u64> = staging
5429 .iter()
5430 .filter_map(|(table_id, op)| matches!(op, Staged::Truncate).then_some(*table_id))
5431 .collect();
5432
5433 let write_keys = {
5434 let cat = self.catalog.read();
5435 let mut keys: Vec<WriteKey> = Vec::new();
5436 for (table_id, staged) in &staging {
5437 match staged {
5438 Staged::Put(cells) => {
5439 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
5440 for col in &entry.schema.columns {
5441 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
5442 if let Some((_, val)) =
5443 cells.iter().find(|(id, _)| *id == col.id)
5444 {
5445 let mut h = DefaultHasher::new();
5446 val.encode_key().hash(&mut h);
5447 keys.push(WriteKey::Unique {
5448 table_id: *table_id,
5449 index_id: 0,
5450 key_hash: h.finish(),
5451 });
5452 }
5453 }
5454 }
5455 for uc in &entry.schema.constraints.uniques {
5462 if let Some(key_bytes) = crate::constraint::encode_composite_key(
5463 &uc.columns,
5464 &cells.iter().cloned().collect(),
5465 ) {
5466 let mut h = DefaultHasher::new();
5467 key_bytes.hash(&mut h);
5468 keys.push(WriteKey::Unique {
5469 table_id: *table_id,
5470 index_id: uc.id | 0x8000,
5471 key_hash: h.finish(),
5472 });
5473 }
5474 }
5475 }
5476 }
5477 Staged::Delete(rid) => keys.push(WriteKey::Row {
5478 table_id: *table_id,
5479 row_id: rid.0,
5480 }),
5481 Staged::Truncate => keys.push(WriteKey::Table {
5482 table_id: *table_id,
5483 }),
5484 Staged::Update(_, _) => unreachable!("updates normalized before prepare"),
5485 }
5486 }
5487 for (name, _) in &external_states {
5488 let mut h = DefaultHasher::new();
5489 name.hash(&mut h);
5490 keys.push(WriteKey::Unique {
5491 table_id: EXTERNAL_TABLE_ID,
5492 index_id: 0,
5493 key_hash: h.finish(),
5494 });
5495 }
5496 keys
5497 };
5498
5499 let min_active = self.active_txns.min_read_epoch();
5501 if min_active < u64::MAX {
5502 self.conflicts.prune_below(Epoch(min_active));
5503 }
5504
5505 if self.conflicts.conflicts(&write_keys, read_epoch) {
5509 return Err(MongrelError::Conflict(
5510 "write-write conflict (pre-validate, first-committer-wins)".into(),
5511 ));
5512 }
5513 let pre_validate_version = self.conflicts.version();
5514
5515 let mut spilled: Vec<SpilledRun> = Vec::new();
5519 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
5520 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
5524 {
5525 let mut table_bytes: HashMap<u64, usize> = HashMap::new();
5526 for (table_id, staged) in &staging {
5527 if let Staged::Put(cells) = staged {
5528 *table_bytes.entry(*table_id).or_default() += cells.len() * 16;
5529 }
5530 }
5531 let tables = self.tables.read();
5532 for (&table_id, &bytes) in &table_bytes {
5533 if bytes as u64
5534 <= self
5535 .spill_threshold
5536 .load(std::sync::atomic::Ordering::Relaxed)
5537 {
5538 continue;
5539 }
5540 let Some(handle) = tables.get(&table_id) else {
5541 continue;
5542 };
5543 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
5544 let mut t = handle.lock();
5545 let tdir = t.table_dir().to_path_buf();
5546 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
5547 std::fs::create_dir_all(&txn_dir)?;
5548 let run_id = t.alloc_run_id() as u128;
5549 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
5550
5551 let mut rows: Vec<Row> = Vec::new();
5552 for (tid, staged) in &staging {
5553 if *tid != table_id {
5554 continue;
5555 }
5556 if let Staged::Put(cells) = staged {
5557 t.validate_cells_not_null(cells)?;
5558 let row_id = t.alloc_row_id();
5559 let mut row = Row::new(row_id, Epoch(0));
5560 for (c, v) in cells {
5561 row.columns.insert(*c, v.clone());
5562 }
5563 rows.push(row);
5564 }
5565 }
5566 let schema = t.schema_ref().clone();
5567 let kek = t.kek_ref().cloned();
5568 let specs = t.indexable_column_specs();
5569 drop(t);
5570
5571 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
5572 .uniform_epoch(true);
5573 if let Some(ref kek) = kek {
5574 writer = writer.with_encryption(kek.as_ref(), specs);
5575 }
5576 let header = writer.write(&pending_path, &rows)?;
5577 let row_count = header.row_count;
5578 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
5579 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
5580
5581 spilled.push(SpilledRun {
5582 table_id,
5583 run_id,
5584 pending_path,
5585 rows,
5586 row_count,
5587 min_rid,
5588 max_rid,
5589 });
5590 spilled_tables.insert(table_id);
5591 }
5592 }
5593
5594 if spill_guard.is_some() {
5596 if let Some(hook) = self.spill_hook.lock().as_ref() {
5597 hook();
5598 }
5599 }
5600
5601 let mut prebuilt: Vec<Option<Row>> = Vec::with_capacity(staging.len());
5612 let mut delete_images: Vec<Option<Row>> = Vec::with_capacity(staging.len());
5613 {
5614 let tables = self.tables.read();
5615 for (table_id, staged) in &staging {
5616 match staged {
5617 Staged::Put(cells) if !spilled_tables.contains(table_id) => {
5618 let handle = tables.get(table_id).ok_or_else(|| {
5619 MongrelError::NotFound(format!("table {table_id} not mounted"))
5620 })?;
5621 let mut t = handle.lock();
5622 t.validate_cells_not_null(cells)?;
5623 let row_id = t.alloc_row_id();
5624 drop(t);
5625 let mut row = Row::new(row_id, Epoch(0));
5626 for (c, v) in cells {
5627 row.columns.insert(*c, v.clone());
5628 }
5629 prebuilt.push(Some(row));
5630 delete_images.push(None);
5631 }
5632 Staged::Delete(row_id) => {
5633 let before = tables.get(table_id).and_then(|handle| {
5634 handle.lock().get(*row_id, Snapshot::at(read_epoch))
5635 });
5636 prebuilt.push(None);
5637 delete_images.push(before);
5638 }
5639 Staged::Put(_) | Staged::Truncate => {
5640 prebuilt.push(None);
5641 delete_images.push(None);
5642 }
5643 Staged::Update(_, _) => unreachable!("updates normalized before prepare"),
5644 }
5645 }
5646 }
5647
5648 let mut prepared_external = Vec::with_capacity(external_states.len());
5649 for (name, state) in &external_states {
5650 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
5651 prepared_external.push((name.clone(), state.clone(), pending));
5652 }
5653
5654 let added_runs: Vec<crate::wal::AddedRun> = spilled
5656 .iter()
5657 .map(|s| crate::wal::AddedRun {
5658 table_id: s.table_id,
5659 run_id: s.run_id,
5660 row_count: s.row_count,
5661 level: 0,
5662 min_row_id: s.min_rid,
5663 max_row_id: s.max_rid,
5664 content_hash: [0u8; 32],
5665 })
5666 .collect();
5667 let (new_epoch, mut _epoch_guard, applies, committed_materialized_views, commit_seq) = {
5668 let mut wal = self.shared_wal.lock();
5669
5670 if self.conflicts.version() != pre_validate_version
5675 && self.conflicts.conflicts(&write_keys, read_epoch)
5676 {
5677 drop(wal);
5681 for s in &spilled {
5682 if let Some(parent) = s.pending_path.parent() {
5683 let _ = std::fs::remove_dir_all(parent);
5684 }
5685 }
5686 for (_, _, pending) in &prepared_external {
5687 let _ = std::fs::remove_file(pending);
5688 }
5689 return Err(MongrelError::Conflict(
5690 "write-write conflict (sequencer delta re-check)".into(),
5691 ));
5692 }
5693
5694 let new_epoch = self.epoch.bump_assigned();
5695 let _epoch_guard = EpochGuard::new(self.epoch.as_ref(), new_epoch);
5696 let mut applies: Vec<(u64, Vec<StagedOp>)> = Vec::new();
5697 let mut committed_materialized_views = Vec::new();
5698
5699 for (idx, (table_id, staged)) in staging.iter().enumerate() {
5700 if spilled_tables.contains(table_id) && matches!(staged, Staged::Put(_)) {
5703 continue;
5704 }
5705 let mut ops = Vec::new();
5706 match staged {
5707 Staged::Put(_) => {
5708 let mut row = prebuilt[idx].take().expect("prebuilt put row");
5710 row.committed_epoch = new_epoch;
5711 let payload = bincode::serialize(&vec![row.clone()])
5712 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
5713 wal.append(
5714 txn_id,
5715 *table_id,
5716 Op::Put {
5717 table_id: *table_id,
5718 rows: payload,
5719 },
5720 )?;
5721 ops.push(StagedOp::Put(row));
5722 }
5723 Staged::Delete(rid) => {
5724 if let Some(before) = &delete_images[idx] {
5725 wal.append(
5726 txn_id,
5727 *table_id,
5728 Op::BeforeImage {
5729 table_id: *table_id,
5730 row_id: *rid,
5731 row: bincode::serialize(before).map_err(|error| {
5732 MongrelError::Other(format!(
5733 "before-image serialize: {error}"
5734 ))
5735 })?,
5736 },
5737 )?;
5738 }
5739 wal.append(
5740 txn_id,
5741 *table_id,
5742 Op::Delete {
5743 table_id: *table_id,
5744 row_ids: vec![*rid],
5745 },
5746 )?;
5747 ops.push(StagedOp::Delete(*rid));
5748 }
5749 Staged::Truncate => {
5750 wal.append(
5751 txn_id,
5752 *table_id,
5753 Op::TruncateTable {
5754 table_id: *table_id,
5755 },
5756 )?;
5757 ops.push(StagedOp::Truncate);
5758 }
5759 Staged::Update(_, _) => unreachable!("updates normalized before sequencer"),
5760 }
5761 applies.push((*table_id, ops));
5762 }
5763
5764 for (name, state, _) in &prepared_external {
5765 wal.append(
5766 txn_id,
5767 EXTERNAL_TABLE_ID,
5768 Op::ExternalTableState {
5769 name: name.clone(),
5770 state: state.clone(),
5771 },
5772 )?;
5773 }
5774
5775 for (table_id, definition) in &prepared_materialized_views {
5776 let mut definition = definition.clone();
5777 definition.last_refresh_epoch = new_epoch.0;
5778 wal.append(
5779 txn_id,
5780 *table_id,
5781 Op::Ddl(crate::wal::DdlOp::SetMaterializedView {
5782 name: definition.name.clone(),
5783 definition_json: crate::wal::DdlOp::encode_materialized_view(&definition)?,
5784 }),
5785 )?;
5786 committed_materialized_views.push(definition);
5787 }
5788
5789 let commit_seq = wal.append_commit(txn_id, new_epoch, &added_runs)?;
5790
5791 self.conflicts.record(&write_keys, new_epoch);
5796 (
5797 new_epoch,
5798 _epoch_guard,
5799 applies,
5800 committed_materialized_views,
5801 commit_seq,
5802 )
5803 };
5804
5805 self.group
5807 .await_durable(&self.shared_wal, commit_seq)
5808 .inspect_err(|_| {
5809 self.poisoned.store(true, Ordering::Relaxed);
5810 })?;
5811
5812 {
5814 let tables = self.tables.read();
5815 for (table_id, ops) in applies {
5819 if let Some(handle) = tables.get(&table_id) {
5820 let mut t = handle.lock();
5821 for op in ops {
5822 match op {
5823 StagedOp::Put(row) => t.apply_put_rows(vec![row])?,
5824 StagedOp::Delete(rid) => t.apply_delete(rid, new_epoch),
5825 StagedOp::Truncate => t.apply_truncate(new_epoch)?,
5826 }
5827 }
5828 t.invalidate_pending_cache();
5829 t.persist_manifest(new_epoch)?;
5830 }
5831 }
5832 for s in &spilled {
5833 if let Some(handle) = tables.get(&s.table_id) {
5834 let mut t = handle.lock();
5835 let dest = t.run_path(s.run_id as u64);
5836 std::fs::rename(&s.pending_path, &dest)?;
5837 if let Some(parent) = s.pending_path.parent() {
5838 let _ = std::fs::remove_dir_all(parent);
5839 }
5840 t.link_run(crate::manifest::RunRef {
5841 run_id: s.run_id,
5842 level: 0,
5843 epoch_created: new_epoch.0,
5844 row_count: s.row_count,
5845 });
5846 t.apply_run_metadata(&s.rows)?;
5847 if truncated_tables.contains(&s.table_id) {
5848 t.set_flushed_epoch(new_epoch);
5853 }
5854 t.invalidate_pending_cache();
5855 t.persist_manifest(new_epoch)?;
5856 }
5857 }
5858 }
5859 for (name, _, pending) in &prepared_external {
5860 publish_external_state_file(&self.root, name, pending)?;
5861 }
5862 if !committed_materialized_views.is_empty() {
5863 {
5864 let mut catalog = self.catalog.write();
5865 for definition in committed_materialized_views {
5866 if let Some(existing) = catalog
5867 .materialized_views
5868 .iter_mut()
5869 .find(|existing| existing.name == definition.name)
5870 {
5871 *existing = definition;
5872 } else {
5873 catalog.materialized_views.push(definition);
5874 }
5875 }
5876 catalog.db_epoch = catalog.db_epoch.max(new_epoch.0);
5877 }
5878 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
5879 }
5880
5881 self.epoch.publish_in_order(new_epoch);
5882 if has_changes {
5883 let _ = self.change_wake.send(());
5884 }
5885 _epoch_guard.disarm();
5886 Ok(new_epoch)
5887 }
5888
5889 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
5892 let e = self.epoch.visible();
5893 let g = self.snapshots.register(e);
5894 (Snapshot::at(e), g)
5895 }
5896
5897 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
5900 let e = self.epoch.visible();
5901 let g = self.snapshots.register_owned(e);
5902 (Snapshot::at(e), g)
5903 }
5904
5905 pub fn set_history_retention_epochs(&self, epochs: u64) -> Result<()> {
5910 let _guard = self.ddl_lock.lock();
5911 let current = self.epoch.visible();
5912 let (old_epochs, old_start) = self.snapshots.history_config();
5913 let earliest_already_guaranteed = if old_epochs == 0 {
5914 current
5915 } else {
5916 Epoch(old_start.0.max(current.0.saturating_sub(old_epochs)))
5917 };
5918 let start = if epochs == 0 {
5919 current
5920 } else {
5921 earliest_already_guaranteed
5922 };
5923 write_history_retention(&self.root, epochs, start)?;
5924 self.snapshots.configure_history(epochs, start);
5925 Ok(())
5926 }
5927
5928 pub fn history_retention_epochs(&self) -> u64 {
5929 self.snapshots.history_config().0
5930 }
5931
5932 pub fn earliest_retained_epoch(&self) -> Epoch {
5933 let current = self.epoch.visible();
5934 self.snapshots.history_floor(current).unwrap_or(current)
5935 }
5936
5937 pub fn snapshot_at_owned(&self, epoch: Epoch) -> Result<(Snapshot, OwnedSnapshotGuard)> {
5940 let current = self.epoch.visible();
5941 if epoch > current {
5942 return Err(MongrelError::InvalidArgument(format!(
5943 "epoch {} is in the future; current epoch is {}",
5944 epoch.0, current.0
5945 )));
5946 }
5947 let earliest = self.earliest_retained_epoch();
5948 if epoch < earliest {
5949 return Err(MongrelError::InvalidArgument(format!(
5950 "epoch {} is no longer retained; earliest available epoch is {}",
5951 epoch.0, earliest.0
5952 )));
5953 }
5954 let guard = self.snapshots.register_owned(epoch);
5955 Ok((Snapshot::at(epoch), guard))
5956 }
5957
5958 pub fn table_names(&self) -> Vec<String> {
5960 self.catalog
5961 .read()
5962 .tables
5963 .iter()
5964 .filter(|t| matches!(t.state, TableState::Live))
5965 .map(|t| t.name.clone())
5966 .collect()
5967 }
5968
5969 pub fn close(&self) -> Result<()> {
5975 for name in self.table_names() {
5976 if let Ok(handle) = self.table(&name) {
5977 if let Err(e) = handle.lock().close() {
5978 eprintln!("[close] flush failed for {name}: {e}");
5979 }
5980 }
5981 }
5982 Ok(())
5983 }
5984
5985 pub fn compact(&self) -> Result<(usize, usize)> {
5992 self.require(&crate::auth::Permission::Ddl)?;
5993 let mut compacted = 0;
5994 let mut skipped = 0;
5995 for name in self.table_names() {
5996 let Ok(handle) = self.table(&name) else {
5997 continue;
5998 };
5999 {
6000 let mut t = handle.lock();
6001 let before = t.run_count();
6002 if before < 2 && !t.should_compact() {
6003 skipped += 1;
6004 continue;
6005 }
6006 match t.compact() {
6007 Ok(()) => {
6008 let after = t.run_count();
6009 compacted += 1;
6010 eprintln!("[compact] {name}: {before} -> {after} runs");
6011 }
6012 Err(e) => {
6013 eprintln!("[compact] {name}: compaction failed: {e}");
6014 skipped += 1;
6015 }
6016 }
6017 }
6018 }
6019 Ok((compacted, skipped))
6020 }
6021
6022 pub fn compact_table(&self, name: &str) -> Result<bool> {
6025 self.require(&crate::auth::Permission::Ddl)?;
6026 let handle = self.table(name)?;
6027 let mut t = handle.lock();
6028 let before = t.run_count();
6029 if before < 2 {
6030 return Ok(false);
6031 }
6032 t.compact()?;
6033 Ok(t.run_count() < before)
6034 }
6035
6036 pub fn table(&self, name: &str) -> Result<TableHandle> {
6038 let cat = self.catalog.read();
6039 let entry = cat
6040 .live(name)
6041 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
6042 let id = entry.table_id;
6043 drop(cat);
6044 self.tables
6045 .read()
6046 .get(&id)
6047 .cloned()
6048 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
6049 }
6050
6051 pub fn has_ttl_tables(&self) -> bool {
6054 self.tables
6055 .read()
6056 .values()
6057 .any(|table| table.lock().ttl().is_some())
6058 }
6059
6060 fn table_by_id(&self, id: u64) -> Result<TableHandle> {
6063 self.tables
6064 .read()
6065 .get(&id)
6066 .cloned()
6067 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
6068 }
6069
6070 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
6076 use crate::wal::DdlOp;
6077 use std::sync::atomic::Ordering;
6078
6079 self.require(&crate::auth::Permission::Ddl)?;
6080 if self.poisoned.load(Ordering::Relaxed) {
6081 return Err(MongrelError::Other(
6082 "database poisoned by fsync error".into(),
6083 ));
6084 }
6085
6086 let _g = self.ddl_lock.lock();
6087 {
6088 let cat = self.catalog.read();
6089 if cat.live(name).is_some() {
6090 return Err(MongrelError::InvalidArgument(format!(
6091 "table {name:?} already exists"
6092 )));
6093 }
6094 }
6095
6096 let commit_lock = Arc::clone(&self.commit_lock);
6099 let _c = commit_lock.lock();
6100 let table_id = {
6101 let mut cat = self.catalog.write();
6102 let id = cat.next_table_id;
6103 cat.next_table_id += 1;
6104 id
6105 };
6106 let epoch = self.epoch.bump_assigned();
6107 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6108 let txn_id = self.alloc_txn_id();
6109
6110 let mut schema = schema;
6114 schema.schema_id = table_id;
6115 schema.validate_auto_increment()?;
6122 schema.validate_defaults()?;
6123 schema.validate_ai()?;
6124 for index in &schema.indexes {
6125 index.validate_options()?;
6126 }
6127 for constraint in &schema.constraints.checks {
6128 constraint.expr.validate()?;
6129 }
6130
6131 let schema_json = DdlOp::encode_schema(&schema)?;
6134 let commit_seq = {
6135 let mut wal = self.shared_wal.lock();
6136 wal.append(
6137 txn_id,
6138 table_id,
6139 crate::wal::Op::Ddl(DdlOp::CreateTable {
6140 table_id,
6141 name: name.to_string(),
6142 schema_json,
6143 }),
6144 )?;
6145 wal.append_commit(txn_id, epoch, &[])?
6146 };
6147 self.group
6148 .await_durable(&self.shared_wal, commit_seq)
6149 .inspect_err(|_| {
6150 self.poisoned.store(true, Ordering::Relaxed);
6151 })?;
6152
6153 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
6155 std::fs::create_dir_all(&tdir)?;
6156 let ctx = SharedCtx {
6157 epoch: Arc::clone(&self.epoch),
6158 page_cache: Arc::clone(&self.page_cache),
6159 decoded_cache: Arc::clone(&self.decoded_cache),
6160 snapshots: Arc::clone(&self.snapshots),
6161 kek: self.kek.clone(),
6162 commit_lock: Arc::clone(&self.commit_lock),
6163 shared: Some(crate::engine::SharedWalCtx {
6164 wal: Arc::clone(&self.shared_wal),
6165 group: Arc::clone(&self.group),
6166 poisoned: Arc::clone(&self.poisoned),
6167 txn_ids: Arc::clone(&self.next_txn_id),
6168 change_wake: self.change_wake.clone(),
6169 }),
6170 table_name: Some(name.to_string()),
6171 auth: self.table_auth_checker(),
6172 read_only: self.read_only,
6173 };
6174 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
6175
6176 {
6179 let mut cat = self.catalog.write();
6180 cat.tables.push(CatalogEntry {
6181 table_id,
6182 name: name.to_string(),
6183 schema,
6184 state: TableState::Live,
6185 created_epoch: epoch.0,
6186 });
6187 }
6188 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
6189 self.tables
6190 .write()
6191 .insert(table_id, Arc::new(Mutex::new(table)));
6192
6193 self.epoch.publish_in_order(epoch);
6194 _epoch_guard.disarm();
6195 Ok(table_id)
6196 }
6197
6198 pub fn drop_table(&self, name: &str) -> Result<()> {
6200 use crate::wal::DdlOp;
6201 use std::sync::atomic::Ordering;
6202
6203 self.require(&crate::auth::Permission::Ddl)?;
6204 if self.poisoned.load(Ordering::Relaxed) {
6205 return Err(MongrelError::Other(
6206 "database poisoned by fsync error".into(),
6207 ));
6208 }
6209
6210 let _g = self.ddl_lock.lock();
6211 let table_id = {
6212 let cat = self.catalog.read();
6213 cat.live(name)
6214 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
6215 .table_id
6216 };
6217
6218 let commit_lock = Arc::clone(&self.commit_lock);
6219 let _c = commit_lock.lock();
6220 let epoch = self.epoch.bump_assigned();
6221 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6222 let txn_id = self.alloc_txn_id();
6223 let commit_seq = {
6224 let mut wal = self.shared_wal.lock();
6225 wal.append(
6226 txn_id,
6227 table_id,
6228 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
6229 )?;
6230 wal.append_commit(txn_id, epoch, &[])?
6231 };
6232 self.group
6233 .await_durable(&self.shared_wal, commit_seq)
6234 .inspect_err(|_| {
6235 self.poisoned.store(true, Ordering::Relaxed);
6236 })?;
6237
6238 {
6239 let mut cat = self.catalog.write();
6240 let entry = cat
6241 .tables
6242 .iter_mut()
6243 .find(|t| t.table_id == table_id)
6244 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
6245 entry.state = TableState::Dropped { at_epoch: epoch.0 };
6246 cat.triggers.retain(|trigger| {
6247 !matches!(
6248 &trigger.trigger.target,
6249 TriggerTarget::Table(target) if target == name
6250 )
6251 });
6252 cat.materialized_views
6253 .retain(|definition| definition.name != name);
6254 cat.security.rls_tables.retain(|table| table != name);
6255 cat.security.policies.retain(|policy| policy.table != name);
6256 cat.security.masks.retain(|mask| mask.table != name);
6257 for role in &mut cat.roles {
6258 role.permissions
6259 .retain(|permission| permission_table(permission) != Some(name));
6260 }
6261 cat.security_version = cat.security_version.wrapping_add(1);
6262 }
6263 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
6264 self.tables.write().remove(&table_id);
6265
6266 self.epoch.publish_in_order(epoch);
6267 _epoch_guard.disarm();
6268 Ok(())
6269 }
6270
6271 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
6280 use crate::wal::DdlOp;
6281 use std::sync::atomic::Ordering;
6282
6283 self.require(&crate::auth::Permission::Ddl)?;
6284 if self.poisoned.load(Ordering::Relaxed) {
6285 return Err(MongrelError::Other(
6286 "database poisoned by fsync error".into(),
6287 ));
6288 }
6289
6290 if name == new_name {
6293 return Ok(());
6294 }
6295 if new_name.is_empty() {
6296 return Err(MongrelError::InvalidArgument(
6297 "rename_table: new name must not be empty".into(),
6298 ));
6299 }
6300
6301 let _g = self.ddl_lock.lock();
6302 let table_id = {
6303 let cat = self.catalog.read();
6304 let src = cat
6305 .live(name)
6306 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
6307 if cat.live(new_name).is_some() {
6311 return Err(MongrelError::InvalidArgument(format!(
6312 "rename_table: a table named {new_name:?} already exists"
6313 )));
6314 }
6315 src.table_id
6316 };
6317
6318 let commit_lock = Arc::clone(&self.commit_lock);
6319 let _c = commit_lock.lock();
6320 let epoch = self.epoch.bump_assigned();
6321 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6322 let txn_id = self.alloc_txn_id();
6323 let commit_seq = {
6324 let mut wal = self.shared_wal.lock();
6325 wal.append(
6326 txn_id,
6327 table_id,
6328 crate::wal::Op::Ddl(DdlOp::RenameTable {
6329 table_id,
6330 new_name: new_name.to_string(),
6331 }),
6332 )?;
6333 wal.append_commit(txn_id, epoch, &[])?
6334 };
6335 self.group
6336 .await_durable(&self.shared_wal, commit_seq)
6337 .inspect_err(|_| {
6338 self.poisoned.store(true, Ordering::Relaxed);
6339 })?;
6340
6341 {
6342 let mut cat = self.catalog.write();
6343 let entry = cat
6344 .tables
6345 .iter_mut()
6346 .find(|t| t.table_id == table_id)
6347 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
6348 entry.name = new_name.to_string();
6349 for trigger in &mut cat.triggers {
6350 if matches!(
6351 &trigger.trigger.target,
6352 TriggerTarget::Table(target) if target == name
6353 ) {
6354 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
6355 }
6356 }
6357 if let Some(definition) = cat
6358 .materialized_views
6359 .iter_mut()
6360 .find(|definition| definition.name == name)
6361 {
6362 definition.name = new_name.to_string();
6363 }
6364 for table in &mut cat.security.rls_tables {
6365 if table == name {
6366 *table = new_name.to_string();
6367 }
6368 }
6369 for policy in &mut cat.security.policies {
6370 if policy.table == name {
6371 policy.table = new_name.to_string();
6372 }
6373 }
6374 for mask in &mut cat.security.masks {
6375 if mask.table == name {
6376 mask.table = new_name.to_string();
6377 }
6378 }
6379 for role in &mut cat.roles {
6380 for permission in &mut role.permissions {
6381 rename_permission_table(permission, name, new_name);
6382 }
6383 }
6384 cat.security_version = cat.security_version.wrapping_add(1);
6385 }
6386 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
6387 if let Some(table) = self.tables.read().get(&table_id) {
6390 table.lock().set_catalog_name(new_name.to_string());
6391 }
6392 self.epoch.publish_in_order(epoch);
6393 _epoch_guard.disarm();
6394 Ok(())
6395 }
6396
6397 pub fn alter_column(
6398 &self,
6399 table_name: &str,
6400 column_name: &str,
6401 change: AlterColumn,
6402 ) -> Result<ColumnDef> {
6403 use crate::wal::DdlOp;
6404 use std::sync::atomic::Ordering;
6405
6406 self.require(&crate::auth::Permission::Ddl)?;
6407 if self.poisoned.load(Ordering::Relaxed) {
6408 return Err(MongrelError::Other(
6409 "database poisoned by fsync error".into(),
6410 ));
6411 }
6412
6413 let _g = self.ddl_lock.lock();
6414 let table_id = {
6415 let cat = self.catalog.read();
6416 cat.live(table_name)
6417 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
6418 .table_id
6419 };
6420 let handle =
6421 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
6422 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
6423 })?;
6424
6425 let backfill = {
6431 let table = handle.lock();
6432 let old = table
6433 .schema()
6434 .column(column_name)
6435 .cloned()
6436 .ok_or_else(|| MongrelError::Schema(format!("unknown column {column_name}")))?;
6437 let next_flags = change.flags.unwrap_or(old.flags);
6438 if old.flags.contains(crate::schema::ColumnFlags::NULLABLE)
6439 && !next_flags.contains(crate::schema::ColumnFlags::NULLABLE)
6440 && old.default_value.is_some()
6441 {
6442 let snapshot = Snapshot::at(self.epoch.visible());
6443 let mut updates = Vec::new();
6444 for row in table.visible_rows(snapshot)? {
6445 if row
6446 .columns
6447 .get(&old.id)
6448 .is_some_and(|value| !matches!(value, Value::Null))
6449 {
6450 continue;
6451 }
6452 let mut cells: Vec<(u16, Value)> = row.columns.into_iter().collect();
6453 table.apply_defaults(&mut cells)?;
6454 updates.push((table_id, crate::txn::Staged::Update(row.row_id, cells)));
6455 }
6456 updates
6457 } else {
6458 Vec::new()
6459 }
6460 };
6461 if !backfill.is_empty() {
6462 self.commit_transaction_with_external_states(
6463 self.alloc_txn_id(),
6464 self.epoch.visible(),
6465 backfill,
6466 Vec::new(),
6467 Vec::new(),
6468 None,
6469 None,
6470 )?;
6471 }
6472 let mut table = handle.lock();
6473 let column = table.prepare_alter_column(column_name, &change)?;
6474 let renamed_column = (column.name != column_name).then(|| column.name.clone());
6475 if table
6476 .schema()
6477 .columns
6478 .iter()
6479 .find(|c| c.id == column.id)
6480 .is_some_and(|c| c == &column)
6481 {
6482 return Ok(column);
6483 }
6484
6485 let commit_lock = Arc::clone(&self.commit_lock);
6486 let _c = commit_lock.lock();
6487 let epoch = self.epoch.bump_assigned();
6488 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6489 let txn_id = self.alloc_txn_id();
6490 let column_json = DdlOp::encode_column(&column)?;
6491 let commit_seq = {
6492 let mut wal = self.shared_wal.lock();
6493 wal.append(
6494 txn_id,
6495 table_id,
6496 crate::wal::Op::Ddl(DdlOp::AlterTable {
6497 table_id,
6498 column_json,
6499 }),
6500 )?;
6501 wal.append_commit(txn_id, epoch, &[])?
6502 };
6503 self.group
6504 .await_durable(&self.shared_wal, commit_seq)
6505 .inspect_err(|_| {
6506 self.poisoned.store(true, Ordering::Relaxed);
6507 })?;
6508
6509 table.apply_altered_column(column.clone())?;
6510 let schema = table.schema().clone();
6511 drop(table);
6512
6513 {
6514 let mut cat = self.catalog.write();
6515 let entry = cat
6516 .tables
6517 .iter_mut()
6518 .find(|t| t.table_id == table_id)
6519 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
6520 entry.schema = schema;
6521 if let Some(new_column_name) = renamed_column {
6522 for trigger in &mut cat.triggers {
6523 if matches!(
6524 &trigger.trigger.target,
6525 TriggerTarget::Table(target) if target == table_name
6526 ) {
6527 trigger.trigger = trigger.trigger.renamed_update_column(
6528 column_name,
6529 new_column_name.clone(),
6530 epoch.0,
6531 )?;
6532 }
6533 }
6534 for role in &mut cat.roles {
6535 for permission in &mut role.permissions {
6536 rename_permission_column(
6537 permission,
6538 table_name,
6539 column_name,
6540 &new_column_name,
6541 );
6542 }
6543 }
6544 cat.security_version = cat.security_version.wrapping_add(1);
6545 }
6546 }
6547 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
6548
6549 self.epoch.publish_in_order(epoch);
6550 _epoch_guard.disarm();
6551 Ok(column)
6552 }
6553
6554 pub fn set_table_ttl(
6557 &self,
6558 table_name: &str,
6559 column_name: &str,
6560 duration_nanos: u64,
6561 ) -> Result<crate::manifest::TtlPolicy> {
6562 let policy = self.replace_table_ttl(table_name, Some((column_name, duration_nanos)))?;
6563 Ok(policy.expect("set TTL produces a policy"))
6564 }
6565
6566 pub fn clear_table_ttl(&self, table_name: &str) -> Result<()> {
6567 self.replace_table_ttl(table_name, None)?;
6568 Ok(())
6569 }
6570
6571 fn replace_table_ttl(
6572 &self,
6573 table_name: &str,
6574 requested: Option<(&str, u64)>,
6575 ) -> Result<Option<crate::manifest::TtlPolicy>> {
6576 use crate::wal::DdlOp;
6577 use std::sync::atomic::Ordering;
6578
6579 self.require(&crate::auth::Permission::Ddl)?;
6580 if self.poisoned.load(Ordering::Relaxed) {
6581 return Err(MongrelError::Other(
6582 "database poisoned by fsync error".into(),
6583 ));
6584 }
6585
6586 let _g = self.ddl_lock.lock();
6587 let table_id = {
6588 let cat = self.catalog.read();
6589 cat.live(table_name)
6590 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
6591 .table_id
6592 };
6593 let handle =
6594 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
6595 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
6596 })?;
6597 let mut table = handle.lock();
6598 let policy = match requested {
6599 Some((column, duration)) => Some(table.prepare_ttl_policy(column, duration)?),
6600 None => None,
6601 };
6602 if table.ttl() == policy {
6603 return Ok(policy);
6604 }
6605
6606 let commit_lock = Arc::clone(&self.commit_lock);
6607 let _c = commit_lock.lock();
6608 let epoch = self.epoch.bump_assigned();
6609 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6610 let txn_id = self.alloc_txn_id();
6611 let policy_json = DdlOp::encode_ttl(policy)?;
6612 let commit_seq = {
6613 let mut wal = self.shared_wal.lock();
6614 wal.append(
6615 txn_id,
6616 table_id,
6617 crate::wal::Op::Ddl(DdlOp::SetTtl {
6618 table_id,
6619 policy_json,
6620 }),
6621 )?;
6622 wal.append_commit(txn_id, epoch, &[])?
6623 };
6624 self.group
6625 .await_durable(&self.shared_wal, commit_seq)
6626 .inspect_err(|_| {
6627 self.poisoned.store(true, Ordering::Relaxed);
6628 })?;
6629
6630 table.apply_ttl_policy_at(policy, epoch)?;
6631 self.epoch.publish_in_order(epoch);
6632 _epoch_guard.disarm();
6633 Ok(policy)
6634 }
6635
6636 pub fn gc(&self) -> Result<usize> {
6642 self.require(&crate::auth::Permission::Ddl)?;
6643 let min_active = self.snapshots.min_active(self.epoch.visible()).0;
6644 let mut reclaimed = 0;
6645
6646 let cat = self.catalog.read();
6648 for entry in &cat.tables {
6649 if let TableState::Dropped { at_epoch } = entry.state {
6650 if at_epoch <= min_active {
6651 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
6652 if tdir.exists() {
6653 std::fs::remove_dir_all(&tdir)?;
6654 reclaimed += 1;
6655 }
6656 }
6657 }
6658 }
6659 drop(cat);
6660
6661 let cat = self.catalog.read();
6666 for entry in &cat.tables {
6667 if !matches!(entry.state, TableState::Live) {
6668 continue;
6669 }
6670 let txn_dir = self
6671 .root
6672 .join(TABLES_DIR)
6673 .join(entry.table_id.to_string())
6674 .join("_txn");
6675 if !txn_dir.exists() {
6676 continue;
6677 }
6678 for sub in std::fs::read_dir(&txn_dir)? {
6679 let sub = sub?;
6680 let name = sub.file_name();
6681 let Some(name) = name.to_str() else { continue };
6682 let is_active = name
6684 .parse::<u64>()
6685 .map(|id| self.active_spills.is_active(id))
6686 .unwrap_or(false);
6687 if is_active {
6688 continue;
6689 }
6690 std::fs::remove_dir_all(sub.path())?;
6691 reclaimed += 1;
6692 }
6693 }
6694 drop(cat);
6695
6696 let external_names = {
6697 let cat = self.catalog.read();
6698 cat.external_tables
6699 .iter()
6700 .map(|entry| entry.name.clone())
6701 .collect::<std::collections::HashSet<_>>()
6702 };
6703 let vtab_dir = self.root.join(VTAB_DIR);
6704 if vtab_dir.exists() {
6705 for entry in std::fs::read_dir(&vtab_dir)? {
6706 let entry = entry?;
6707 let name = entry.file_name();
6708 let Some(name) = name.to_str() else { continue };
6709 if external_names.contains(name) {
6710 continue;
6711 }
6712 let path = entry.path();
6713 if path.is_dir() {
6714 std::fs::remove_dir_all(path)?;
6715 } else {
6716 std::fs::remove_file(path)?;
6717 }
6718 reclaimed += 1;
6719 }
6720 }
6721
6722 let tables = self.tables.read();
6726 for (table_id, handle) in tables.iter() {
6727 let backup_pinned: HashSet<u128> = self
6728 .backup_pins
6729 .lock()
6730 .keys()
6731 .filter_map(|(pinned_table, run_id)| {
6732 (*pinned_table == *table_id).then_some(*run_id)
6733 })
6734 .collect();
6735 reclaimed += handle
6736 .lock()
6737 .reap_retiring(Epoch(min_active), &backup_pinned)?;
6738 }
6739
6740 let all_durable = self.active_spills.is_idle()
6748 && tables.values().all(|h| {
6749 let g = h.lock();
6750 g.memtable_len() == 0 && g.mutable_run_len() == 0
6751 });
6752 drop(tables);
6753 if all_durable {
6754 let retain = self
6755 .replication_wal_retention_segments
6756 .load(std::sync::atomic::Ordering::Relaxed);
6757 reclaimed += self
6758 .shared_wal
6759 .lock()
6760 .gc_segments_retain_recent(u64::MAX, retain)?;
6761 }
6762
6763 Ok(reclaimed)
6764 }
6765
6766 pub fn checkpoint(&self) -> Result<()> {
6784 self.close()?;
6786
6787 let _ = self.compact()?;
6789
6790 self.gc()?;
6793
6794 {
6801 let wal = self.shared_wal.lock();
6802 let active = wal.active_segment_no();
6803 drop(wal);
6804 let wal_dir = self.root.join("_wal");
6806 if wal_dir.exists() {
6807 for entry in std::fs::read_dir(&wal_dir)? {
6808 let entry = entry?;
6809 let path = entry.path();
6810 if path.extension().is_some_and(|ext| ext == "wal") {
6811 let _ = std::fs::remove_file(&path);
6812 }
6813 }
6814 }
6815 let _ = active; }
6817
6818 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
6820
6821 let tables = self.tables.read();
6824 let visible = self.epoch.visible();
6825 for handle in tables.values() {
6826 handle.lock().persist_manifest(visible)?;
6827 }
6828
6829 Ok(())
6830 }
6831 fn alloc_txn_id(&self) -> u64 {
6832 let mut g = self.next_txn_id.lock();
6833 let id = *g;
6834 *g = g.wrapping_add(1);
6835 id
6836 }
6837
6838 pub fn set_spill_threshold(&self, bytes: u64) {
6842 self.spill_threshold
6843 .store(bytes, std::sync::atomic::Ordering::Relaxed);
6844 }
6845
6846 #[doc(hidden)]
6850 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
6851 *self.spill_hook.lock() = Some(Box::new(f));
6852 }
6853
6854 #[doc(hidden)]
6857 pub fn __set_backup_hook(&self, f: impl Fn() + Send + Sync + 'static) {
6858 *self.backup_hook.lock() = Some(Box::new(f));
6859 }
6860
6861 #[doc(hidden)]
6865 pub fn __wal_group_sync_count(&self) -> u64 {
6866 self.shared_wal.lock().group_sync_count()
6867 }
6868
6869 #[doc(hidden)]
6872 pub fn __poison(&self) {
6873 self.poisoned
6874 .store(true, std::sync::atomic::Ordering::Relaxed);
6875 }
6876
6877 pub fn check(&self) -> Vec<CheckIssue> {
6890 let mut issues = Vec::new();
6891 let cat = self.catalog.read();
6892 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
6893 for entry in &cat.tables {
6894 if !matches!(entry.state, TableState::Live) {
6895 continue;
6896 }
6897 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
6898 let mut err = |sev: &str, desc: String| {
6899 issues.push(CheckIssue {
6900 table_id: entry.table_id,
6901 table_name: entry.name.clone(),
6902 severity: sev.into(),
6903 description: desc,
6904 });
6905 };
6906 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
6907 Ok(m) => m,
6908 Err(e) => {
6909 err("error", format!("manifest read failed: {e}"));
6910 continue;
6911 }
6912 };
6913 if m.flushed_epoch > m.current_epoch {
6914 err(
6915 "error",
6916 format!(
6917 "flushed_epoch {} exceeds current_epoch {} (impossible)",
6918 m.flushed_epoch, m.current_epoch
6919 ),
6920 );
6921 }
6922
6923 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
6924 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
6925 for rr in &m.runs {
6926 referenced.insert(rr.run_id);
6927 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
6928 if !run_path.exists() {
6929 err("error", format!("missing run file: r-{}.sr", rr.run_id));
6930 continue;
6931 }
6932 match crate::sorted_run::RunReader::open(
6933 &run_path,
6934 entry.schema.clone(),
6935 self.kek.clone(),
6936 ) {
6937 Ok(reader) => {
6938 if reader.row_count() as u64 != rr.row_count {
6939 err(
6940 "error",
6941 format!(
6942 "run r-{} row count mismatch: manifest {} vs run {}",
6943 rr.run_id,
6944 rr.row_count,
6945 reader.row_count()
6946 ),
6947 );
6948 }
6949 }
6950 Err(e) => {
6951 err(
6952 "error",
6953 format!("run r-{} integrity check failed: {e}", rr.run_id),
6954 );
6955 }
6956 }
6957 }
6958
6959 for r in &m.retiring {
6963 referenced.insert(r.run_id);
6964 }
6965
6966 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
6968 for ent in rd.flatten() {
6969 let p = ent.path();
6970 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
6971 continue;
6972 }
6973 let run_id = p
6974 .file_stem()
6975 .and_then(|s| s.to_str())
6976 .and_then(|s| s.strip_prefix("r-"))
6977 .and_then(|s| s.parse::<u128>().ok());
6978 if let Some(id) = run_id {
6979 if !referenced.contains(&id) {
6980 err(
6981 "warning",
6982 format!("orphan run file r-{id}.sr not referenced by the manifest"),
6983 );
6984 }
6985 }
6986 }
6987 }
6988 }
6989
6990 let external_names = cat
6991 .external_tables
6992 .iter()
6993 .map(|entry| entry.name.clone())
6994 .collect::<std::collections::HashSet<_>>();
6995 let vtab_dir = self.root.join(VTAB_DIR);
6996 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
6997 for entry in entries.flatten() {
6998 let name = entry.file_name();
6999 let Some(name) = name.to_str() else { continue };
7000 if !external_names.contains(name) {
7001 issues.push(CheckIssue {
7002 table_id: EXTERNAL_TABLE_ID,
7003 table_name: name.to_string(),
7004 severity: "warning".into(),
7005 description: format!(
7006 "orphan external table state entry {:?} not referenced by the catalog",
7007 entry.path()
7008 ),
7009 });
7010 }
7011 }
7012 }
7013
7014 for (seg, msg) in self.shared_wal.lock().verify_segments() {
7021 issues.push(CheckIssue {
7022 table_id: WAL_TABLE_ID,
7023 table_name: "<wal>".into(),
7024 severity: "error".into(),
7025 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
7026 });
7027 }
7028 issues
7029 }
7030
7031 pub fn doctor(&self) -> Result<Vec<u64>> {
7035 let _ddl = self.ddl_lock.lock();
7038 let issues = self.check();
7039 let bad_tables: std::collections::HashSet<u64> = issues
7044 .iter()
7045 .filter(|i| {
7046 i.severity == "error"
7047 && i.table_id != WAL_TABLE_ID
7048 && i.table_id != EXTERNAL_TABLE_ID
7049 })
7050 .map(|i| i.table_id)
7051 .collect();
7052 if bad_tables.is_empty() {
7053 return Ok(Vec::new());
7054 }
7055
7056 let qdir = self.root.join("_quarantine");
7057 std::fs::create_dir_all(&qdir)?;
7058 let mut quarantined = Vec::new();
7059 for &table_id in &bad_tables {
7060 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
7061 if tdir.exists() {
7062 let dest = qdir.join(table_id.to_string());
7063 std::fs::rename(&tdir, &dest)?;
7064 }
7065 {
7066 let mut cat = self.catalog.write();
7067 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
7068 entry.state = TableState::Dropped {
7069 at_epoch: self.epoch.visible().0,
7070 };
7071 }
7072 }
7073 self.tables.write().remove(&table_id);
7075 quarantined.push(table_id);
7076 }
7077 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
7078 Ok(quarantined)
7079 }
7080
7081 #[allow(dead_code)]
7083 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
7084 self.kek.as_ref()
7085 }
7086
7087 #[allow(dead_code)]
7089 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
7090 &self.epoch
7091 }
7092
7093 #[allow(dead_code)]
7095 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
7096 &self.snapshots
7097 }
7098}
7099
7100fn external_state_dir(root: &Path, name: &str) -> PathBuf {
7101 root.join(VTAB_DIR).join(name)
7102}
7103
7104fn filter_ignored_staging(
7105 staging: Vec<(u64, crate::txn::Staged)>,
7106 ignored_indices: &std::collections::BTreeSet<usize>,
7107) -> Vec<(u64, crate::txn::Staged)> {
7108 if ignored_indices.is_empty() {
7109 return staging;
7110 }
7111 staging
7112 .into_iter()
7113 .enumerate()
7114 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
7115 .collect()
7116}
7117
7118fn external_state_file(root: &Path, name: &str) -> PathBuf {
7119 external_state_dir(root, name).join("state.json")
7120}
7121
7122fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
7123 let path = external_state_file(root, name);
7124 match std::fs::read(path) {
7125 Ok(bytes) => Ok(bytes),
7126 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
7127 Err(e) => Err(e.into()),
7128 }
7129}
7130
7131fn current_external_state_bytes(
7132 root: &Path,
7133 external_states: &[(String, Vec<u8>)],
7134 name: &str,
7135) -> Result<Vec<u8>> {
7136 for (table, state) in external_states.iter().rev() {
7137 if table == name {
7138 return Ok(state.clone());
7139 }
7140 }
7141 read_external_state_file(root, name)
7142}
7143
7144fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
7145 let mut out = external_states;
7146 dedup_external_states_in_place(&mut out);
7147 out
7148}
7149
7150fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
7151 let mut seen = std::collections::HashSet::new();
7152 let mut out = Vec::with_capacity(external_states.len());
7153 for (name, state) in std::mem::take(external_states).into_iter().rev() {
7154 if seen.insert(name.clone()) {
7155 out.push((name, state));
7156 }
7157 }
7158 out.reverse();
7159 *external_states = out;
7160}
7161
7162fn prepare_external_state_file(
7163 root: &Path,
7164 name: &str,
7165 state: &[u8],
7166 txn_id: u64,
7167) -> Result<PathBuf> {
7168 let dir = external_state_dir(root, name);
7169 std::fs::create_dir_all(&dir)?;
7170 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
7171 {
7172 let mut file = std::fs::File::create(&pending)?;
7173 file.write_all(state)?;
7174 file.sync_all()?;
7175 }
7176 Ok(pending)
7177}
7178
7179fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
7180 let path = external_state_file(root, name);
7181 std::fs::rename(pending, &path)?;
7182 if let Ok(dir) = std::fs::File::open(external_state_dir(root, name)) {
7183 let _ = dir.sync_all();
7184 }
7185 Ok(())
7186}
7187
7188fn write_external_state_file(root: &Path, name: &str, state: &[u8]) -> Result<()> {
7189 let pending = prepare_external_state_file(root, name, state, 0)?;
7190 publish_external_state_file(root, name, &pending)
7191}
7192
7193fn recover_shared_wal(
7202 root: &Path,
7203 tables: &HashMap<u64, TableHandle>,
7204 epoch: &EpochAuthority,
7205 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
7206) -> Result<()> {
7207 use crate::memtable::Row;
7208 use crate::rowid::RowId;
7209 use crate::wal::{Op, SharedWal};
7210
7211 let records = SharedWal::replay_with_dek(root, wal_dek)?;
7212
7213 let mut committed: HashMap<u64, u64> = HashMap::new();
7215 let mut spilled_to_link: Vec<(
7216 u64, u64, Vec<crate::wal::AddedRun>,
7219 )> = Vec::new();
7220 for r in &records {
7221 if let Op::TxnCommit {
7222 epoch: ce,
7223 ref added_runs,
7224 } = r.op
7225 {
7226 committed.insert(r.txn_id, ce);
7227 if !added_runs.is_empty() {
7228 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
7229 }
7230 }
7231 }
7232 let truncated_transactions: HashSet<(u64, u64)> = records
7233 .iter()
7234 .filter_map(|record| {
7235 committed.get(&record.txn_id)?;
7236 match record.op {
7237 Op::TruncateTable { table_id } => Some((record.txn_id, table_id)),
7238 _ => None,
7239 }
7240 })
7241 .collect();
7242
7243 type TableStage = (Vec<Row>, Vec<(RowId, Epoch)>, Option<Epoch>, Epoch);
7245 let mut stage: HashMap<u64, TableStage> = HashMap::new();
7246 let mut max_epoch = epoch.visible().0;
7247 for r in records {
7248 let Some(&ce) = committed.get(&r.txn_id) else {
7249 continue; };
7251 let commit_epoch = Epoch(ce);
7252 max_epoch = max_epoch.max(ce);
7253 match r.op {
7254 Op::Put { table_id, rows } => {
7255 let skip = tables
7257 .get(&table_id)
7258 .map(|h| h.lock().flushed_epoch() >= ce)
7259 .unwrap_or(true);
7260 if skip {
7261 continue;
7262 }
7263 let rows: Vec<Row> = match bincode::deserialize(&rows) {
7264 Ok(v) => v,
7265 Err(_) => continue,
7266 };
7267 let rows: Vec<Row> = rows
7270 .into_iter()
7271 .map(|mut row| {
7272 row.committed_epoch = commit_epoch;
7273 row
7274 })
7275 .collect();
7276 let entry = stage
7277 .entry(table_id)
7278 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
7279 entry.0.extend(rows);
7280 entry.3 = commit_epoch;
7281 }
7282 Op::Delete { table_id, row_ids } => {
7283 let skip = tables
7284 .get(&table_id)
7285 .map(|h| h.lock().flushed_epoch() >= ce)
7286 .unwrap_or(true);
7287 if skip {
7288 continue;
7289 }
7290 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
7291 let entry = stage
7292 .entry(table_id)
7293 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
7294 entry.1.extend(dels);
7295 entry.3 = commit_epoch;
7296 }
7297 Op::TruncateTable { table_id } => {
7298 let skip = tables
7299 .get(&table_id)
7300 .map(|h| h.lock().flushed_epoch() >= ce)
7301 .unwrap_or(true);
7302 if skip {
7303 continue;
7304 }
7305 stage.insert(
7306 table_id,
7307 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
7308 );
7309 }
7310 Op::ExternalTableState { name, state } => {
7311 write_external_state_file(root, &name, &state)?;
7312 }
7313 Op::Flush { .. }
7314 | Op::TxnCommit { .. }
7315 | Op::TxnAbort
7316 | Op::Ddl(_)
7317 | Op::BeforeImage { .. }
7318 | Op::CommitTimestamp { .. } => {}
7319 }
7320 }
7321 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
7322 let Some(handle) = tables.get(&table_id) else {
7323 continue;
7324 };
7325 let mut t = handle.lock();
7326 if let Some(epoch) = truncate_epoch {
7327 t.apply_truncate(epoch)?;
7328 }
7329 t.recover_apply(rows, deletes)?;
7330 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
7334 t.live_count = rows.len() as u64;
7335 t.persist_manifest(table_epoch)?;
7336 }
7337
7338 for (txn_id, ce, added_runs) in &spilled_to_link {
7342 for ar in added_runs {
7343 let Some(handle) = tables.get(&ar.table_id) else {
7344 continue;
7345 };
7346 let mut t = handle.lock();
7347 let dest = t.run_path(ar.run_id as u64);
7348 if !dest.exists() {
7349 let pending = root
7350 .join(TABLES_DIR)
7351 .join(ar.table_id.to_string())
7352 .join("_txn")
7353 .join(txn_id.to_string())
7354 .join(format!("r-{}.sr", ar.run_id));
7355 if pending.exists() {
7356 if let Some(parent) = pending.parent() {
7357 std::fs::rename(&pending, &dest)?;
7358 let _ = std::fs::remove_dir_all(parent);
7359 }
7360 }
7361 }
7362 if t.run_path(ar.run_id as u64).exists() {
7368 let linked = t.recover_spilled_run(crate::manifest::RunRef {
7369 run_id: ar.run_id,
7370 level: ar.level,
7371 epoch_created: *ce,
7372 row_count: ar.row_count,
7373 });
7374 let replaced = truncated_transactions.contains(&(*txn_id, ar.table_id));
7375 if replaced {
7376 t.set_flushed_epoch(Epoch(*ce));
7377 }
7378 if linked || replaced {
7379 t.persist_manifest(Epoch(*ce))?;
7380 }
7381 }
7382 }
7383 }
7384
7385 epoch.advance_recovered(Epoch(max_epoch));
7386 Ok(())
7387}
7388
7389fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
7390 match condition {
7391 ProcedureCondition::Pk { .. } => {
7392 if schema.primary_key().is_none() {
7393 return Err(MongrelError::InvalidArgument(
7394 "procedure condition Pk references a table without a primary key".into(),
7395 ));
7396 }
7397 }
7398 ProcedureCondition::BitmapEq { column_id, .. }
7399 | ProcedureCondition::BitmapIn { column_id, .. }
7400 | ProcedureCondition::Range { column_id, .. }
7401 | ProcedureCondition::RangeF64 { column_id, .. }
7402 | ProcedureCondition::IsNull { column_id }
7403 | ProcedureCondition::IsNotNull { column_id }
7404 | ProcedureCondition::FmContains { column_id, .. } => {
7405 validate_column_id(*column_id, schema)?;
7406 }
7407 }
7408 Ok(())
7409}
7410
7411fn bind_procedure_args(
7412 procedure: &StoredProcedure,
7413 mut args: HashMap<String, crate::Value>,
7414) -> Result<HashMap<String, crate::Value>> {
7415 let mut out = HashMap::new();
7416 for param in &procedure.params {
7417 let value = match args.remove(¶m.name) {
7418 Some(value) => value,
7419 None => param.default.clone().ok_or_else(|| {
7420 MongrelError::InvalidArgument(format!(
7421 "missing required procedure parameter {:?}",
7422 param.name
7423 ))
7424 })?,
7425 };
7426 if !param.nullable && matches!(value, crate::Value::Null) {
7427 return Err(MongrelError::InvalidArgument(format!(
7428 "procedure parameter {:?} must not be NULL",
7429 param.name
7430 )));
7431 }
7432 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty.clone()) {
7433 return Err(MongrelError::InvalidArgument(format!(
7434 "procedure parameter {:?} has wrong type",
7435 param.name
7436 )));
7437 }
7438 out.insert(param.name.clone(), value);
7439 }
7440 if let Some(extra) = args.keys().next() {
7441 return Err(MongrelError::InvalidArgument(format!(
7442 "unknown procedure parameter {extra:?}"
7443 )));
7444 }
7445 Ok(out)
7446}
7447
7448fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
7449 matches!(
7450 (value, ty),
7451 (crate::Value::Bool(_), crate::TypeId::Bool)
7452 | (crate::Value::Int64(_), crate::TypeId::Int8)
7453 | (crate::Value::Int64(_), crate::TypeId::Int16)
7454 | (crate::Value::Int64(_), crate::TypeId::Int32)
7455 | (crate::Value::Int64(_), crate::TypeId::Int64)
7456 | (crate::Value::Int64(_), crate::TypeId::UInt8)
7457 | (crate::Value::Int64(_), crate::TypeId::UInt16)
7458 | (crate::Value::Int64(_), crate::TypeId::UInt32)
7459 | (crate::Value::Int64(_), crate::TypeId::UInt64)
7460 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
7461 | (crate::Value::Int64(_), crate::TypeId::Date32)
7462 | (crate::Value::Float64(_), crate::TypeId::Float32)
7463 | (crate::Value::Float64(_), crate::TypeId::Float64)
7464 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
7465 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
7466 )
7467}
7468
7469fn eval_cells(
7470 cells: &[crate::procedure::ProcedureCell],
7471 args: &HashMap<String, crate::Value>,
7472 outputs: &HashMap<String, ProcedureCallOutput>,
7473) -> Result<Vec<(u16, crate::Value)>> {
7474 cells
7475 .iter()
7476 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
7477 .collect()
7478}
7479
7480fn eval_condition(
7481 condition: &ProcedureCondition,
7482 args: &HashMap<String, crate::Value>,
7483 outputs: &HashMap<String, ProcedureCallOutput>,
7484) -> Result<crate::Condition> {
7485 Ok(match condition {
7486 ProcedureCondition::Pk { value } => {
7487 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
7488 }
7489 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
7490 column_id: *column_id,
7491 value: eval_value(value, args, outputs)?.encode_key(),
7492 },
7493 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
7494 column_id: *column_id,
7495 values: values
7496 .iter()
7497 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
7498 .collect::<Result<Vec<_>>>()?,
7499 },
7500 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
7501 column_id: *column_id,
7502 lo: expect_i64(eval_value(lo, args, outputs)?)?,
7503 hi: expect_i64(eval_value(hi, args, outputs)?)?,
7504 },
7505 ProcedureCondition::RangeF64 {
7506 column_id,
7507 lo,
7508 lo_inclusive,
7509 hi,
7510 hi_inclusive,
7511 } => crate::Condition::RangeF64 {
7512 column_id: *column_id,
7513 lo: expect_f64(eval_value(lo, args, outputs)?)?,
7514 lo_inclusive: *lo_inclusive,
7515 hi: expect_f64(eval_value(hi, args, outputs)?)?,
7516 hi_inclusive: *hi_inclusive,
7517 },
7518 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
7519 column_id: *column_id,
7520 },
7521 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
7522 column_id: *column_id,
7523 },
7524 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
7525 column_id: *column_id,
7526 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
7527 },
7528 })
7529}
7530
7531fn eval_value(
7532 value: &ProcedureValue,
7533 args: &HashMap<String, crate::Value>,
7534 outputs: &HashMap<String, ProcedureCallOutput>,
7535) -> Result<crate::Value> {
7536 match value {
7537 ProcedureValue::Literal(value) => Ok(value.clone()),
7538 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
7539 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
7540 }),
7541 ProcedureValue::StepScalar(id) => match outputs.get(id) {
7542 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
7543 _ => Err(MongrelError::InvalidArgument(format!(
7544 "procedure step {id:?} did not return a scalar"
7545 ))),
7546 },
7547 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
7548 Err(MongrelError::InvalidArgument(
7549 "row-valued procedure reference cannot be used as a scalar".into(),
7550 ))
7551 }
7552 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
7553 "structured procedure value cannot be used as a scalar cell".into(),
7554 )),
7555 }
7556}
7557
7558fn eval_return_output(
7559 value: &ProcedureValue,
7560 args: &HashMap<String, crate::Value>,
7561 outputs: &HashMap<String, ProcedureCallOutput>,
7562) -> Result<ProcedureCallOutput> {
7563 match value {
7564 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
7565 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
7566 args.get(name).cloned().ok_or_else(|| {
7567 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
7568 })?,
7569 )),
7570 ProcedureValue::StepRows(id)
7571 | ProcedureValue::StepRow(id)
7572 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
7573 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
7574 }),
7575 ProcedureValue::Object(fields) => {
7576 let mut out = Vec::with_capacity(fields.len());
7577 for (name, value) in fields {
7578 out.push((name.clone(), eval_return_output(value, args, outputs)?));
7579 }
7580 Ok(ProcedureCallOutput::Object(out))
7581 }
7582 ProcedureValue::Array(values) => {
7583 let mut out = Vec::with_capacity(values.len());
7584 for value in values {
7585 out.push(eval_return_output(value, args, outputs)?);
7586 }
7587 Ok(ProcedureCallOutput::Array(out))
7588 }
7589 }
7590}
7591
7592fn expect_i64(value: crate::Value) -> Result<i64> {
7593 match value {
7594 crate::Value::Int64(value) => Ok(value),
7595 _ => Err(MongrelError::InvalidArgument(
7596 "procedure value must be Int64".into(),
7597 )),
7598 }
7599}
7600
7601fn expect_f64(value: crate::Value) -> Result<f64> {
7602 match value {
7603 crate::Value::Float64(value) => Ok(value),
7604 _ => Err(MongrelError::InvalidArgument(
7605 "procedure value must be Float64".into(),
7606 )),
7607 }
7608}
7609
7610fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
7611 match value {
7612 crate::Value::Bytes(value) => Ok(value),
7613 _ => Err(MongrelError::InvalidArgument(
7614 "procedure value must be Bytes".into(),
7615 )),
7616 }
7617}
7618
7619fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
7620 if schema.columns.iter().any(|c| c.id == column_id) {
7621 Ok(())
7622 } else {
7623 Err(MongrelError::InvalidArgument(format!(
7624 "unknown column id {column_id}"
7625 )))
7626 }
7627}
7628
7629fn trigger_matches_event(
7630 trigger: &StoredTrigger,
7631 event: &WriteEvent,
7632 cat: &Catalog,
7633) -> Result<bool> {
7634 if trigger.event != event.kind {
7635 return Ok(false);
7636 }
7637 let TriggerTarget::Table(target) = &trigger.target else {
7638 return Ok(false);
7639 };
7640 if target != &event.table {
7641 return Ok(false);
7642 }
7643 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
7644 let schema = &cat
7645 .live(target)
7646 .ok_or_else(|| {
7647 MongrelError::InvalidArgument(format!(
7648 "trigger {:?} references unknown table {target:?}",
7649 trigger.name
7650 ))
7651 })?
7652 .schema;
7653 let mut watched = Vec::with_capacity(trigger.update_of.len());
7654 for name in &trigger.update_of {
7655 let col = schema.column(name).ok_or_else(|| {
7656 MongrelError::InvalidArgument(format!(
7657 "trigger {:?} references unknown UPDATE OF column {name:?}",
7658 trigger.name
7659 ))
7660 })?;
7661 watched.push(col.id);
7662 }
7663 if !event
7664 .changed_columns
7665 .iter()
7666 .any(|column_id| watched.contains(column_id))
7667 {
7668 return Ok(false);
7669 }
7670 }
7671 Ok(true)
7672}
7673
7674fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
7675 let mut ids = std::collections::BTreeSet::new();
7676 if let Some(old) = old {
7677 ids.extend(old.columns.keys().copied());
7678 }
7679 if let Some(new) = new {
7680 ids.extend(new.columns.keys().copied());
7681 }
7682 ids.into_iter()
7683 .filter(|id| {
7684 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
7685 })
7686 .collect()
7687}
7688
7689fn eval_trigger_cells(
7690 cells: &[crate::trigger::TriggerCell],
7691 event: &WriteEvent,
7692 selected: Option<&TriggerRowImage>,
7693) -> Result<Vec<(u16, Value)>> {
7694 cells
7695 .iter()
7696 .map(|cell| {
7697 Ok((
7698 cell.column_id,
7699 eval_trigger_value(&cell.value, event, selected)?,
7700 ))
7701 })
7702 .collect()
7703}
7704
7705fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
7706 match expr {
7707 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
7708 Value::Bool(value) => Ok(value),
7709 Value::Null => Ok(false),
7710 other => Err(MongrelError::InvalidArgument(format!(
7711 "trigger WHEN value must be boolean, got {other:?}"
7712 ))),
7713 },
7714 TriggerExpr::Eq { left, right } => Ok(values_equal(
7715 &eval_trigger_value(left, event, None)?,
7716 &eval_trigger_value(right, event, None)?,
7717 )),
7718 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
7719 &eval_trigger_value(left, event, None)?,
7720 &eval_trigger_value(right, event, None)?,
7721 )),
7722 TriggerExpr::Lt { left, right } => match value_order(
7723 &eval_trigger_value(left, event, None)?,
7724 &eval_trigger_value(right, event, None)?,
7725 ) {
7726 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
7727 None => Ok(false),
7728 },
7729 TriggerExpr::Lte { left, right } => match value_order(
7730 &eval_trigger_value(left, event, None)?,
7731 &eval_trigger_value(right, event, None)?,
7732 ) {
7733 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
7734 None => Ok(false),
7735 },
7736 TriggerExpr::Gt { left, right } => match value_order(
7737 &eval_trigger_value(left, event, None)?,
7738 &eval_trigger_value(right, event, None)?,
7739 ) {
7740 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
7741 None => Ok(false),
7742 },
7743 TriggerExpr::Gte { left, right } => match value_order(
7744 &eval_trigger_value(left, event, None)?,
7745 &eval_trigger_value(right, event, None)?,
7746 ) {
7747 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
7748 None => Ok(false),
7749 },
7750 TriggerExpr::IsNull(value) => Ok(matches!(
7751 eval_trigger_value(value, event, None)?,
7752 Value::Null
7753 )),
7754 TriggerExpr::IsNotNull(value) => Ok(!matches!(
7755 eval_trigger_value(value, event, None)?,
7756 Value::Null
7757 )),
7758 TriggerExpr::And { left, right } => {
7759 if !eval_trigger_expr(left, event)? {
7760 Ok(false)
7761 } else {
7762 Ok(eval_trigger_expr(right, event)?)
7763 }
7764 }
7765 TriggerExpr::Or { left, right } => {
7766 if eval_trigger_expr(left, event)? {
7767 Ok(true)
7768 } else {
7769 Ok(eval_trigger_expr(right, event)?)
7770 }
7771 }
7772 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
7773 }
7774}
7775
7776fn eval_trigger_condition(
7777 condition: &TriggerCondition,
7778 event: &WriteEvent,
7779 selected: &TriggerRowImage,
7780 schema: &Schema,
7781) -> Result<bool> {
7782 match condition {
7783 TriggerCondition::Pk { value } => {
7784 let pk = schema.primary_key().ok_or_else(|| {
7785 MongrelError::InvalidArgument(
7786 "trigger condition Pk references a table without a primary key".into(),
7787 )
7788 })?;
7789 let lhs = eval_trigger_value(value, event, Some(selected))?;
7790 Ok(values_equal(
7791 &lhs,
7792 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
7793 ))
7794 }
7795 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
7796 selected.columns.get(column_id).unwrap_or(&Value::Null),
7797 &eval_trigger_value(value, event, Some(selected))?,
7798 )),
7799 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
7800 selected.columns.get(column_id).unwrap_or(&Value::Null),
7801 &eval_trigger_value(value, event, Some(selected))?,
7802 )),
7803 TriggerCondition::Lt { column_id, value } => match value_order(
7804 selected.columns.get(column_id).unwrap_or(&Value::Null),
7805 &eval_trigger_value(value, event, Some(selected))?,
7806 ) {
7807 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
7808 None => Ok(false),
7809 },
7810 TriggerCondition::Lte { column_id, value } => match value_order(
7811 selected.columns.get(column_id).unwrap_or(&Value::Null),
7812 &eval_trigger_value(value, event, Some(selected))?,
7813 ) {
7814 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
7815 None => Ok(false),
7816 },
7817 TriggerCondition::Gt { column_id, value } => match value_order(
7818 selected.columns.get(column_id).unwrap_or(&Value::Null),
7819 &eval_trigger_value(value, event, Some(selected))?,
7820 ) {
7821 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
7822 None => Ok(false),
7823 },
7824 TriggerCondition::Gte { column_id, value } => match value_order(
7825 selected.columns.get(column_id).unwrap_or(&Value::Null),
7826 &eval_trigger_value(value, event, Some(selected))?,
7827 ) {
7828 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
7829 None => Ok(false),
7830 },
7831 TriggerCondition::IsNull { column_id } => Ok(matches!(
7832 selected.columns.get(column_id),
7833 None | Some(Value::Null)
7834 )),
7835 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
7836 selected.columns.get(column_id),
7837 None | Some(Value::Null)
7838 )),
7839 TriggerCondition::And { left, right } => {
7840 if !eval_trigger_condition(left, event, selected, schema)? {
7841 Ok(false)
7842 } else {
7843 Ok(eval_trigger_condition(right, event, selected, schema)?)
7844 }
7845 }
7846 TriggerCondition::Or { left, right } => {
7847 if eval_trigger_condition(left, event, selected, schema)? {
7848 Ok(true)
7849 } else {
7850 Ok(eval_trigger_condition(right, event, selected, schema)?)
7851 }
7852 }
7853 TriggerCondition::Not(condition) => {
7854 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
7855 }
7856 }
7857}
7858
7859fn eval_trigger_value(
7860 value: &TriggerValue,
7861 event: &WriteEvent,
7862 selected: Option<&TriggerRowImage>,
7863) -> Result<Value> {
7864 match value {
7865 TriggerValue::Literal(value) => Ok(value.clone()),
7866 TriggerValue::NewColumn(column_id) => event
7867 .new
7868 .as_ref()
7869 .and_then(|row| row.columns.get(column_id))
7870 .cloned()
7871 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
7872 TriggerValue::OldColumn(column_id) => event
7873 .old
7874 .as_ref()
7875 .and_then(|row| row.columns.get(column_id))
7876 .cloned()
7877 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
7878 TriggerValue::SelectedColumn(column_id) => selected
7879 .and_then(|row| row.columns.get(column_id))
7880 .cloned()
7881 .ok_or_else(|| {
7882 MongrelError::InvalidArgument("SELECTED column is not available".into())
7883 }),
7884 }
7885}
7886
7887fn values_equal(left: &Value, right: &Value) -> bool {
7888 match (left, right) {
7889 (Value::Null, Value::Null) => true,
7890 (Value::Bool(a), Value::Bool(b)) => a == b,
7891 (Value::Int64(a), Value::Int64(b)) => a == b,
7892 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
7893 (Value::Bytes(a), Value::Bytes(b)) => a == b,
7894 (Value::Embedding(a), Value::Embedding(b)) => {
7895 a.len() == b.len()
7896 && a.iter()
7897 .zip(b.iter())
7898 .all(|(a, b)| a.to_bits() == b.to_bits())
7899 }
7900 _ => false,
7901 }
7902}
7903
7904fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
7905 match (left, right) {
7906 (Value::Null, _) | (_, Value::Null) => None,
7907 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
7908 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
7909 (Value::Int64(a), Value::Float64(b)) => {
7912 let af = *a as f64;
7913 Some(af.total_cmp(b))
7914 }
7915 (Value::Float64(a), Value::Int64(b)) => {
7918 let bf = *b as f64;
7919 Some(a.total_cmp(&bf))
7920 }
7921 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
7922 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
7923 (Value::Embedding(_), Value::Embedding(_)) => None,
7924 _ => None,
7925 }
7926}
7927
7928fn trigger_message(value: Value) -> String {
7929 match value {
7930 Value::Null => "NULL".into(),
7931 Value::Bool(value) => value.to_string(),
7932 Value::Int64(value) => value.to_string(),
7933 Value::Float64(value) => value.to_string(),
7934 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
7935 Value::Embedding(value) => format!("{value:?}"),
7936 Value::Decimal(value) => value.to_string(),
7937 Value::Interval {
7938 months,
7939 days,
7940 nanos,
7941 } => format!("{months}m {days}d {nanos}ns"),
7942 Value::Uuid(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
7943 Value::Json(b) => String::from_utf8_lossy(&b).into_owned(),
7944 }
7945}
7946
7947fn validate_trigger_step<'a>(
7948 step: &TriggerStep,
7949 cat: &'a Catalog,
7950 target_schema: &Schema,
7951 event: TriggerEvent,
7952 select_schemas: &mut HashMap<String, &'a Schema>,
7953) -> Result<()> {
7954 match step {
7955 TriggerStep::SetNew { cells } => {
7956 if event == TriggerEvent::Delete {
7957 return Err(MongrelError::InvalidArgument(
7958 "SetNew trigger step is not valid for DELETE triggers".into(),
7959 ));
7960 }
7961 for cell in cells {
7962 validate_column_id(cell.column_id, target_schema)?;
7963 validate_trigger_value(&cell.value, target_schema, event)?;
7964 }
7965 }
7966 TriggerStep::Insert { table, cells } => {
7967 let schema = trigger_write_schema(cat, table, "insert")?;
7968 for cell in cells {
7969 validate_column_id(cell.column_id, schema)?;
7970 validate_trigger_value(&cell.value, target_schema, event)?;
7971 }
7972 }
7973 TriggerStep::UpdateByPk { table, pk, cells } => {
7974 let schema = trigger_write_schema(cat, table, "update")?;
7975 if schema.primary_key().is_none() {
7976 return Err(MongrelError::InvalidArgument(format!(
7977 "trigger update_by_pk references table {table:?} without a primary key"
7978 )));
7979 }
7980 validate_trigger_value(pk, target_schema, event)?;
7981 for cell in cells {
7982 validate_column_id(cell.column_id, schema)?;
7983 validate_trigger_value(&cell.value, target_schema, event)?;
7984 }
7985 }
7986 TriggerStep::DeleteByPk { table, pk } => {
7987 let schema = trigger_write_schema(cat, table, "delete")?;
7988 if schema.primary_key().is_none() {
7989 return Err(MongrelError::InvalidArgument(format!(
7990 "trigger delete_by_pk references table {table:?} without a primary key"
7991 )));
7992 }
7993 validate_trigger_value(pk, target_schema, event)?;
7994 }
7995 TriggerStep::Select {
7996 id,
7997 table,
7998 conditions,
7999 } => {
8000 let schema = trigger_read_schema(cat, table)?;
8001 for condition in conditions {
8002 validate_trigger_condition(condition, schema, target_schema, event)?;
8003 }
8004 if select_schemas.contains_key(id) {
8005 return Err(MongrelError::InvalidArgument(format!(
8006 "duplicate select id {id:?} in trigger program"
8007 )));
8008 }
8009 select_schemas.insert(id.clone(), schema);
8010 }
8011 TriggerStep::Foreach { id, steps } => {
8012 if !select_schemas.contains_key(id) {
8013 return Err(MongrelError::InvalidArgument(format!(
8014 "foreach references unknown select id {id:?}"
8015 )));
8016 }
8017 let mut inner_select_schemas = select_schemas.clone();
8018 for step in steps {
8019 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
8020 }
8021 }
8022 TriggerStep::DeleteWhere { table, conditions } => {
8023 let schema = trigger_write_schema(cat, table, "delete")?;
8024 for condition in conditions {
8025 validate_trigger_condition(condition, schema, target_schema, event)?;
8026 }
8027 }
8028 TriggerStep::UpdateWhere {
8029 table,
8030 conditions,
8031 cells,
8032 } => {
8033 let schema = trigger_write_schema(cat, table, "update")?;
8034 for condition in conditions {
8035 validate_trigger_condition(condition, schema, target_schema, event)?;
8036 }
8037 for cell in cells {
8038 validate_column_id(cell.column_id, schema)?;
8039 validate_trigger_value(&cell.value, target_schema, event)?;
8040 }
8041 }
8042 TriggerStep::Raise { message, .. } => {
8043 validate_trigger_value(message, target_schema, event)?
8044 }
8045 }
8046 Ok(())
8047}
8048
8049fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
8050 if let Some(entry) = cat.live(table) {
8051 return Ok(&entry.schema);
8052 }
8053 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
8054 let allowed = match op {
8055 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
8056 "update" | "delete" => entry.capabilities.writable,
8057 _ => false,
8058 };
8059 if !allowed {
8060 return Err(MongrelError::InvalidArgument(format!(
8061 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
8062 entry.module
8063 )));
8064 }
8065 if !entry.capabilities.transaction_safe {
8066 return Err(MongrelError::InvalidArgument(format!(
8067 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
8068 entry.module
8069 )));
8070 }
8071 return Ok(&entry.declared_schema);
8072 }
8073 Err(MongrelError::InvalidArgument(format!(
8074 "trigger references unknown table {table:?}"
8075 )))
8076}
8077
8078fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
8079 if let Some(entry) = cat.live(table) {
8080 return Ok(&entry.schema);
8081 }
8082 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
8083 if entry.capabilities.trigger_safe {
8084 return Ok(&entry.declared_schema);
8085 }
8086 return Err(MongrelError::InvalidArgument(format!(
8087 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
8088 entry.module
8089 )));
8090 }
8091 Err(MongrelError::InvalidArgument(format!(
8092 "trigger references unknown table {table:?}"
8093 )))
8094}
8095
8096fn validate_trigger_condition(
8097 condition: &TriggerCondition,
8098 schema: &Schema,
8099 target_schema: &Schema,
8100 event: TriggerEvent,
8101) -> Result<()> {
8102 match condition {
8103 TriggerCondition::Pk { value } => {
8104 if schema.primary_key().is_none() {
8105 return Err(MongrelError::InvalidArgument(
8106 "trigger condition Pk references a table without a primary key".into(),
8107 ));
8108 }
8109 validate_trigger_value(value, target_schema, event)
8110 }
8111 TriggerCondition::Eq { column_id, value }
8112 | TriggerCondition::NotEq { column_id, value }
8113 | TriggerCondition::Lt { column_id, value }
8114 | TriggerCondition::Lte { column_id, value }
8115 | TriggerCondition::Gt { column_id, value }
8116 | TriggerCondition::Gte { column_id, value } => {
8117 validate_column_id(*column_id, schema)?;
8118 validate_trigger_value(value, target_schema, event)
8119 }
8120 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
8121 validate_column_id(*column_id, schema)
8122 }
8123 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
8124 validate_trigger_condition(left, schema, target_schema, event)?;
8125 validate_trigger_condition(right, schema, target_schema, event)
8126 }
8127 TriggerCondition::Not(condition) => {
8128 validate_trigger_condition(condition, schema, target_schema, event)
8129 }
8130 }
8131}
8132
8133fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
8134 match expr {
8135 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
8136 validate_trigger_value(value, schema, event)
8137 }
8138 TriggerExpr::Eq { left, right }
8139 | TriggerExpr::NotEq { left, right }
8140 | TriggerExpr::Lt { left, right }
8141 | TriggerExpr::Lte { left, right }
8142 | TriggerExpr::Gt { left, right }
8143 | TriggerExpr::Gte { left, right } => {
8144 validate_trigger_value(left, schema, event)?;
8145 validate_trigger_value(right, schema, event)
8146 }
8147 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
8148 validate_trigger_expr(left, schema, event)?;
8149 validate_trigger_expr(right, schema, event)
8150 }
8151 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
8152 }
8153}
8154
8155fn validate_trigger_value(
8156 value: &TriggerValue,
8157 schema: &Schema,
8158 event: TriggerEvent,
8159) -> Result<()> {
8160 match value {
8161 TriggerValue::Literal(_) => Ok(()),
8162 TriggerValue::NewColumn(id) => {
8163 if event == TriggerEvent::Delete {
8164 return Err(MongrelError::InvalidArgument(
8165 "DELETE triggers cannot reference NEW".into(),
8166 ));
8167 }
8168 validate_column_id(*id, schema)
8169 }
8170 TriggerValue::OldColumn(id) => {
8171 if event == TriggerEvent::Insert {
8172 return Err(MongrelError::InvalidArgument(
8173 "INSERT triggers cannot reference OLD".into(),
8174 ));
8175 }
8176 validate_column_id(*id, schema)
8177 }
8178 TriggerValue::SelectedColumn(_) => Ok(()),
8182 }
8183}
8184
8185fn recover_ddl_from_wal(
8191 root: &Path,
8192 cat: &mut Catalog,
8193 meta_dek: Option<&[u8; META_DEK_LEN]>,
8194 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
8195) -> Result<()> {
8196 use crate::wal::{DdlOp, Op, SharedWal};
8197
8198 let records = match SharedWal::replay_with_dek(root, wal_dek) {
8199 Ok(r) => r,
8200 Err(_) => return Ok(()),
8201 };
8202
8203 let mut committed: HashMap<u64, u64> = HashMap::new();
8204 for r in &records {
8205 if let Op::TxnCommit { epoch: ce, .. } = r.op {
8206 committed.insert(r.txn_id, ce);
8207 }
8208 }
8209
8210 let mut changed = false;
8211 for r in records {
8212 let Some(&ce) = committed.get(&r.txn_id) else {
8213 continue;
8214 };
8215 match r.op {
8216 Op::Ddl(DdlOp::CreateTable {
8217 table_id,
8218 ref name,
8219 ref schema_json,
8220 }) => {
8221 if cat.tables.iter().any(|t| t.table_id == table_id) {
8222 continue;
8223 }
8224 let schema = DdlOp::decode_schema(schema_json)?;
8225 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
8226 if !tdir.exists() {
8227 std::fs::create_dir_all(tdir.join(crate::engine::WAL_DIR))?;
8228 std::fs::create_dir_all(tdir.join(crate::engine::RUNS_DIR))?;
8229 crate::engine::write_schema(&tdir, &schema)?;
8230 let mut m = crate::manifest::Manifest::new(table_id, schema.schema_id);
8236 crate::manifest::write_atomic(&tdir, &mut m, meta_dek)?;
8237 }
8238 cat.tables.push(CatalogEntry {
8239 table_id,
8240 name: name.clone(),
8241 schema,
8242 state: TableState::Live,
8243 created_epoch: ce,
8244 });
8245 cat.next_table_id = cat.next_table_id.max(table_id + 1);
8246 changed = true;
8247 }
8248 Op::Ddl(DdlOp::DropTable { table_id }) => {
8249 let mut dropped_name = None;
8250 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
8251 dropped_name = Some(entry.name.clone());
8252 if matches!(entry.state, TableState::Live) {
8253 entry.state = TableState::Dropped { at_epoch: ce };
8254 changed = true;
8255 }
8256 }
8257 if let Some(name) = dropped_name {
8258 let before = cat.materialized_views.len();
8259 cat.materialized_views
8260 .retain(|definition| definition.name != name);
8261 changed |= before != cat.materialized_views.len();
8262 cat.security.rls_tables.retain(|table| table != &name);
8263 cat.security.policies.retain(|policy| policy.table != name);
8264 cat.security.masks.retain(|mask| mask.table != name);
8265 for role in &mut cat.roles {
8266 role.permissions
8267 .retain(|permission| permission_table(permission) != Some(&name));
8268 }
8269 cat.security_version = cat.security_version.wrapping_add(1);
8270 }
8271 }
8272 Op::Ddl(DdlOp::RenameTable {
8273 table_id,
8274 ref new_name,
8275 }) => {
8276 let mut old_name = None;
8277 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
8278 if entry.name != *new_name {
8279 old_name = Some(entry.name.clone());
8280 entry.name = new_name.clone();
8281 changed = true;
8282 }
8283 }
8284 if let Some(old_name) = old_name {
8285 if let Some(definition) = cat
8286 .materialized_views
8287 .iter_mut()
8288 .find(|definition| definition.name == old_name)
8289 {
8290 definition.name = new_name.clone();
8291 }
8292 for table in &mut cat.security.rls_tables {
8293 if *table == old_name {
8294 *table = new_name.clone();
8295 }
8296 }
8297 for policy in &mut cat.security.policies {
8298 if policy.table == old_name {
8299 policy.table = new_name.clone();
8300 }
8301 }
8302 for mask in &mut cat.security.masks {
8303 if mask.table == old_name {
8304 mask.table = new_name.clone();
8305 }
8306 }
8307 for role in &mut cat.roles {
8308 for permission in &mut role.permissions {
8309 rename_permission_table(permission, &old_name, new_name);
8310 }
8311 }
8312 cat.security_version = cat.security_version.wrapping_add(1);
8313 }
8314 }
8318 Op::Ddl(DdlOp::AlterTable {
8319 table_id,
8320 ref column_json,
8321 }) => {
8322 let column = DdlOp::decode_column(column_json)?;
8323 let mut renamed = None;
8324 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
8325 renamed = entry
8326 .schema
8327 .columns
8328 .iter()
8329 .find(|existing| existing.id == column.id && existing.name != column.name)
8330 .map(|existing| {
8331 (
8332 entry.name.clone(),
8333 existing.name.clone(),
8334 column.name.clone(),
8335 )
8336 });
8337 if apply_recovered_column_def(&mut entry.schema, column) {
8338 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
8339 if tdir.exists() {
8340 crate::engine::write_schema(&tdir, &entry.schema)?;
8341 }
8342 changed = true;
8343 }
8344 }
8345 if let Some((table, old_name, new_name)) = renamed {
8346 for role in &mut cat.roles {
8347 for permission in &mut role.permissions {
8348 rename_permission_column(permission, &table, &old_name, &new_name);
8349 }
8350 }
8351 cat.security_version = cat.security_version.wrapping_add(1);
8352 }
8353 }
8354 Op::Ddl(DdlOp::SetTtl {
8355 table_id,
8356 ref policy_json,
8357 }) => {
8358 let policy = DdlOp::decode_ttl(policy_json)?;
8359 if let Some(policy) = policy {
8360 let valid = cat
8361 .tables
8362 .iter()
8363 .find(|entry| entry.table_id == table_id)
8364 .and_then(|entry| {
8365 entry
8366 .schema
8367 .columns
8368 .iter()
8369 .find(|column| column.id == policy.column_id)
8370 })
8371 .is_some_and(|column| {
8372 column.ty == TypeId::TimestampNanos
8373 && policy.duration_nanos > 0
8374 && policy.duration_nanos <= i64::MAX as u64
8375 });
8376 if !valid {
8377 return Err(MongrelError::Schema(format!(
8378 "invalid recovered TTL policy for table id {table_id}"
8379 )));
8380 }
8381 }
8382 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
8383 if tdir.exists() {
8384 let mut manifest = crate::manifest::read(&tdir, meta_dek)?;
8385 if manifest.ttl != policy || manifest.current_epoch < ce {
8386 manifest.ttl = policy;
8387 manifest.current_epoch = manifest.current_epoch.max(ce);
8388 crate::manifest::write_atomic(&tdir, &mut manifest, meta_dek)?;
8389 }
8390 }
8391 }
8392 Op::Ddl(DdlOp::SetMaterializedView {
8393 ref name,
8394 ref definition_json,
8395 }) => {
8396 let definition = DdlOp::decode_materialized_view(definition_json)?;
8397 if definition.name != *name {
8398 return Err(MongrelError::Schema(format!(
8399 "materialized view WAL name mismatch: {name:?}"
8400 )));
8401 }
8402 if cat.live(name).is_some() {
8403 if let Some(existing) = cat
8404 .materialized_views
8405 .iter_mut()
8406 .find(|existing| existing.name == *name)
8407 {
8408 if *existing != definition {
8409 *existing = definition;
8410 changed = true;
8411 }
8412 } else {
8413 cat.materialized_views.push(definition);
8414 changed = true;
8415 }
8416 }
8417 }
8418 Op::Ddl(DdlOp::SetSecurityCatalog { ref security_json }) => {
8419 let security = DdlOp::decode_security(security_json)?;
8420 validate_security_catalog(cat, &security)?;
8421 if cat.security != security {
8422 cat.security = security;
8423 cat.security_version = cat.security_version.wrapping_add(1);
8424 changed = true;
8425 }
8426 }
8427 _ => {}
8428 }
8429 }
8430
8431 if changed {
8432 catalog::write_atomic(root, cat, meta_dek)?;
8433 }
8434 Ok(())
8435}
8436
8437fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> bool {
8438 match schema.columns.iter_mut().find(|c| c.id == column.id) {
8439 Some(existing) if *existing == column => false,
8440 Some(existing) => {
8441 *existing = column;
8442 schema.schema_id = schema.schema_id.saturating_add(1);
8443 true
8444 }
8445 None => {
8446 schema.columns.push(column);
8447 schema.schema_id = schema.schema_id.saturating_add(1);
8448 true
8449 }
8450 }
8451}
8452
8453fn permission_table(permission: &crate::auth::Permission) -> Option<&str> {
8454 use crate::auth::Permission;
8455 match permission {
8456 Permission::Select { table }
8457 | Permission::Insert { table }
8458 | Permission::Update { table }
8459 | Permission::Delete { table }
8460 | Permission::SelectColumns { table, .. }
8461 | Permission::InsertColumns { table, .. }
8462 | Permission::UpdateColumns { table, .. } => Some(table),
8463 Permission::All | Permission::Ddl | Permission::Admin => None,
8464 }
8465}
8466
8467fn rename_permission_table(permission: &mut crate::auth::Permission, old: &str, new: &str) {
8468 use crate::auth::Permission;
8469 let table = match permission {
8470 Permission::Select { table }
8471 | Permission::Insert { table }
8472 | Permission::Update { table }
8473 | Permission::Delete { table }
8474 | Permission::SelectColumns { table, .. }
8475 | Permission::InsertColumns { table, .. }
8476 | Permission::UpdateColumns { table, .. } => Some(table),
8477 Permission::All | Permission::Ddl | Permission::Admin => None,
8478 };
8479 if let Some(table) = table.filter(|table| table.as_str() == old) {
8480 *table = new.to_string();
8481 }
8482}
8483
8484fn rename_permission_column(
8485 permission: &mut crate::auth::Permission,
8486 target_table: &str,
8487 old: &str,
8488 new: &str,
8489) {
8490 use crate::auth::Permission;
8491 let columns = match permission {
8492 Permission::SelectColumns { table, columns }
8493 | Permission::InsertColumns { table, columns }
8494 | Permission::UpdateColumns { table, columns }
8495 if table == target_table =>
8496 {
8497 Some(columns)
8498 }
8499 _ => None,
8500 };
8501 if let Some(column) = columns
8502 .into_iter()
8503 .flatten()
8504 .find(|column| column.as_str() == old)
8505 {
8506 *column = new.to_string();
8507 }
8508}
8509
8510fn merge_permission(
8511 permissions: &mut Vec<crate::auth::Permission>,
8512 permission: crate::auth::Permission,
8513) {
8514 use crate::auth::Permission;
8515 let (kind, table, mut columns) = match permission {
8516 Permission::SelectColumns { table, columns } => (0, table, columns),
8517 Permission::InsertColumns { table, columns } => (1, table, columns),
8518 Permission::UpdateColumns { table, columns } => (2, table, columns),
8519 permission if !permissions.contains(&permission) => {
8520 permissions.push(permission);
8521 return;
8522 }
8523 _ => return,
8524 };
8525 for permission in permissions.iter_mut() {
8526 let existing = match permission {
8527 Permission::SelectColumns {
8528 table: existing_table,
8529 columns,
8530 } if kind == 0 && existing_table == &table => Some(columns),
8531 Permission::InsertColumns {
8532 table: existing_table,
8533 columns,
8534 } if kind == 1 && existing_table == &table => Some(columns),
8535 Permission::UpdateColumns {
8536 table: existing_table,
8537 columns,
8538 } if kind == 2 && existing_table == &table => Some(columns),
8539 _ => None,
8540 };
8541 if let Some(existing) = existing {
8542 existing.append(&mut columns);
8543 existing.sort();
8544 existing.dedup();
8545 return;
8546 }
8547 }
8548 columns.sort();
8549 columns.dedup();
8550 permissions.push(match kind {
8551 0 => Permission::SelectColumns { table, columns },
8552 1 => Permission::InsertColumns { table, columns },
8553 2 => Permission::UpdateColumns { table, columns },
8554 _ => unreachable!(),
8555 });
8556}
8557
8558fn revoke_permission_from(
8559 permissions: &mut Vec<crate::auth::Permission>,
8560 revoked: &crate::auth::Permission,
8561) {
8562 use crate::auth::Permission;
8563 let revoked_columns = match revoked {
8564 Permission::SelectColumns { table, columns } => Some((0, table, columns)),
8565 Permission::InsertColumns { table, columns } => Some((1, table, columns)),
8566 Permission::UpdateColumns { table, columns } => Some((2, table, columns)),
8567 _ => None,
8568 };
8569 let Some((kind, table, columns)) = revoked_columns else {
8570 permissions.retain(|permission| permission != revoked);
8571 return;
8572 };
8573 for permission in permissions.iter_mut() {
8574 let current = match permission {
8575 Permission::SelectColumns {
8576 table: current_table,
8577 columns,
8578 } if kind == 0 && current_table == table => Some(columns),
8579 Permission::InsertColumns {
8580 table: current_table,
8581 columns,
8582 } if kind == 1 && current_table == table => Some(columns),
8583 Permission::UpdateColumns {
8584 table: current_table,
8585 columns,
8586 } if kind == 2 && current_table == table => Some(columns),
8587 _ => None,
8588 };
8589 if let Some(current) = current {
8590 current.retain(|column| !columns.contains(column));
8591 }
8592 }
8593 permissions.retain(|permission| match permission {
8594 Permission::SelectColumns { columns, .. }
8595 | Permission::InsertColumns { columns, .. }
8596 | Permission::UpdateColumns { columns, .. } => !columns.is_empty(),
8597 _ => true,
8598 });
8599}
8600
8601fn validate_security_catalog(
8602 catalog: &Catalog,
8603 security: &crate::security::SecurityCatalog,
8604) -> Result<()> {
8605 let mut policy_names = HashSet::new();
8606 for table in &security.rls_tables {
8607 if catalog.live(table).is_none() {
8608 return Err(MongrelError::NotFound(format!(
8609 "RLS table {table:?} not found"
8610 )));
8611 }
8612 }
8613 for policy in &security.policies {
8614 if !policy_names.insert((policy.table.clone(), policy.name.clone())) {
8615 return Err(MongrelError::InvalidArgument(format!(
8616 "duplicate policy {:?} on {:?}",
8617 policy.name, policy.table
8618 )));
8619 }
8620 let schema = &catalog
8621 .live(&policy.table)
8622 .ok_or_else(|| {
8623 MongrelError::NotFound(format!("policy table {:?} not found", policy.table))
8624 })?
8625 .schema;
8626 if let Some(expression) = &policy.using {
8627 validate_security_expression(expression, schema)?;
8628 }
8629 if let Some(expression) = &policy.with_check {
8630 validate_security_expression(expression, schema)?;
8631 }
8632 }
8633 let mut mask_names = HashSet::new();
8634 for mask in &security.masks {
8635 if !mask_names.insert((mask.table.clone(), mask.name.clone())) {
8636 return Err(MongrelError::InvalidArgument(format!(
8637 "duplicate mask {:?} on {:?}",
8638 mask.name, mask.table
8639 )));
8640 }
8641 let column = catalog
8642 .live(&mask.table)
8643 .and_then(|entry| {
8644 entry
8645 .schema
8646 .columns
8647 .iter()
8648 .find(|column| column.id == mask.column)
8649 })
8650 .ok_or_else(|| {
8651 MongrelError::NotFound(format!(
8652 "mask column {} on {:?} not found",
8653 mask.column, mask.table
8654 ))
8655 })?;
8656 if matches!(
8657 mask.strategy,
8658 crate::security::MaskStrategy::Redact { .. } | crate::security::MaskStrategy::Sha256
8659 ) && !matches!(column.ty, TypeId::Bytes | TypeId::Enum { .. })
8660 {
8661 return Err(MongrelError::InvalidArgument(format!(
8662 "mask {:?} requires a string/bytes column",
8663 mask.name
8664 )));
8665 }
8666 }
8667 Ok(())
8668}
8669
8670fn validate_security_expression(
8671 expression: &crate::security::SecurityExpr,
8672 schema: &Schema,
8673) -> Result<()> {
8674 use crate::security::SecurityExpr;
8675 match expression {
8676 SecurityExpr::True => Ok(()),
8677 SecurityExpr::ColumnEqCurrentUser { column }
8678 | SecurityExpr::ColumnEqValue { column, .. } => {
8679 if schema
8680 .columns
8681 .iter()
8682 .any(|candidate| candidate.id == *column)
8683 {
8684 Ok(())
8685 } else {
8686 Err(MongrelError::InvalidArgument(format!(
8687 "security expression references unknown column id {column}"
8688 )))
8689 }
8690 }
8691 SecurityExpr::And { left, right } | SecurityExpr::Or { left, right } => {
8692 validate_security_expression(left, schema)?;
8693 validate_security_expression(right, schema)
8694 }
8695 SecurityExpr::Not { expression } => validate_security_expression(expression, schema),
8696 }
8697}
8698
8699fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
8704 for entry in &cat.tables {
8705 let txn_dir = root
8706 .join(TABLES_DIR)
8707 .join(entry.table_id.to_string())
8708 .join("_txn");
8709 if txn_dir.exists() {
8710 let _ = std::fs::remove_dir_all(&txn_dir);
8711 }
8712 }
8713}
8714
8715#[cfg(test)]
8716mod trigger_engine_tests {
8717 use super::*;
8718
8719 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
8720 WriteEvent {
8721 table: "test".into(),
8722 kind: TriggerEvent::Insert,
8723 new: Some(TriggerRowImage {
8724 columns: new_cells.iter().cloned().collect(),
8725 }),
8726 old: Some(TriggerRowImage {
8727 columns: old_cells.iter().cloned().collect(),
8728 }),
8729 changed_columns: Vec::new(),
8730 op_indices: Vec::new(),
8731 put_idx: None,
8732 trigger_stack: Vec::new(),
8733 }
8734 }
8735
8736 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
8737 WriteEvent {
8738 table: "test".into(),
8739 kind: TriggerEvent::Insert,
8740 new: Some(TriggerRowImage {
8741 columns: new_cells.iter().cloned().collect(),
8742 }),
8743 old: None,
8744 changed_columns: Vec::new(),
8745 op_indices: Vec::new(),
8746 put_idx: None,
8747 trigger_stack: Vec::new(),
8748 }
8749 }
8750
8751 #[test]
8752 fn value_order_int64_vs_float64() {
8753 assert_eq!(
8754 value_order(&Value::Int64(5), &Value::Float64(5.0)),
8755 Some(std::cmp::Ordering::Equal)
8756 );
8757 assert_eq!(
8758 value_order(&Value::Int64(5), &Value::Float64(3.0)),
8759 Some(std::cmp::Ordering::Greater)
8760 );
8761 assert_eq!(
8762 value_order(&Value::Int64(2), &Value::Float64(3.0)),
8763 Some(std::cmp::Ordering::Less)
8764 );
8765 }
8766
8767 #[test]
8768 fn value_order_null_returns_none() {
8769 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
8770 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
8771 assert_eq!(value_order(&Value::Null, &Value::Null), None);
8772 }
8773
8774 #[test]
8775 fn value_order_cross_group_returns_none() {
8776 assert_eq!(
8777 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
8778 None
8779 );
8780 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
8781 assert_eq!(
8782 value_order(
8783 &Value::Embedding(vec![1.0, 2.0]),
8784 &Value::Embedding(vec![1.0, 2.0])
8785 ),
8786 None
8787 );
8788 }
8789
8790 #[test]
8791 fn eval_trigger_expr_ranges_and_booleans() {
8792 let expr = TriggerExpr::And {
8793 left: Box::new(TriggerExpr::Gt {
8794 left: TriggerValue::NewColumn(1),
8795 right: TriggerValue::Literal(Value::Int64(0)),
8796 }),
8797 right: Box::new(TriggerExpr::Lte {
8798 left: TriggerValue::NewColumn(1),
8799 right: TriggerValue::Literal(Value::Int64(100)),
8800 }),
8801 };
8802 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
8803 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
8804 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
8805
8806 let or_expr = TriggerExpr::Or {
8807 left: Box::new(TriggerExpr::Lt {
8808 left: TriggerValue::NewColumn(1),
8809 right: TriggerValue::Literal(Value::Int64(0)),
8810 }),
8811 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
8812 TriggerValue::OldColumn(2),
8813 )))),
8814 };
8815 assert!(eval_trigger_expr(
8816 &or_expr,
8817 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
8818 )
8819 .unwrap());
8820 assert!(!eval_trigger_expr(
8821 &or_expr,
8822 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
8823 )
8824 .unwrap());
8825
8826 assert!(eval_trigger_expr(
8827 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
8828 &event_insert(&[])
8829 )
8830 .unwrap());
8831 assert!(!eval_trigger_expr(
8832 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
8833 &event_insert(&[])
8834 )
8835 .unwrap());
8836 assert!(!eval_trigger_expr(
8837 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
8838 &event_insert(&[])
8839 )
8840 .unwrap());
8841 }
8842}