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
450#[derive(Clone)]
453pub struct TableHandle(TableHandleInner);
454
455#[derive(Clone)]
456enum TableHandleInner {
457 CopyOnWrite(Arc<RwLock<Arc<Table>>>),
458 Direct(Arc<Mutex<Table>>),
459}
460
461pub enum TableGuard<'a> {
462 CopyOnWrite(parking_lot::RwLockWriteGuard<'a, Arc<Table>>),
463 Direct(parking_lot::MutexGuard<'a, Table>),
464}
465
466enum TableReadGuard<'a> {
467 CopyOnWrite(parking_lot::RwLockReadGuard<'a, Arc<Table>>),
468 Direct(parking_lot::MutexGuard<'a, Table>),
469}
470
471impl TableHandle {
472 fn new(table: Table) -> Self {
473 Self(TableHandleInner::CopyOnWrite(Arc::new(RwLock::new(
474 Arc::new(table),
475 ))))
476 }
477
478 pub fn from_table(table: Table) -> Self {
479 Self::new(table)
480 }
481
482 pub fn lock(&self) -> TableGuard<'_> {
483 match &self.0 {
484 TableHandleInner::CopyOnWrite(table) => TableGuard::CopyOnWrite(table.write()),
485 TableHandleInner::Direct(table) => TableGuard::Direct(table.lock()),
486 }
487 }
488
489 fn try_lock_for(&self, timeout: std::time::Duration) -> Option<TableGuard<'_>> {
490 match &self.0 {
491 TableHandleInner::CopyOnWrite(table) => {
492 table.try_write_for(timeout).map(TableGuard::CopyOnWrite)
493 }
494 TableHandleInner::Direct(table) => table.try_lock_for(timeout).map(TableGuard::Direct),
495 }
496 }
497
498 fn read(&self) -> TableReadGuard<'_> {
499 match &self.0 {
500 TableHandleInner::CopyOnWrite(table) => TableReadGuard::CopyOnWrite(table.read()),
501 TableHandleInner::Direct(table) => TableReadGuard::Direct(table.lock()),
502 }
503 }
504
505 fn try_read_for(&self, timeout: std::time::Duration) -> Option<TableReadGuard<'_>> {
506 match &self.0 {
507 TableHandleInner::CopyOnWrite(table) => {
508 table.try_read_for(timeout).map(TableReadGuard::CopyOnWrite)
509 }
510 TableHandleInner::Direct(table) => {
511 table.try_lock_for(timeout).map(TableReadGuard::Direct)
512 }
513 }
514 }
515
516 pub fn ptr_eq(&self, other: &Self) -> bool {
517 match (&self.0, &other.0) {
518 (TableHandleInner::CopyOnWrite(left), TableHandleInner::CopyOnWrite(right)) => {
519 Arc::ptr_eq(left, right)
520 }
521 (TableHandleInner::Direct(left), TableHandleInner::Direct(right)) => {
522 Arc::ptr_eq(left, right)
523 }
524 _ => false,
525 }
526 }
527
528 pub fn read_generation_with_context(
529 &self,
530 context: Option<&crate::query::AiExecutionContext>,
531 ) -> Result<(Arc<Table>, Snapshot)> {
532 let mut table = if let Some(context) = context {
533 loop {
534 context.checkpoint()?;
535 let wait = context
536 .remaining_duration()
537 .unwrap_or(std::time::Duration::from_millis(5))
538 .min(std::time::Duration::from_millis(5));
539 if let Some(table) = self.try_read_for(wait) {
540 break table;
541 }
542 }
543 } else {
544 self.read()
545 };
546 let snapshot = table.snapshot();
547 Ok((table.generation()?, snapshot))
548 }
549}
550
551impl From<Arc<Mutex<Table>>> for TableHandle {
552 fn from(table: Arc<Mutex<Table>>) -> Self {
553 Self(TableHandleInner::Direct(table))
554 }
555}
556
557impl std::ops::Deref for TableGuard<'_> {
558 type Target = Table;
559
560 fn deref(&self) -> &Self::Target {
561 match self {
562 Self::CopyOnWrite(table) => table.as_ref(),
563 Self::Direct(table) => table,
564 }
565 }
566}
567
568impl std::ops::DerefMut for TableGuard<'_> {
569 fn deref_mut(&mut self) -> &mut Self::Target {
570 match self {
571 Self::CopyOnWrite(table) => Arc::make_mut(table),
572 Self::Direct(table) => table,
573 }
574 }
575}
576
577impl TableReadGuard<'_> {
578 fn generation(&mut self) -> Result<Arc<Table>> {
579 match self {
580 Self::CopyOnWrite(table) => Ok(Arc::clone(table)),
581 Self::Direct(table) => Ok(Arc::new(table.clone_read_generation()?)),
582 }
583 }
584}
585
586impl std::ops::Deref for TableReadGuard<'_> {
587 type Target = Table;
588
589 fn deref(&self) -> &Self::Target {
590 match self {
591 Self::CopyOnWrite(table) => table.as_ref(),
592 Self::Direct(table) => table,
593 }
594 }
595}
596
597#[derive(Clone, Debug)]
598pub struct ReadAuthorization {
599 pub operation: crate::auth::ColumnOperation,
600 pub columns: Vec<u16>,
601}
602
603pub fn lock_table_with_context<'a>(
604 handle: &'a TableHandle,
605 context: Option<&crate::query::AiExecutionContext>,
606) -> Result<TableGuard<'a>> {
607 let Some(context) = context else {
608 return Ok(handle.lock());
609 };
610 loop {
611 context.checkpoint()?;
612 let wait = context
613 .remaining_duration()
614 .unwrap_or(std::time::Duration::from_millis(5))
615 .min(std::time::Duration::from_millis(5));
616 if let Some(guard) = handle.try_lock_for(wait) {
617 return Ok(guard);
618 }
619 }
620}
621
622#[derive(Clone, Debug, Default)]
628pub struct OpenOptions {
629 pub lock_timeout_ms: u32,
644}
645
646impl OpenOptions {
647 pub fn with_lock_timeout_ms(mut self, ms: u32) -> Self {
650 self.lock_timeout_ms = ms;
651 self
652 }
653}
654
655pub struct Database {
658 root: PathBuf,
659 read_only: bool,
661 catalog: RwLock<Catalog>,
662 rls_cache: Mutex<RlsCache>,
663 epoch: Arc<EpochAuthority>,
664 snapshots: Arc<SnapshotRegistry>,
665 page_cache: Arc<crate::cache::Sharded<crate::cache::PageCache>>,
666 decoded_cache: Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>,
667 commit_lock: Arc<Mutex<()>>,
668 shared_wal: Arc<Mutex<crate::wal::SharedWal>>,
673 next_txn_id: Arc<Mutex<u64>>,
677 tables: RwLock<HashMap<u64, TableHandle>>,
678 kek: Option<Arc<crate::encryption::Kek>>,
679 ddl_lock: Mutex<()>,
682 meta_dek: Option<[u8; META_DEK_LEN]>,
683 spill_threshold: std::sync::atomic::AtomicU64,
686 conflicts: crate::txn::ConflictIndex,
689 active_txns: crate::txn::ActiveTxns,
692 poisoned: Arc<std::sync::atomic::AtomicBool>,
695 group: Arc<crate::txn::GroupCommit>,
699 active_spills: Arc<crate::retention::ActiveSpills>,
702 replication_barrier: parking_lot::RwLock<()>,
705 replication_wal_retention_segments: AtomicUsize,
707 backup_pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
710 #[doc(hidden)]
714 spill_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
715 #[doc(hidden)]
718 backup_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
719 trigger_recursive: AtomicBool,
720 trigger_max_depth: AtomicU32,
721 trigger_max_loop_iterations: AtomicU32,
722 _lock: Option<std::fs::File>,
725 notify: tokio::sync::broadcast::Sender<ChangeEvent>,
728 change_wake: tokio::sync::broadcast::Sender<()>,
731 principal: RwLock<Option<crate::auth::Principal>>,
741 auth_state: crate::auth_state::AuthState,
747}
748
749struct EpochGuard<'a> {
762 authority: &'a EpochAuthority,
763 epoch: Epoch,
764 armed: bool,
765}
766
767struct RunPins {
768 pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
769 runs: Vec<(u64, u128)>,
770}
771
772struct BackupFilePins {
773 root: PathBuf,
774}
775
776impl Drop for BackupFilePins {
777 fn drop(&mut self) {
778 let _ = std::fs::remove_dir_all(&self.root);
779 }
780}
781
782impl Drop for RunPins {
783 fn drop(&mut self) {
784 let mut pins = self.pins.lock();
785 for run in &self.runs {
786 if let Some(count) = pins.get_mut(run) {
787 *count -= 1;
788 if *count == 0 {
789 pins.remove(run);
790 }
791 }
792 }
793 }
794}
795
796impl<'a> EpochGuard<'a> {
797 fn new(authority: &'a EpochAuthority, epoch: Epoch) -> Self {
798 Self {
799 authority,
800 epoch,
801 armed: true,
802 }
803 }
804
805 fn disarm(&mut self) {
808 self.armed = false;
809 }
810}
811
812impl Drop for EpochGuard<'_> {
813 fn drop(&mut self) {
814 if self.armed {
815 self.authority.abandon(self.epoch);
816 }
817 }
818}
819
820#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
823pub struct ChangeEvent {
824 pub id: Option<String>,
825 pub channel: String,
826 pub table_id: Option<u64>,
827 pub table: String,
828 pub op: String,
829 pub epoch: u64,
830 pub txn_id: Option<u64>,
831 pub message: Option<String>,
832 pub data: Option<serde_json::Value>,
833}
834
835#[derive(Debug, Clone)]
836pub struct CdcBatch {
837 pub events: Vec<ChangeEvent>,
838 pub current_epoch: u64,
839 pub earliest_epoch: Option<u64>,
840 pub gap: bool,
841}
842
843impl std::fmt::Debug for Database {
850 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
851 let cat = self.catalog.read();
852 let principal_guard = self.principal.read();
853 let principal: &str = principal_guard
854 .as_ref()
855 .map(|p| p.username.as_str())
856 .unwrap_or("<none>");
857 f.debug_struct("Database")
858 .field("root", &self.root)
859 .field("db_epoch", &cat.db_epoch)
860 .field("open_generation", &"sidecar")
861 .field("tables", &cat.tables.len())
862 .field("visible_epoch", &self.epoch.visible().0)
863 .field("encrypted", &self.kek.is_some())
864 .field("require_auth", &cat.require_auth)
865 .field("principal", &principal)
866 .finish()
867 }
868}
869
870impl Database {
871 pub fn create(root: impl AsRef<Path>) -> Result<Self> {
873 Self::create_inner(root, None)
874 }
875
876 #[cfg(feature = "encryption")]
879 pub fn create_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
880 let root = root.as_ref();
881 Self::reject_existing_database(root)?;
882 std::fs::create_dir_all(root)?;
883 std::fs::create_dir_all(root.join(META_DIR))?;
884 let salt = crate::encryption::random_salt();
885 std::fs::write(root.join(META_DIR).join(KEYS_FILENAME), salt)?;
886 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
887 Self::create_inner(root, Some(kek))
888 }
889
890 #[cfg(feature = "encryption")]
893 pub fn create_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
894 let root = root.as_ref();
895 Self::reject_existing_database(root)?;
896 std::fs::create_dir_all(root)?;
897 std::fs::create_dir_all(root.join(META_DIR))?;
898 let salt = crate::encryption::random_salt();
899 std::fs::write(root.join(META_DIR).join(KEYS_FILENAME), salt)?;
900 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
901 Self::create_inner(root, Some(kek))
902 }
903
904 fn create_inner(
905 root: impl AsRef<Path>,
906 kek: Option<Arc<crate::encryption::Kek>>,
907 ) -> Result<Self> {
908 let root = root.as_ref().to_path_buf();
909 Self::reject_existing_database(&root)?;
910 std::fs::create_dir_all(&root)?;
911 std::fs::create_dir_all(root.join(TABLES_DIR))?;
912 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
913 let cat = Catalog::empty();
914 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
915 Self::finish_open(root, cat, kek, meta_dek, false, None, 0)
916 }
917
918 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
920 Self::open_inner(root, None, None)
921 }
922
923 #[cfg(feature = "encryption")]
925 pub fn open_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
926 let root = root.as_ref();
927 let salt_bytes = std::fs::read(root.join(META_DIR).join(KEYS_FILENAME))
928 .map_err(|e| MongrelError::NotFound(format!("encryption salt file: {e}")))?;
929 let mut salt = [0u8; crate::encryption::SALT_LEN];
930 salt.copy_from_slice(&salt_bytes);
931 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
932 Self::open_inner(root, Some(kek), None)
933 }
934
935 #[cfg(feature = "encryption")]
938 pub fn open_encrypted_with_options(
939 root: impl AsRef<Path>,
940 passphrase: &str,
941 options: OpenOptions,
942 ) -> Result<Self> {
943 let root = root.as_ref();
944 let salt_bytes = std::fs::read(root.join(META_DIR).join(KEYS_FILENAME))
945 .map_err(|e| MongrelError::NotFound(format!("encryption salt file: {e}")))?;
946 let mut salt = [0u8; crate::encryption::SALT_LEN];
947 salt.copy_from_slice(&salt_bytes);
948 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
949 Self::open_inner_with_lock_timeout(root, Some(kek), None, options.lock_timeout_ms)
950 }
951
952 #[cfg(feature = "encryption")]
954 pub fn open_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
955 let root = root.as_ref();
956 let salt_path = root.join(META_DIR).join(KEYS_FILENAME);
957 let salt_bytes = std::fs::read(&salt_path).map_err(|e| {
958 MongrelError::NotFound(format!(
959 "encryption salt file {:?}: {e} (database not encrypted, or corrupted)",
960 salt_path
961 ))
962 })?;
963 if salt_bytes.len() != crate::encryption::SALT_LEN {
964 return Err(MongrelError::InvalidArgument(format!(
965 "salt file is {} bytes, expected {}",
966 salt_bytes.len(),
967 crate::encryption::SALT_LEN
968 )));
969 }
970 let mut salt = [0u8; crate::encryption::SALT_LEN];
971 salt.copy_from_slice(&salt_bytes);
972 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
973 Self::open_inner(root, Some(kek), None)
974 }
975
976 pub fn open_with_credentials(
988 root: impl AsRef<Path>,
989 username: &str,
990 password: &str,
991 ) -> Result<Self> {
992 Self::open_inner_with_credentials(root, None, username, password)
993 }
994
995 pub fn open_with_credentials_and_options(
999 root: impl AsRef<Path>,
1000 username: &str,
1001 password: &str,
1002 options: OpenOptions,
1003 ) -> Result<Self> {
1004 Self::open_inner_with_credentials_and_lock_timeout(
1005 root,
1006 None,
1007 username,
1008 password,
1009 options.lock_timeout_ms,
1010 )
1011 }
1012
1013 #[cfg(feature = "encryption")]
1016 pub fn open_encrypted_with_credentials(
1017 root: impl AsRef<Path>,
1018 passphrase: &str,
1019 username: &str,
1020 password: &str,
1021 ) -> Result<Self> {
1022 let root = root.as_ref();
1023 let salt_bytes = std::fs::read(root.join(META_DIR).join(KEYS_FILENAME))
1024 .map_err(|e| MongrelError::NotFound(format!("encryption salt file: {e}")))?;
1025 let mut salt = [0u8; crate::encryption::SALT_LEN];
1026 salt.copy_from_slice(&salt_bytes);
1027 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1028 Self::open_inner_with_credentials(root, Some(kek), username, password)
1029 }
1030
1031 #[cfg(feature = "encryption")]
1035 pub fn open_encrypted_with_credentials_and_options(
1036 root: impl AsRef<Path>,
1037 passphrase: &str,
1038 username: &str,
1039 password: &str,
1040 options: OpenOptions,
1041 ) -> Result<Self> {
1042 let root = root.as_ref();
1043 let salt_bytes = std::fs::read(root.join(META_DIR).join(KEYS_FILENAME))
1044 .map_err(|e| MongrelError::NotFound(format!("encryption salt file: {e}")))?;
1045 let mut salt = [0u8; crate::encryption::SALT_LEN];
1046 salt.copy_from_slice(&salt_bytes);
1047 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1048 Self::open_inner_with_credentials_and_lock_timeout(
1049 root,
1050 Some(kek),
1051 username,
1052 password,
1053 options.lock_timeout_ms,
1054 )
1055 }
1056
1057 pub fn open_with_options(root: impl AsRef<Path>, options: OpenOptions) -> Result<Self> {
1064 Self::open_inner_with_lock_timeout(root, None, None, options.lock_timeout_ms)
1067 }
1068
1069 fn open_inner_with_lock_timeout(
1070 root: impl AsRef<Path>,
1071 kek: Option<Arc<crate::encryption::Kek>>,
1072 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
1073 lock_timeout_ms: u32,
1074 ) -> Result<Self> {
1075 let root = root.as_ref().to_path_buf();
1076 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
1077 let cat = catalog::read(&root, meta_dek.as_ref())?
1078 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
1079 Self::finish_open(root, cat, kek, meta_dek, true, None, lock_timeout_ms)
1080 }
1081
1082 fn open_inner_with_credentials(
1089 root: impl AsRef<Path>,
1090 kek: Option<Arc<crate::encryption::Kek>>,
1091 username: &str,
1092 password: &str,
1093 ) -> Result<Self> {
1094 Self::open_inner_with_credentials_and_lock_timeout(root, kek, username, password, 0)
1095 }
1096
1097 fn open_inner_with_credentials_and_lock_timeout(
1101 root: impl AsRef<Path>,
1102 kek: Option<Arc<crate::encryption::Kek>>,
1103 username: &str,
1104 password: &str,
1105 lock_timeout_ms: u32,
1106 ) -> Result<Self> {
1107 let root = root.as_ref().to_path_buf();
1108 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
1109 let cat = catalog::read(&root, meta_dek.as_ref())?
1110 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
1111
1112 if !cat.require_auth {
1115 return Err(MongrelError::AuthNotRequired);
1116 }
1117
1118 let user = cat
1123 .users
1124 .iter()
1125 .find(|u| u.username == username)
1126 .filter(|u| !u.password_hash.is_empty())
1127 .ok_or_else(|| MongrelError::InvalidCredentials {
1128 username: username.to_string(),
1129 })?;
1130 let password_ok = crate::auth::verify_password(password, &user.password_hash)
1131 .map_err(MongrelError::Other)?;
1132 if !password_ok {
1133 return Err(MongrelError::InvalidCredentials {
1134 username: username.to_string(),
1135 });
1136 }
1137
1138 let principal =
1140 Self::resolve_principal_from_catalog(&cat, &user.username).ok_or_else(|| {
1141 MongrelError::InvalidCredentials {
1142 username: username.to_string(),
1143 }
1144 })?;
1145
1146 Self::finish_open(
1147 root,
1148 cat,
1149 kek,
1150 meta_dek,
1151 true,
1152 Some(principal),
1153 lock_timeout_ms,
1154 )
1155 }
1156
1157 pub fn create_with_credentials(
1167 root: impl AsRef<Path>,
1168 admin_username: &str,
1169 admin_password: &str,
1170 ) -> Result<Self> {
1171 Self::create_inner_with_credentials(root, None, admin_username, admin_password)
1172 }
1173
1174 #[cfg(feature = "encryption")]
1178 pub fn create_encrypted_with_credentials(
1179 root: impl AsRef<Path>,
1180 passphrase: &str,
1181 admin_username: &str,
1182 admin_password: &str,
1183 ) -> Result<Self> {
1184 let root = root.as_ref();
1185 Self::reject_existing_database(root)?;
1186 std::fs::create_dir_all(root)?;
1187 std::fs::create_dir_all(root.join(META_DIR))?;
1188 let salt = crate::encryption::random_salt();
1189 std::fs::write(root.join(META_DIR).join(KEYS_FILENAME), salt)?;
1190 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1191 Self::create_inner_with_credentials(root, Some(kek), admin_username, admin_password)
1192 }
1193
1194 fn create_inner_with_credentials(
1195 root: impl AsRef<Path>,
1196 kek: Option<Arc<crate::encryption::Kek>>,
1197 admin_username: &str,
1198 admin_password: &str,
1199 ) -> Result<Self> {
1200 let root = root.as_ref().to_path_buf();
1201 Self::reject_existing_database(&root)?;
1202 std::fs::create_dir_all(&root)?;
1203 std::fs::create_dir_all(root.join(TABLES_DIR))?;
1204 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
1205
1206 let password_hash =
1208 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
1209 let mut cat = Catalog::empty();
1210 cat.require_auth = true;
1211 cat.next_user_id = 1;
1212 cat.users.push(crate::auth::UserEntry {
1213 id: 1,
1214 username: admin_username.to_string(),
1215 password_hash,
1216 roles: Vec::new(),
1217 is_admin: true,
1218 created_epoch: 0,
1219 });
1220 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
1221
1222 let admin_principal = crate::auth::Principal {
1225 username: admin_username.to_string(),
1226 is_admin: true,
1227 roles: Vec::new(),
1228 permissions: Vec::new(),
1229 };
1230 Self::finish_open(root, cat, kek, meta_dek, false, Some(admin_principal), 0)
1231 }
1232
1233 fn reject_existing_database(root: &Path) -> Result<()> {
1234 if root.join(catalog::CATALOG_FILENAME).exists() {
1237 return Err(MongrelError::InvalidArgument(format!(
1238 "database already exists at {}; use Database::open() to open it, \
1239 or remove the directory first",
1240 root.display()
1241 )));
1242 }
1243 Ok(())
1244 }
1245
1246 fn open_inner(
1247 root: impl AsRef<Path>,
1248 kek: Option<Arc<crate::encryption::Kek>>,
1249 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
1250 ) -> Result<Self> {
1251 let root = root.as_ref().to_path_buf();
1252 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
1253 let cat = catalog::read(&root, meta_dek.as_ref())?
1254 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
1255 Self::finish_open(root, cat, kek, meta_dek, true, None, 0)
1256 }
1257
1258 fn fs_lock_exclusive(f: &std::fs::File, timeout_ms: u32) -> std::io::Result<()> {
1270 use fs2::FileExt;
1271 if timeout_ms == 0 {
1272 return f.try_lock_exclusive();
1273 }
1274 let deadline =
1276 std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms as u64);
1277 let mut next_sleep = std::time::Duration::from_millis(1);
1278 loop {
1279 match f.try_lock_exclusive() {
1280 Ok(()) => return Ok(()),
1281 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
1282 let now = std::time::Instant::now();
1283 if now >= deadline {
1284 return Err(std::io::Error::new(
1285 std::io::ErrorKind::WouldBlock,
1286 format!("could not acquire database lock within {timeout_ms}ms"),
1287 ));
1288 }
1289 let remaining = deadline - now;
1290 let sleep = next_sleep.min(remaining);
1291 std::thread::sleep(sleep);
1292 next_sleep = next_sleep
1295 .saturating_mul(10)
1296 .min(std::time::Duration::from_millis(50));
1297 }
1298 Err(e) => return Err(e),
1299 }
1300 }
1301 }
1302
1303 fn finish_open(
1304 root: PathBuf,
1305 cat: Catalog,
1306 kek: Option<Arc<crate::encryption::Kek>>,
1307 meta_dek: Option<[u8; META_DEK_LEN]>,
1308 existing: bool,
1309 principal: Option<crate::auth::Principal>,
1310 lock_timeout_ms: u32,
1311 ) -> Result<Self> {
1312 let read_only = existing && root.join(META_DIR).join("replica").exists();
1313 std::fs::create_dir_all(root.join("_meta")).ok();
1319 let lock_path = root.join("_meta").join(".lock");
1320 let canonical = lock_path.canonicalize().unwrap_or(lock_path.clone());
1321 let lock_file = {
1322 static LOCKED_PATHS: std::sync::OnceLock<
1323 std::sync::Mutex<std::collections::HashSet<PathBuf>>,
1324 > = std::sync::OnceLock::new();
1325 let locked = LOCKED_PATHS
1326 .get_or_init(|| std::sync::Mutex::new(std::collections::HashSet::new()));
1327 let mut guard = locked.lock().unwrap();
1328 if guard.contains(&canonical) {
1329 None
1331 } else {
1332 let f = std::fs::OpenOptions::new()
1333 .create(true)
1334 .truncate(false)
1335 .write(true)
1336 .open(&lock_path)?;
1337 Self::fs_lock_exclusive(&f, lock_timeout_ms).map_err(|e| {
1338 MongrelError::Io(std::io::Error::other(format!(
1339 "database at {} is locked by another process: {e}",
1340 root.display()
1341 )))
1342 })?;
1343 guard.insert(canonical.clone());
1344 Some(f)
1345 }
1346 };
1347 if lock_file.is_some() {
1348 let stale_backup_pins = root.join(META_DIR).join("backup-pins");
1349 if stale_backup_pins.exists() {
1350 std::fs::remove_dir_all(stale_backup_pins)?;
1351 }
1352 }
1353
1354 let epoch = Arc::new(EpochAuthority::new(cat.db_epoch));
1355 let snapshots = Arc::new(SnapshotRegistry::new());
1356 let (history_epochs, history_start) = read_history_retention(&root, Epoch(cat.db_epoch))?;
1357 snapshots.configure_history(history_epochs, history_start);
1358 let page_cache = Arc::new(crate::cache::Sharded::new(
1359 crate::cache::CACHE_SHARDS,
1360 || {
1361 crate::cache::PageCache::new(
1362 crate::engine::PAGE_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
1363 )
1364 },
1365 ));
1366 let decoded_cache = Arc::new(crate::cache::Sharded::new(
1367 crate::cache::CACHE_SHARDS,
1368 || {
1369 crate::cache::DecodedPageCache::new(
1370 crate::engine::DECODED_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
1371 )
1372 },
1373 ));
1374 let commit_lock = Arc::new(Mutex::new(()));
1375 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
1376 let shared_wal = Arc::new(Mutex::new(if existing {
1377 crate::wal::SharedWal::open(&root, Epoch(cat.db_epoch), wal_dek.clone())?
1378 } else {
1379 crate::wal::SharedWal::create_with_dek(&root, Epoch(cat.db_epoch), wal_dek.clone())?
1380 }));
1381 let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));
1385 let group = Arc::new(crate::txn::GroupCommit::new(
1386 shared_wal.lock().durable_seq(),
1387 ));
1388 let (change_wake, _change_rx) = tokio::sync::broadcast::channel(256);
1389 let txn_ids = Arc::new(Mutex::new(1u64));
1393
1394 let mut cat = cat;
1400 if existing {
1401 recover_ddl_from_wal(&root, &mut cat, meta_dek.as_ref(), wal_dek.as_ref())?;
1402 }
1403
1404 let auth_state = crate::auth_state::AuthState::new(cat.require_auth, principal.clone());
1409 let auth_checker: Option<Arc<dyn crate::auth_state::TableAuthChecker>> = Some(Arc::new(
1410 crate::auth_state::DefaultTableAuthChecker::new(auth_state.clone()),
1411 ));
1412
1413 let mut tables: HashMap<u64, TableHandle> = HashMap::new();
1419 for entry in &cat.tables {
1420 if !matches!(entry.state, TableState::Live) {
1421 continue;
1422 }
1423 let tdir = root.join(TABLES_DIR).join(entry.table_id.to_string());
1424 let ctx = SharedCtx {
1425 epoch: Arc::clone(&epoch),
1426 page_cache: Arc::clone(&page_cache),
1427 decoded_cache: Arc::clone(&decoded_cache),
1428 snapshots: Arc::clone(&snapshots),
1429 kek: kek.clone(),
1430 commit_lock: Arc::clone(&commit_lock),
1431 shared: Some(crate::engine::SharedWalCtx {
1432 wal: Arc::clone(&shared_wal),
1433 group: Arc::clone(&group),
1434 poisoned: Arc::clone(&poisoned),
1435 txn_ids: Arc::clone(&txn_ids),
1436 change_wake: change_wake.clone(),
1437 }),
1438 table_name: Some(entry.name.clone()),
1439 auth: auth_checker.clone(),
1440 read_only,
1441 };
1442 let t = Table::open_in(&tdir, ctx)?;
1443 tables.insert(entry.table_id, TableHandle::new(t));
1444 }
1445
1446 if existing {
1452 recover_shared_wal(&root, &tables, &epoch, wal_dek.as_ref())?;
1453 sweep_pending_txn_dirs(&root, &cat);
1456 }
1457
1458 let open_generation = if existing {
1464 let meta_dir = root.join(META_DIR);
1465 let gen = catalog::read_generation(&meta_dir);
1466 let bumped = gen.wrapping_add(1);
1467 catalog::write_generation(&meta_dir, bumped)?;
1468 bumped
1469 } else {
1470 0
1471 };
1472 let next_txn_id = (open_generation << 32) | 1;
1473 *txn_ids.lock() = next_txn_id;
1475
1476 if existing && cat.require_auth && principal.is_none() {
1484 return Err(MongrelError::AuthRequired);
1485 }
1486
1487 Ok(Self {
1488 root,
1489 read_only,
1490 catalog: RwLock::new(cat),
1491 rls_cache: Mutex::new(RlsCache::default()),
1492 epoch,
1493 snapshots,
1494 page_cache,
1495 decoded_cache,
1496 commit_lock,
1497 shared_wal,
1498 next_txn_id: txn_ids,
1499 tables: RwLock::new(tables),
1500 kek,
1501 ddl_lock: Mutex::new(()),
1502 meta_dek,
1503 conflicts: crate::txn::ConflictIndex::new(),
1504 active_txns: crate::txn::ActiveTxns::new(),
1505 poisoned,
1506 group,
1507 spill_threshold: std::sync::atomic::AtomicU64::new(64 * 1024 * 1024),
1508 active_spills: Arc::new(crate::retention::ActiveSpills::new()),
1509 replication_barrier: parking_lot::RwLock::new(()),
1510 replication_wal_retention_segments: AtomicUsize::new(0),
1511 backup_pins: Arc::new(Mutex::new(HashMap::new())),
1512 spill_hook: Mutex::new(None),
1513 backup_hook: Mutex::new(None),
1514 trigger_recursive: AtomicBool::new(TriggerConfig::default().recursive_triggers),
1515 trigger_max_depth: AtomicU32::new(TriggerConfig::default().max_depth),
1516 trigger_max_loop_iterations: AtomicU32::new(
1517 TriggerConfig::default().max_loop_iterations,
1518 ),
1519 _lock: lock_file,
1520 notify: {
1521 let (tx, _rx) = tokio::sync::broadcast::channel(256);
1522 tx
1523 },
1524 change_wake,
1525 principal: RwLock::new(principal),
1526 auth_state,
1527 })
1528 }
1529
1530 pub fn visible_epoch(&self) -> Epoch {
1532 self.epoch.visible()
1533 }
1534
1535 pub fn catalog_snapshot(&self) -> Catalog {
1537 self.catalog.read().clone()
1538 }
1539
1540 pub fn materialized_view(&self, name: &str) -> Option<crate::catalog::MaterializedViewEntry> {
1541 self.catalog
1542 .read()
1543 .materialized_views
1544 .iter()
1545 .find(|definition| definition.name == name)
1546 .cloned()
1547 }
1548
1549 pub fn materialized_views(&self) -> Vec<crate::catalog::MaterializedViewEntry> {
1550 self.catalog.read().materialized_views.clone()
1551 }
1552
1553 pub fn security_catalog(&self) -> crate::security::SecurityCatalog {
1554 self.catalog.read().security.clone()
1555 }
1556
1557 pub fn security_active_for(&self, table: &str) -> bool {
1558 self.catalog.read().security.table_has_security(table)
1559 }
1560
1561 pub fn set_security_catalog(&self, security: crate::security::SecurityCatalog) -> Result<()> {
1563 self.set_security_catalog_as(security, None)
1564 }
1565
1566 pub fn set_security_catalog_as(
1568 &self,
1569 security: crate::security::SecurityCatalog,
1570 principal: Option<&crate::auth::Principal>,
1571 ) -> Result<()> {
1572 use crate::wal::DdlOp;
1573 use std::sync::atomic::Ordering;
1574
1575 self.require_for(principal, &crate::auth::Permission::Admin)?;
1576 if self.poisoned.load(Ordering::Relaxed) {
1577 return Err(MongrelError::Other(
1578 "database poisoned by fsync error".into(),
1579 ));
1580 }
1581 let _ddl = self.ddl_lock.lock();
1582 validate_security_catalog(&self.catalog.read(), &security)?;
1583 let payload = DdlOp::encode_security(&security)?;
1584 let _commit = self.commit_lock.lock();
1585 let epoch = self.epoch.bump_assigned();
1586 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
1587 let txn_id = self.alloc_txn_id();
1588 let commit_seq = {
1589 let mut wal = self.shared_wal.lock();
1590 wal.append(
1591 txn_id,
1592 WAL_TABLE_ID,
1593 crate::wal::Op::Ddl(DdlOp::SetSecurityCatalog {
1594 security_json: payload,
1595 }),
1596 )?;
1597 wal.append_commit(txn_id, epoch, &[])?
1598 };
1599 self.group
1600 .await_durable(&self.shared_wal, commit_seq)
1601 .inspect_err(|_| {
1602 self.poisoned.store(true, Ordering::Relaxed);
1603 })?;
1604 {
1605 let mut catalog = self.catalog.write();
1606 catalog.security = security;
1607 catalog.security_version = catalog.security_version.wrapping_add(1);
1608 catalog.db_epoch = catalog.db_epoch.max(epoch.0);
1609 }
1610 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1611 self.epoch.publish_in_order(epoch);
1612 epoch_guard.disarm();
1613 Ok(())
1614 }
1615
1616 pub fn require_for(
1617 &self,
1618 principal: Option<&crate::auth::Principal>,
1619 permission: &crate::auth::Permission,
1620 ) -> Result<()> {
1621 let Some(principal) = principal else {
1622 return self.require(permission);
1623 };
1624 if principal.has_permission(permission) {
1625 Ok(())
1626 } else {
1627 Err(MongrelError::PermissionDenied {
1628 required: permission.clone(),
1629 principal: principal.username.clone(),
1630 })
1631 }
1632 }
1633
1634 pub fn principal_snapshot(&self) -> Option<crate::auth::Principal> {
1635 self.principal.read().clone()
1636 }
1637
1638 pub fn require_columns_for(
1639 &self,
1640 table: &str,
1641 operation: crate::auth::ColumnOperation,
1642 column_ids: &[u16],
1643 principal: Option<&crate::auth::Principal>,
1644 ) -> Result<()> {
1645 let schema = self
1646 .catalog
1647 .read()
1648 .live(table)
1649 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
1650 .schema
1651 .clone();
1652 let cached = self.principal.read().clone();
1653 let principal = principal.or(cached.as_ref());
1654 let Some(principal) = principal else {
1655 let permission = match operation {
1656 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
1657 table: table.to_string(),
1658 },
1659 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
1660 table: table.to_string(),
1661 },
1662 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
1663 table: table.to_string(),
1664 },
1665 };
1666 return self.require(&permission);
1667 };
1668 match principal.column_access(table, operation) {
1669 crate::auth::ColumnAccess::All => Ok(()),
1670 crate::auth::ColumnAccess::Columns(allowed) => {
1671 let denied = column_ids.iter().find_map(|column_id| {
1672 schema
1673 .columns
1674 .iter()
1675 .find(|column| column.id == *column_id)
1676 .filter(|column| !allowed.contains(&column.name))
1677 });
1678 if denied.is_none() {
1679 Ok(())
1680 } else {
1681 Err(MongrelError::PermissionDenied {
1682 required: match operation {
1683 crate::auth::ColumnOperation::Select => {
1684 crate::auth::Permission::SelectColumns {
1685 table: table.to_string(),
1686 columns: denied
1687 .into_iter()
1688 .map(|column| column.name.clone())
1689 .collect(),
1690 }
1691 }
1692 crate::auth::ColumnOperation::Insert => {
1693 crate::auth::Permission::InsertColumns {
1694 table: table.to_string(),
1695 columns: denied
1696 .into_iter()
1697 .map(|column| column.name.clone())
1698 .collect(),
1699 }
1700 }
1701 crate::auth::ColumnOperation::Update => {
1702 crate::auth::Permission::UpdateColumns {
1703 table: table.to_string(),
1704 columns: denied
1705 .into_iter()
1706 .map(|column| column.name.clone())
1707 .collect(),
1708 }
1709 }
1710 },
1711 principal: principal.username.clone(),
1712 })
1713 }
1714 }
1715 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
1716 required: match operation {
1717 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
1718 table: table.to_string(),
1719 },
1720 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
1721 table: table.to_string(),
1722 },
1723 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
1724 table: table.to_string(),
1725 },
1726 },
1727 principal: principal.username.clone(),
1728 }),
1729 }
1730 }
1731
1732 pub fn select_column_ids_for(
1733 &self,
1734 table: &str,
1735 principal: Option<&crate::auth::Principal>,
1736 ) -> Result<Vec<u16>> {
1737 let catalog = self.catalog.read();
1738 let columns = catalog
1739 .live(table)
1740 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
1741 .schema
1742 .columns
1743 .iter()
1744 .map(|column| (column.id, column.name.clone()))
1745 .collect::<Vec<_>>();
1746 drop(catalog);
1747 let cached_principal = self.principal.read().clone();
1748 let principal = principal.or(cached_principal.as_ref());
1749 let Some(principal) = principal else {
1750 self.require(&crate::auth::Permission::Select {
1751 table: table.to_string(),
1752 })?;
1753 return Ok(columns.iter().map(|(id, _)| *id).collect());
1754 };
1755 match principal.column_access(table, crate::auth::ColumnOperation::Select) {
1756 crate::auth::ColumnAccess::All => Ok(columns.iter().map(|(id, _)| *id).collect()),
1757 crate::auth::ColumnAccess::Columns(allowed) => Ok(columns
1758 .iter()
1759 .filter(|(_, name)| allowed.contains(name))
1760 .map(|(id, _)| *id)
1761 .collect()),
1762 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
1763 required: crate::auth::Permission::Select {
1764 table: table.to_string(),
1765 },
1766 principal: principal.username.clone(),
1767 }),
1768 }
1769 }
1770
1771 pub fn secure_rows_for(
1772 &self,
1773 table: &str,
1774 rows: Vec<crate::memtable::Row>,
1775 principal: Option<&crate::auth::Principal>,
1776 ) -> Result<Vec<crate::memtable::Row>> {
1777 self.secure_rows_for_with_context(table, rows, principal, None)
1778 }
1779
1780 pub fn secure_rows_for_with_context(
1781 &self,
1782 table: &str,
1783 rows: Vec<crate::memtable::Row>,
1784 principal: Option<&crate::auth::Principal>,
1785 context: Option<&crate::query::AiExecutionContext>,
1786 ) -> Result<Vec<crate::memtable::Row>> {
1787 let security = self.catalog.read().security.clone();
1788 if !security.table_has_security(table) {
1789 return Ok(rows);
1790 }
1791 let owned;
1792 let principal = match principal {
1793 Some(principal) => principal,
1794 None => {
1795 owned = self
1796 .principal
1797 .read()
1798 .clone()
1799 .ok_or(MongrelError::AuthRequired)?;
1800 &owned
1801 }
1802 };
1803 let mut output = Vec::new();
1804 for mut row in rows {
1805 if let Some(context) = context {
1806 context.consume(1)?;
1807 }
1808 if security.row_allowed(
1809 table,
1810 crate::security::PolicyCommand::Select,
1811 &row,
1812 principal,
1813 false,
1814 ) {
1815 security.apply_masks(table, &mut row, principal);
1816 output.push(row);
1817 }
1818 }
1819 Ok(output)
1820 }
1821
1822 pub fn mask_search_hits_for(
1825 &self,
1826 table: &str,
1827 hits: &mut [crate::query::SearchHit],
1828 principal: Option<&crate::auth::Principal>,
1829 ) -> Result<()> {
1830 let security = self.catalog.read().security.clone();
1831 if !security.table_has_security(table) {
1832 return Ok(());
1833 }
1834 let owned;
1835 let principal = match principal {
1836 Some(principal) => principal,
1837 None => {
1838 owned = self.principal.read().clone();
1839 let Some(principal) = owned.as_ref() else {
1840 return Ok(());
1841 };
1842 principal
1843 }
1844 };
1845 for hit in hits {
1846 security.apply_masks_to_cells(table, &mut hit.cells, principal);
1847 }
1848 Ok(())
1849 }
1850
1851 pub fn mask_rows_for(
1853 &self,
1854 table: &str,
1855 rows: &mut [crate::memtable::Row],
1856 principal: Option<&crate::auth::Principal>,
1857 ) -> Result<()> {
1858 let security = self.catalog.read().security.clone();
1859 if !security.table_has_security(table) {
1860 return Ok(());
1861 }
1862 let owned;
1863 let principal = match principal {
1864 Some(principal) => principal,
1865 None => {
1866 owned = self
1867 .principal
1868 .read()
1869 .clone()
1870 .ok_or(MongrelError::AuthRequired)?;
1871 &owned
1872 }
1873 };
1874 for row in rows {
1875 security.apply_masks(table, row, principal);
1876 }
1877 Ok(())
1878 }
1879
1880 pub fn authorized_candidate_ids_for(
1882 &self,
1883 table: &str,
1884 principal: Option<&crate::auth::Principal>,
1885 ) -> Result<Option<std::collections::HashSet<RowId>>> {
1886 Ok(self
1887 .authorized_read_snapshot(table, principal)?
1888 .allowed_row_ids)
1889 }
1890
1891 fn allowed_row_ids_locked(
1892 &self,
1893 table_name: &str,
1894 table: &Table,
1895 table_snapshot: Snapshot,
1896 security_state: (&crate::security::SecurityCatalog, u64),
1897 principal: Option<&crate::auth::Principal>,
1898 context: Option<&crate::query::AiExecutionContext>,
1899 ) -> Result<Option<Arc<HashSet<RowId>>>> {
1900 let (security, security_version) = security_state;
1901 if !security.rls_enabled(table_name) {
1902 return Ok(None);
1903 }
1904 let authorization_started = std::time::Instant::now();
1905 let principal = principal.ok_or(MongrelError::AuthRequired)?;
1906 let mut roles = principal.roles.clone();
1907 roles.sort_unstable();
1908 let principal_key = format!("{}:{}:{roles:?}", principal.username, principal.is_admin);
1909 let cache_key = (
1910 table_name.to_string(),
1911 table.data_generation(),
1912 security_version,
1913 principal_key,
1914 );
1915 if let Some(allowed) = self.rls_cache.lock().get(&cache_key) {
1916 crate::trace::QueryTrace::record(|trace| {
1917 trace.rls_cache_hit = true;
1918 trace.authorization_nanos = trace
1919 .authorization_nanos
1920 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
1921 });
1922 return Ok(Some(allowed));
1923 }
1924 if let Some(context) = context {
1925 context.checkpoint()?;
1926 }
1927 let started = std::time::Instant::now();
1929 let rows = table.visible_rows(table_snapshot)?;
1930 let rows_evaluated = rows.len() as u64;
1931 let mut allowed = HashSet::new();
1932 for chunk in rows.chunks(256) {
1933 if let Some(context) = context {
1934 context.consume(chunk.len())?;
1935 }
1936 allowed.extend(chunk.iter().filter_map(|row| {
1937 security
1938 .row_allowed(
1939 table_name,
1940 crate::security::PolicyCommand::Select,
1941 row,
1942 principal,
1943 false,
1944 )
1945 .then_some(row.row_id)
1946 }));
1947 }
1948 let allowed = Arc::new(allowed);
1949 let mut cache = self.rls_cache.lock();
1950 cache.build_nanos = cache
1951 .build_nanos
1952 .saturating_add(started.elapsed().as_nanos() as u64);
1953 cache.rows_evaluated = cache.rows_evaluated.saturating_add(rows_evaluated);
1954 cache.insert(cache_key, Arc::clone(&allowed));
1955 crate::trace::QueryTrace::record(|trace| {
1956 trace.rls_rows_evaluated = trace
1957 .rls_rows_evaluated
1958 .saturating_add(rows_evaluated as usize);
1959 trace.authorization_nanos = trace
1960 .authorization_nanos
1961 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
1962 });
1963 Ok(Some(allowed))
1964 }
1965
1966 fn principal_for_authorized_read(
1967 &self,
1968 catalog: &Catalog,
1969 principal: Option<&crate::auth::Principal>,
1970 catalog_bound: bool,
1971 ) -> Result<Option<crate::auth::Principal>> {
1972 let principal = principal.cloned().or_else(|| self.principal.read().clone());
1973 let Some(principal) = principal else {
1974 return Ok(None);
1975 };
1976 if catalog_bound
1977 || catalog
1978 .users
1979 .iter()
1980 .any(|user| user.username == principal.username)
1981 {
1982 return Self::resolve_principal_from_catalog(catalog, &principal.username)
1983 .map(Some)
1984 .ok_or(MongrelError::AuthRequired);
1985 }
1986 Ok(Some(principal))
1987 }
1988
1989 pub fn with_authorized_read<T, F>(
1993 &self,
1994 table_name: &str,
1995 principal: Option<&crate::auth::Principal>,
1996 catalog_bound: bool,
1997 read: F,
1998 ) -> Result<T>
1999 where
2000 F: FnMut(
2001 &mut Table,
2002 Snapshot,
2003 Option<&HashSet<RowId>>,
2004 Option<&crate::auth::Principal>,
2005 ) -> Result<T>,
2006 {
2007 self.with_authorized_read_context(
2008 table_name,
2009 principal,
2010 catalog_bound,
2011 None,
2012 None,
2013 None,
2014 read,
2015 )
2016 }
2017
2018 #[allow(clippy::too_many_arguments)]
2019 pub fn with_authorized_read_context<T, F>(
2020 &self,
2021 table_name: &str,
2022 principal: Option<&crate::auth::Principal>,
2023 catalog_bound: bool,
2024 authorization: Option<&ReadAuthorization>,
2025 context: Option<&crate::query::AiExecutionContext>,
2026 snapshot_override: Option<Snapshot>,
2027 mut read: F,
2028 ) -> Result<T>
2029 where
2030 F: FnMut(
2031 &mut Table,
2032 Snapshot,
2033 Option<&HashSet<RowId>>,
2034 Option<&crate::auth::Principal>,
2035 ) -> Result<T>,
2036 {
2037 if principal.is_none() && self.principal.read().is_some() {
2038 self.refresh_principal()?;
2039 }
2040 const RETRIES: usize = 3;
2041 let handle = self.table(table_name)?;
2042 for attempt in 0..RETRIES {
2043 crate::trace::QueryTrace::record(|trace| {
2044 trace.authorization_retries = attempt;
2045 });
2046 let (security, security_version, effective_principal) = {
2047 let catalog = self.catalog.read();
2048 (
2049 catalog.security.clone(),
2050 catalog.security_version,
2051 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
2052 )
2053 };
2054 if let Some(authorization) = authorization {
2055 self.require_columns_for(
2056 table_name,
2057 authorization.operation,
2058 &authorization.columns,
2059 effective_principal.as_ref(),
2060 )?;
2061 }
2062 let result = {
2063 let mut table = lock_table_with_context(&handle, context)?;
2064 let snapshot = snapshot_override.unwrap_or_else(|| table.snapshot());
2065 let allowed = self.allowed_row_ids_locked(
2066 table_name,
2067 &table,
2068 snapshot,
2069 (&security, security_version),
2070 effective_principal.as_ref(),
2071 context,
2072 )?;
2073 read(
2074 &mut table,
2075 snapshot,
2076 allowed.as_deref(),
2077 effective_principal.as_ref(),
2078 )?
2079 };
2080 if let Some(context) = context {
2081 context.checkpoint()?;
2082 }
2083 if self.catalog.read().security_version == security_version {
2084 return Ok(result);
2085 }
2086 if attempt + 1 == RETRIES {
2087 return Err(MongrelError::Conflict(
2088 "security policy changed during scored read".into(),
2089 ));
2090 }
2091 }
2092 unreachable!()
2093 }
2094
2095 pub fn with_authorized_scored_read_context<T, F>(
2099 &self,
2100 table_name: &str,
2101 principal: Option<&crate::auth::Principal>,
2102 catalog_bound: bool,
2103 authorization: Option<&ReadAuthorization>,
2104 context: Option<&crate::query::AiExecutionContext>,
2105 mut read: F,
2106 ) -> Result<T>
2107 where
2108 F: FnMut(
2109 &mut Table,
2110 Snapshot,
2111 Option<&crate::security::CandidateAuthorization<'_>>,
2112 Option<&crate::auth::Principal>,
2113 ) -> Result<T>,
2114 {
2115 self.with_authorized_scored_read_context_at(
2116 table_name,
2117 principal,
2118 catalog_bound,
2119 authorization,
2120 context,
2121 None,
2122 |table, snapshot, authorization, principal| {
2123 let mut table = table.clone();
2124 read(&mut table, snapshot, authorization, principal)
2125 },
2126 )
2127 }
2128
2129 #[allow(clippy::too_many_arguments)]
2130 pub fn with_authorized_scored_read_context_at<T, F>(
2131 &self,
2132 table_name: &str,
2133 principal: Option<&crate::auth::Principal>,
2134 catalog_bound: bool,
2135 authorization: Option<&ReadAuthorization>,
2136 context: Option<&crate::query::AiExecutionContext>,
2137 snapshot_override: Option<Snapshot>,
2138 mut read: F,
2139 ) -> Result<T>
2140 where
2141 F: FnMut(
2142 &Table,
2143 Snapshot,
2144 Option<&crate::security::CandidateAuthorization<'_>>,
2145 Option<&crate::auth::Principal>,
2146 ) -> Result<T>,
2147 {
2148 if principal.is_none() && self.principal.read().is_some() {
2149 self.refresh_principal()?;
2150 }
2151 const RETRIES: usize = 3;
2152 let handle = self.table(table_name)?;
2153 for attempt in 0..RETRIES {
2154 if let Some(context) = context {
2155 context.checkpoint()?;
2156 }
2157 crate::trace::QueryTrace::record(|trace| {
2158 trace.authorization_retries = attempt;
2159 });
2160 let (security, security_version, effective_principal) = {
2161 let catalog = self.catalog.read();
2162 (
2163 catalog.security.clone(),
2164 catalog.security_version,
2165 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
2166 )
2167 };
2168 if let Some(authorization) = authorization {
2169 self.require_columns_for(
2170 table_name,
2171 authorization.operation,
2172 &authorization.columns,
2173 effective_principal.as_ref(),
2174 )?;
2175 }
2176 let result = {
2177 let (table, snapshot, _snapshot_guard, _run_pins) =
2178 self.scored_read_generation(&handle, context, snapshot_override)?;
2179 let candidate_authorization = if security.rls_enabled(table_name) {
2180 Some(crate::security::CandidateAuthorization {
2181 table: table_name,
2182 security: &security,
2183 principal: effective_principal
2184 .as_ref()
2185 .ok_or(MongrelError::AuthRequired)?,
2186 })
2187 } else {
2188 None
2189 };
2190 read(
2191 table.as_ref(),
2192 snapshot,
2193 candidate_authorization.as_ref(),
2194 effective_principal.as_ref(),
2195 )?
2196 };
2197 if let Some(context) = context {
2198 context.checkpoint()?;
2199 }
2200 if self.catalog.read().security_version == security_version {
2201 return Ok(result);
2202 }
2203 if attempt + 1 == RETRIES {
2204 return Err(MongrelError::Conflict(
2205 "security policy changed during scored read".into(),
2206 ));
2207 }
2208 }
2209 unreachable!()
2210 }
2211
2212 fn scored_read_generation(
2213 &self,
2214 handle: &TableHandle,
2215 context: Option<&crate::query::AiExecutionContext>,
2216 snapshot_override: Option<Snapshot>,
2217 ) -> Result<(
2218 Arc<Table>,
2219 Snapshot,
2220 crate::retention::OwnedSnapshotGuard,
2221 RunPins,
2222 )> {
2223 let mut table = if let Some(context) = context {
2224 loop {
2225 context.checkpoint()?;
2226 let wait = context
2227 .remaining_duration()
2228 .unwrap_or(std::time::Duration::from_millis(5))
2229 .min(std::time::Duration::from_millis(5));
2230 if let Some(table) = handle.try_read_for(wait) {
2231 break table;
2232 }
2233 }
2234 } else {
2235 handle.read()
2236 };
2237 let (snapshot, snapshot_guard) = if let Some(snapshot) = snapshot_override {
2238 self.snapshot_at_owned(snapshot.epoch)?
2239 } else {
2240 let snapshot = table.snapshot();
2241 let guard = self.snapshots.register_owned(snapshot.epoch);
2242 (snapshot, guard)
2243 };
2244 let table_id = table.table_id();
2245 let run_keys: Vec<_> = table
2246 .active_run_ids()
2247 .map(|run_id| (table_id, run_id))
2248 .collect();
2249 let generation = table.generation()?;
2250 let run_pins = self.pin_runs(&run_keys);
2251 Ok((generation, snapshot, snapshot_guard, run_pins))
2252 }
2253
2254 fn pin_runs(&self, runs: &[(u64, u128)]) -> RunPins {
2255 let mut pins = self.backup_pins.lock();
2256 for run in runs {
2257 *pins.entry(*run).or_insert(0) += 1;
2258 }
2259 drop(pins);
2260 RunPins {
2261 pins: Arc::clone(&self.backup_pins),
2262 runs: runs.to_vec(),
2263 }
2264 }
2265
2266 pub fn query_for_current_principal(
2270 &self,
2271 table_name: &str,
2272 query: &crate::query::Query,
2273 projection: Option<&[u16]>,
2274 ) -> Result<Vec<crate::memtable::Row>> {
2275 let condition_columns = crate::query::condition_columns(&query.conditions);
2276 self.with_authorized_read(
2277 table_name,
2278 None,
2279 true,
2280 |table, snapshot, allowed, principal| {
2281 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
2282 self.require_columns_for(
2283 table_name,
2284 crate::auth::ColumnOperation::Select,
2285 &condition_columns,
2286 principal,
2287 )?;
2288 if let Some(projection) = projection {
2289 self.require_columns_for(
2290 table_name,
2291 crate::auth::ColumnOperation::Select,
2292 projection,
2293 principal,
2294 )?;
2295 }
2296 let mut rows = table.query_at_with_allowed(query, snapshot, allowed)?;
2297 let projection =
2298 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
2299 for row in &mut rows {
2300 row.columns.retain(|column, _| {
2301 allowed_columns.contains(column)
2302 && projection
2303 .as_ref()
2304 .map_or(true, |projection| projection.contains(column))
2305 });
2306 }
2307 self.secure_rows_for(table_name, rows, principal)
2308 },
2309 )
2310 }
2311
2312 pub fn get_for_current_principal(
2315 &self,
2316 table_name: &str,
2317 row_id: RowId,
2318 ) -> Result<Option<crate::memtable::Row>> {
2319 self.with_authorized_read(
2320 table_name,
2321 None,
2322 true,
2323 |table, snapshot, allowed, principal| {
2324 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
2325 let Some(row) = table.get(row_id, snapshot) else {
2326 return Ok(None);
2327 };
2328 if allowed.is_some_and(|allowed| !allowed.contains(&row.row_id)) {
2329 return Ok(None);
2330 }
2331 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
2332 if let Some(row) = rows.first_mut() {
2333 row.columns
2334 .retain(|column, _| allowed_columns.contains(column));
2335 }
2336 Ok(rows.pop())
2337 },
2338 )
2339 }
2340
2341 pub fn ann_rerank_for_current_principal(
2344 &self,
2345 table_name: &str,
2346 request: &crate::query::AnnRerankRequest,
2347 ) -> Result<Vec<crate::query::AnnRerankHit>> {
2348 self.with_authorized_scored_read_context_at(
2349 table_name,
2350 None,
2351 true,
2352 Some(&ReadAuthorization {
2353 operation: crate::auth::ColumnOperation::Select,
2354 columns: vec![request.column_id],
2355 }),
2356 None,
2357 None,
2358 |table, snapshot, authorization, principal| {
2359 self.require_columns_for(
2360 table_name,
2361 crate::auth::ColumnOperation::Select,
2362 &[request.column_id],
2363 principal,
2364 )?;
2365 table.ann_rerank_at_with_candidate_authorization_on_generation(
2366 request,
2367 snapshot,
2368 authorization,
2369 None,
2370 )
2371 },
2372 )
2373 }
2374
2375 pub fn authorized_read_snapshot(
2378 &self,
2379 table: &str,
2380 principal: Option<&crate::auth::Principal>,
2381 ) -> Result<AuthorizedReadSnapshot> {
2382 let (security, security_version, effective_principal) = {
2383 let catalog = self.catalog.read();
2384 (
2385 catalog.security.clone(),
2386 catalog.security_version,
2387 self.principal_for_authorized_read(&catalog, principal, false)?,
2388 )
2389 };
2390 let handle = self.table(table)?;
2391 let (table_snapshot, data_generation, allowed_row_ids) = {
2392 let table_handle = handle.lock();
2393 let table_snapshot = table_handle.snapshot();
2394 let data_generation = table_handle.data_generation();
2395 let allowed = self.allowed_row_ids_locked(
2396 table,
2397 &table_handle,
2398 table_snapshot,
2399 (&security, security_version),
2400 effective_principal.as_ref(),
2401 None,
2402 )?;
2403 (
2404 table_snapshot,
2405 data_generation,
2406 allowed.map(|allowed| (*allowed).clone()),
2407 )
2408 };
2409 Ok(AuthorizedReadSnapshot {
2410 table: table.to_string(),
2411 table_snapshot,
2412 data_generation,
2413 security_version,
2414 allowed_row_ids,
2415 })
2416 }
2417
2418 pub fn authorized_read_snapshot_valid(&self, snapshot: &AuthorizedReadSnapshot) -> bool {
2419 if self.catalog.read().security_version != snapshot.security_version {
2420 return false;
2421 }
2422 self.table(&snapshot.table)
2423 .ok()
2424 .is_some_and(|table| table.lock().data_generation() == snapshot.data_generation)
2425 }
2426
2427 pub fn rls_cache_stats(&self) -> RlsCacheStats {
2428 self.rls_cache.lock().stats()
2429 }
2430
2431 pub fn rows_for(
2433 &self,
2434 table: &str,
2435 principal: Option<&crate::auth::Principal>,
2436 ) -> Result<Vec<crate::memtable::Row>> {
2437 if principal.is_none() && self.principal.read().is_some() {
2438 self.refresh_principal()?;
2439 }
2440 let allowed = self.select_column_ids_for(table, principal)?;
2441 let handle = self.table(table)?;
2442 let rows = {
2443 let table = handle.lock();
2444 table.visible_rows(table.snapshot())?
2445 };
2446 let mut rows = self.secure_rows_for(table, rows, principal)?;
2447 for row in &mut rows {
2448 row.columns.retain(|column, _| allowed.contains(column));
2449 }
2450 Ok(rows)
2451 }
2452
2453 pub fn rows_at_epoch_for_current_principal(
2456 &self,
2457 table_name: &str,
2458 snapshot: Snapshot,
2459 ) -> Result<Vec<crate::memtable::Row>> {
2460 self.with_authorized_read_context(
2461 table_name,
2462 None,
2463 true,
2464 Some(&ReadAuthorization {
2465 operation: crate::auth::ColumnOperation::Select,
2466 columns: Vec::new(),
2467 }),
2468 None,
2469 Some(snapshot),
2470 |table, snapshot, allowed, principal| {
2471 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
2472 let mut rows = table.visible_rows(snapshot)?;
2473 if let Some(allowed) = allowed {
2474 rows.retain(|row| allowed.contains(&row.row_id));
2475 }
2476 rows = self.secure_rows_for(table_name, rows, principal)?;
2477 for row in &mut rows {
2478 row.columns
2479 .retain(|column, _| allowed_columns.contains(column));
2480 }
2481 Ok(rows)
2482 },
2483 )
2484 }
2485
2486 pub fn count_for(
2488 &self,
2489 table: &str,
2490 principal: Option<&crate::auth::Principal>,
2491 ) -> Result<u64> {
2492 if principal.is_none() && self.principal.read().is_some() {
2493 self.refresh_principal()?;
2494 }
2495 self.select_column_ids_for(table, principal)?;
2496 if self.security_active_for(table) {
2497 Ok(self.rows_for(table, principal)?.len() as u64)
2498 } else {
2499 Ok(self.table(table)?.lock().count())
2500 }
2501 }
2502
2503 pub fn put_for(
2505 &self,
2506 table: &str,
2507 mut cells: Vec<(u16, crate::memtable::Value)>,
2508 principal: Option<&crate::auth::Principal>,
2509 ) -> Result<RowId> {
2510 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
2511 self.require_columns_for(
2512 table,
2513 crate::auth::ColumnOperation::Insert,
2514 &columns,
2515 principal,
2516 )?;
2517 let handle = self.table(table)?;
2518 let mut table_handle = handle.lock();
2519 table_handle.fill_auto_inc(&mut cells)?;
2520 table_handle.apply_defaults(&mut cells)?;
2521 let mut row = crate::memtable::Row::new(RowId(0), self.epoch.visible());
2522 row.columns.extend(cells.iter().cloned());
2523 self.check_row_policy_for(
2524 table,
2525 crate::security::PolicyCommand::Insert,
2526 &row,
2527 true,
2528 principal,
2529 )?;
2530 table_handle.put(cells)
2531 }
2532
2533 pub fn check_row_policy_for(
2534 &self,
2535 table: &str,
2536 command: crate::security::PolicyCommand,
2537 row: &crate::memtable::Row,
2538 check_new: bool,
2539 principal: Option<&crate::auth::Principal>,
2540 ) -> Result<()> {
2541 let security = self.catalog.read().security.clone();
2542 if !security.rls_enabled(table) {
2543 return Ok(());
2544 }
2545 let cached = self.principal.read().clone();
2546 let principal = principal
2547 .or(cached.as_ref())
2548 .ok_or(MongrelError::AuthRequired)?;
2549 if security.row_allowed(table, command, row, principal, check_new) {
2550 return Ok(());
2551 }
2552 let required = match command {
2553 crate::security::PolicyCommand::Insert => crate::auth::Permission::Insert {
2554 table: table.to_string(),
2555 },
2556 crate::security::PolicyCommand::Update => crate::auth::Permission::Update {
2557 table: table.to_string(),
2558 },
2559 crate::security::PolicyCommand::Select => crate::auth::Permission::Select {
2560 table: table.to_string(),
2561 },
2562 crate::security::PolicyCommand::Delete | crate::security::PolicyCommand::All => {
2563 crate::auth::Permission::Delete {
2564 table: table.to_string(),
2565 }
2566 }
2567 };
2568 Err(MongrelError::PermissionDenied {
2569 required,
2570 principal: principal.username.clone(),
2571 })
2572 }
2573
2574 pub fn set_materialized_view(
2577 &self,
2578 definition: crate::catalog::MaterializedViewEntry,
2579 ) -> Result<()> {
2580 use crate::wal::DdlOp;
2581 use std::sync::atomic::Ordering;
2582
2583 self.require(&crate::auth::Permission::Ddl)?;
2584 if self.poisoned.load(Ordering::Relaxed) {
2585 return Err(MongrelError::Other(
2586 "database poisoned by fsync error".into(),
2587 ));
2588 }
2589 if definition.name.is_empty() || definition.query.trim().is_empty() {
2590 return Err(MongrelError::InvalidArgument(
2591 "materialized view name and query must not be empty".into(),
2592 ));
2593 }
2594
2595 let _ddl = self.ddl_lock.lock();
2596 let table_id = self
2597 .catalog
2598 .read()
2599 .live(&definition.name)
2600 .ok_or_else(|| {
2601 MongrelError::NotFound(format!(
2602 "materialized view table {:?} not found",
2603 definition.name
2604 ))
2605 })?
2606 .table_id;
2607 let definition_json = DdlOp::encode_materialized_view(&definition)?;
2608 let _commit = self.commit_lock.lock();
2609 let epoch = self.epoch.bump_assigned();
2610 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2611 let txn_id = self.alloc_txn_id();
2612 let commit_seq = {
2613 let mut wal = self.shared_wal.lock();
2614 wal.append(
2615 txn_id,
2616 table_id,
2617 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
2618 name: definition.name.clone(),
2619 definition_json,
2620 }),
2621 )?;
2622 wal.append_commit(txn_id, epoch, &[])?
2623 };
2624 self.group
2625 .await_durable(&self.shared_wal, commit_seq)
2626 .inspect_err(|_| {
2627 self.poisoned.store(true, Ordering::Relaxed);
2628 })?;
2629
2630 {
2631 let mut catalog = self.catalog.write();
2632 if let Some(existing) = catalog
2633 .materialized_views
2634 .iter_mut()
2635 .find(|existing| existing.name == definition.name)
2636 {
2637 *existing = definition;
2638 } else {
2639 catalog.materialized_views.push(definition);
2640 }
2641 }
2642 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2643 self.epoch.publish_in_order(epoch);
2644 epoch_guard.disarm();
2645 Ok(())
2646 }
2647
2648 pub fn root(&self) -> &Path {
2650 &self.root
2651 }
2652
2653 pub fn is_read_only_replica(&self) -> bool {
2654 self.read_only
2655 }
2656
2657 pub fn set_replication_wal_retention_segments(&self, segments: usize) {
2658 self.replication_wal_retention_segments
2659 .store(segments, std::sync::atomic::Ordering::Relaxed);
2660 }
2661
2662 pub fn replication_snapshot(&self) -> Result<crate::replication::ReplicationSnapshot> {
2667 let _barrier = self.replication_barrier.write();
2668 let _ddl = self.ddl_lock.lock();
2669 let mut handles: Vec<_> = self
2670 .tables
2671 .read()
2672 .iter()
2673 .map(|(id, handle)| (*id, handle.clone()))
2674 .collect();
2675 handles.sort_by_key(|(id, _)| *id);
2676 let _table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
2677 let _commit = self.commit_lock.lock();
2678 let mut wal = self.shared_wal.lock();
2679 wal.group_sync()?;
2680 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
2681 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
2682 let epoch = records
2683 .iter()
2684 .filter_map(|record| match &record.op {
2685 crate::wal::Op::TxnCommit { epoch, .. } => Some(*epoch),
2686 _ => None,
2687 })
2688 .max()
2689 .unwrap_or(0)
2690 .max(self.visible_epoch().0);
2691 let files = crate::replication::capture_files(&self.root)?;
2692 drop(wal);
2693 Ok(crate::replication::ReplicationSnapshot::new(epoch, files))
2694 }
2695
2696 pub fn hot_backup(&self, destination: impl AsRef<Path>) -> Result<crate::backup::BackupReport> {
2704 self.require(&crate::auth::Permission::Ddl)?;
2705 let (destination, parent, stage) =
2706 prepare_backup_destination(&self.root, destination.as_ref())?;
2707 std::fs::create_dir(&stage)?;
2708
2709 let outcome = (|| {
2710 let barrier = self.replication_barrier.write();
2711 let ddl = self.ddl_lock.lock();
2712 let mut handles: Vec<_> = self
2713 .tables
2714 .read()
2715 .iter()
2716 .map(|(id, handle)| (*id, handle.clone()))
2717 .collect();
2718 handles.sort_by_key(|(id, _)| *id);
2719 let table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
2720 let commit = self.commit_lock.lock();
2721 let mut wal = self.shared_wal.lock();
2722 wal.group_sync()?;
2723 let epoch = self.visible_epoch().0;
2724
2725 let pin_nonce = std::time::SystemTime::now()
2726 .duration_since(std::time::UNIX_EPOCH)
2727 .unwrap_or_default()
2728 .as_nanos();
2729 let file_pin_root = self
2730 .root
2731 .join(META_DIR)
2732 .join("backup-pins")
2733 .join(format!("{}-{pin_nonce}", std::process::id()));
2734 std::fs::create_dir_all(&file_pin_root)?;
2735 let _file_pins = BackupFilePins {
2736 root: file_pin_root.clone(),
2737 };
2738 let mut run_files = Vec::new();
2739 for (index, (table_id, _)) in handles.iter().enumerate() {
2740 let table = &table_guards[index];
2741 for run in table.run_refs() {
2742 let source = table.runs_dir().join(format!("r-{}.sr", run.run_id));
2743 let relative = source
2744 .strip_prefix(&self.root)
2745 .map_err(|error| MongrelError::Other(format!("backup run path: {error}")))?
2746 .to_path_buf();
2747 let pinned = file_pin_root.join(format!("{table_id}-{}.sr", run.run_id));
2748 if std::fs::hard_link(&source, &pinned).is_err() {
2749 crate::backup::copy_file_synced(&source, &pinned)?;
2750 }
2751 run_files.push(((*table_id, run.run_id), pinned, relative));
2752 }
2753 }
2754 std::fs::File::open(&file_pin_root)?.sync_all()?;
2755 let run_keys: Vec<_> = run_files.iter().map(|(key, _, _)| *key).collect();
2756 {
2757 let mut pins = self.backup_pins.lock();
2758 for key in &run_keys {
2759 *pins.entry(*key).or_insert(0) += 1;
2760 }
2761 }
2762 let _run_pins = RunPins {
2763 pins: Arc::clone(&self.backup_pins),
2764 runs: run_keys,
2765 };
2766 let deferred: HashSet<_> = run_files
2767 .iter()
2768 .map(|(_, _, relative)| relative.clone())
2769 .collect();
2770 let mut copied = Vec::new();
2771 copy_backup_boundary(&self.root, &stage, &deferred, &mut copied)?;
2772
2773 drop(wal);
2774 drop(commit);
2775 drop(table_guards);
2776 drop(ddl);
2777 drop(barrier);
2778
2779 if let Some(hook) = self.backup_hook.lock().as_ref() {
2780 hook();
2781 }
2782 for (_, source, relative) in run_files {
2783 crate::backup::copy_file_synced(&source, &stage.join(&relative))?;
2784 copied.push(relative);
2785 }
2786
2787 let manifest = crate::backup::BackupManifest::create(&stage, epoch, &copied)?;
2788 manifest.write(&stage)?;
2789 crate::backup::sync_directories(&stage)?;
2790 if destination.exists() {
2791 return Err(MongrelError::Conflict(format!(
2792 "backup destination already exists: {}",
2793 destination.display()
2794 )));
2795 }
2796 std::fs::rename(&stage, &destination)?;
2797 std::fs::File::open(&parent)?.sync_all()?;
2798 Ok(crate::backup::BackupReport {
2799 destination,
2800 epoch,
2801 files: manifest.files.len(),
2802 bytes: manifest.total_bytes(),
2803 })
2804 })();
2805
2806 if outcome.is_err() && stage.exists() {
2807 let _ = std::fs::remove_dir_all(&stage);
2808 }
2809 outcome
2810 }
2811
2812 pub fn replication_batch_since(
2815 &self,
2816 since_epoch: u64,
2817 ) -> Result<crate::replication::ReplicationBatch> {
2818 use crate::wal::Op;
2819
2820 let mut wal = self.shared_wal.lock();
2821 wal.group_sync()?;
2822 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
2823 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
2824 drop(wal);
2825
2826 let commits: HashMap<u64, u64> = records
2827 .iter()
2828 .filter_map(|record| match &record.op {
2829 Op::TxnCommit { epoch, .. } => Some((record.txn_id, *epoch)),
2830 _ => None,
2831 })
2832 .collect();
2833 let earliest_epoch = commits.values().copied().min();
2834 let current_epoch = commits
2835 .values()
2836 .copied()
2837 .max()
2838 .unwrap_or(0)
2839 .max(self.visible_epoch().0);
2840 let selected: HashSet<u64> = commits
2841 .iter()
2842 .filter_map(|(txn_id, epoch)| (*epoch > since_epoch).then_some(*txn_id))
2843 .collect();
2844 let retention_gap = since_epoch < current_epoch
2845 && earliest_epoch.map_or(true, |epoch| epoch > since_epoch.saturating_add(1));
2846 let spilled = records.iter().any(|record| {
2847 selected.contains(&record.txn_id)
2848 && matches!(
2849 &record.op,
2850 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty()
2851 )
2852 });
2853 let records = records
2854 .into_iter()
2855 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
2856 .filter(|record| selected.contains(&record.txn_id))
2857 .collect();
2858 Ok(crate::replication::ReplicationBatch {
2859 current_epoch,
2860 earliest_epoch,
2861 requires_snapshot: retention_gap || spilled,
2862 records,
2863 })
2864 }
2865
2866 pub fn append_replication_batch(&self, records: &[crate::wal::Record]) -> Result<u64> {
2870 use crate::wal::Op;
2871
2872 if !self.read_only {
2873 return Err(MongrelError::InvalidArgument(
2874 "replication batches may only target a marked replica".into(),
2875 ));
2876 }
2877 let current = crate::replication::replica_epoch(&self.root)?;
2878 let mut commits = HashMap::new();
2879 let mut commit_timestamps = HashMap::new();
2880 for record in records {
2881 match &record.op {
2882 Op::TxnCommit { epoch, added_runs } => {
2883 if !added_runs.is_empty() {
2884 return Err(MongrelError::Conflict(
2885 "replication snapshot required for spilled-run transaction".into(),
2886 ));
2887 }
2888 if commits.insert(record.txn_id, *epoch).is_some() {
2889 return Err(MongrelError::InvalidArgument(format!(
2890 "duplicate commit for replication transaction {}",
2891 record.txn_id
2892 )));
2893 }
2894 }
2895 Op::CommitTimestamp { unix_nanos } => {
2896 commit_timestamps.insert(record.txn_id, *unix_nanos);
2897 }
2898 _ => {}
2899 }
2900 }
2901 for record in records {
2902 if record.txn_id != crate::wal::SYSTEM_TXN_ID
2903 && !matches!(&record.op, Op::TxnAbort)
2904 && !commits.contains_key(&record.txn_id)
2905 {
2906 return Err(MongrelError::InvalidArgument(format!(
2907 "incomplete replication transaction {}",
2908 record.txn_id
2909 )));
2910 }
2911 }
2912 let target_epoch = commits
2913 .values()
2914 .copied()
2915 .filter(|epoch| *epoch > current)
2916 .max()
2917 .unwrap_or(current);
2918 let mut selected: HashSet<u64> = commits
2919 .iter()
2920 .filter_map(|(txn_id, epoch)| (*epoch > current).then_some(*txn_id))
2921 .collect();
2922 if selected.is_empty() {
2923 return Ok(current);
2924 }
2925 let mut wal = self.shared_wal.lock();
2926 wal.group_sync()?;
2927 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
2928 let existing: HashSet<(u64, u64)> =
2929 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
2930 .into_iter()
2931 .filter_map(|record| match record.op {
2932 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
2933 _ => None,
2934 })
2935 .collect();
2936 selected.retain(|txn_id| {
2937 commits
2938 .get(txn_id)
2939 .is_some_and(|epoch| !existing.contains(&(*txn_id, *epoch)))
2940 });
2941 for record in records {
2942 if !selected.contains(&record.txn_id) {
2943 continue;
2944 }
2945 match &record.op {
2946 Op::TxnCommit { epoch, added_runs } => {
2947 let timestamp = commit_timestamps
2948 .get(&record.txn_id)
2949 .copied()
2950 .unwrap_or_else(current_unix_nanos);
2951 wal.append_commit_at(record.txn_id, Epoch(*epoch), added_runs, timestamp)?;
2952 }
2953 Op::TxnAbort | Op::Flush { .. } | Op::CommitTimestamp { .. } => {}
2954 op => {
2955 wal.append(record.txn_id, 0, op.clone())?;
2956 }
2957 }
2958 }
2959 if !selected.is_empty() {
2960 wal.group_sync()?;
2961 }
2962 Ok(target_epoch)
2963 }
2964
2965 pub fn table_id(&self, name: &str) -> Result<u64> {
2968 let cat = self.catalog.read();
2969 cat.live(name)
2970 .map(|e| e.table_id)
2971 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
2972 }
2973
2974 pub fn procedures(&self) -> Vec<StoredProcedure> {
2975 self.catalog
2976 .read()
2977 .procedures
2978 .iter()
2979 .map(|p| p.procedure.clone())
2980 .collect()
2981 }
2982
2983 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
2984 self.catalog
2985 .read()
2986 .procedures
2987 .iter()
2988 .find(|p| p.procedure.name == name)
2989 .map(|p| p.procedure.clone())
2990 }
2991
2992 pub fn create_procedure(&self, mut procedure: StoredProcedure) -> Result<StoredProcedure> {
2993 self.require(&crate::auth::Permission::Ddl)?;
2994 let _g = self.ddl_lock.lock();
2995 procedure.validate()?;
2996 self.validate_procedure_references(&procedure)?;
2997 {
2998 let cat = self.catalog.read();
2999 if cat
3000 .procedures
3001 .iter()
3002 .any(|p| p.procedure.name == procedure.name)
3003 {
3004 return Err(MongrelError::InvalidArgument(format!(
3005 "procedure {:?} already exists",
3006 procedure.name
3007 )));
3008 }
3009 }
3010 let commit_lock = Arc::clone(&self.commit_lock);
3011 let _c = commit_lock.lock();
3012 let epoch = self.epoch.bump_assigned();
3013 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3014 procedure.created_epoch = epoch.0;
3015 procedure.updated_epoch = epoch.0;
3016 {
3017 let mut cat = self.catalog.write();
3018 cat.procedures.push(ProcedureEntry::from(procedure.clone()));
3019 cat.db_epoch = epoch.0;
3020 }
3021 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3022 self.epoch.publish_in_order(epoch);
3023 _epoch_guard.disarm();
3024 Ok(procedure)
3025 }
3026
3027 pub fn create_or_replace_procedure(
3028 &self,
3029 procedure: StoredProcedure,
3030 ) -> Result<StoredProcedure> {
3031 let _g = self.ddl_lock.lock();
3032 procedure.validate()?;
3033 self.validate_procedure_references(&procedure)?;
3034 let commit_lock = Arc::clone(&self.commit_lock);
3035 let _c = commit_lock.lock();
3036 let epoch = self.epoch.bump_assigned();
3037 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3038 let replaced = {
3039 let mut cat = self.catalog.write();
3040 let next = match cat
3041 .procedures
3042 .iter()
3043 .position(|p| p.procedure.name == procedure.name)
3044 {
3045 Some(idx) => {
3046 let next = cat.procedures[idx]
3047 .procedure
3048 .replaced(procedure.clone(), epoch.0)?;
3049 cat.procedures[idx] = ProcedureEntry::from(next.clone());
3050 next
3051 }
3052 None => {
3053 let mut next = procedure;
3054 next.created_epoch = epoch.0;
3055 next.updated_epoch = epoch.0;
3056 cat.procedures.push(ProcedureEntry::from(next.clone()));
3057 next
3058 }
3059 };
3060 cat.db_epoch = epoch.0;
3061 next
3062 };
3063 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3064 self.epoch.publish_in_order(epoch);
3065 _epoch_guard.disarm();
3066 Ok(replaced)
3067 }
3068
3069 pub fn drop_procedure(&self, name: &str) -> Result<()> {
3070 self.require(&crate::auth::Permission::Ddl)?;
3071 let _g = self.ddl_lock.lock();
3072 let commit_lock = Arc::clone(&self.commit_lock);
3073 let _c = commit_lock.lock();
3074 let epoch = self.epoch.bump_assigned();
3075 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3076 {
3077 let mut cat = self.catalog.write();
3078 let before = cat.procedures.len();
3079 cat.procedures.retain(|p| p.procedure.name != name);
3080 if cat.procedures.len() == before {
3081 return Err(MongrelError::NotFound(format!(
3082 "procedure {name:?} not found"
3083 )));
3084 }
3085 cat.db_epoch = epoch.0;
3086 }
3087 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3088 self.epoch.publish_in_order(epoch);
3089 _epoch_guard.disarm();
3090 Ok(())
3091 }
3092
3093 pub fn users(&self) -> Vec<crate::auth::UserEntry> {
3098 self.catalog.read().users.clone()
3099 }
3100
3101 pub fn roles(&self) -> Vec<crate::auth::RoleEntry> {
3103 self.catalog.read().roles.clone()
3104 }
3105
3106 pub fn create_user(&self, username: &str, password: &str) -> Result<crate::auth::UserEntry> {
3108 self.require(&crate::auth::Permission::Admin)?;
3109 let hash = crate::auth::hash_password(password).map_err(MongrelError::Other)?;
3110 let epoch = self.epoch.bump_assigned();
3111 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3112 let id = {
3113 let mut cat = self.catalog.write();
3114 if cat.users.iter().any(|u| u.username == username) {
3115 return Err(MongrelError::InvalidArgument(format!(
3116 "user {username:?} already exists"
3117 )));
3118 }
3119 cat.next_user_id += 1;
3120 let id = cat.next_user_id;
3121 let entry = crate::auth::UserEntry {
3122 id,
3123 username: username.into(),
3124 password_hash: hash,
3125 roles: Vec::new(),
3126 is_admin: false,
3127 created_epoch: epoch.0,
3128 };
3129 cat.users.push(entry.clone());
3130 cat.security_version = cat.security_version.wrapping_add(1);
3131 cat.db_epoch = epoch.0;
3132 entry
3133 };
3134 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3135 self.epoch.publish_in_order(epoch);
3136 _epoch_guard.disarm();
3137 Ok(id)
3138 }
3139
3140 pub fn drop_user(&self, username: &str) -> Result<()> {
3142 self.require(&crate::auth::Permission::Admin)?;
3143 let epoch = self.epoch.bump_assigned();
3144 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3145 {
3146 let mut cat = self.catalog.write();
3147 let before = cat.users.len();
3148 cat.users.retain(|u| u.username != username);
3149 if cat.users.len() == before {
3150 return Err(MongrelError::NotFound(format!(
3151 "user {username:?} not found"
3152 )));
3153 }
3154 cat.security_version = cat.security_version.wrapping_add(1);
3155 cat.db_epoch = epoch.0;
3156 }
3157 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3158 self.epoch.publish_in_order(epoch);
3159 _epoch_guard.disarm();
3160 Ok(())
3161 }
3162
3163 pub fn alter_user_password(&self, username: &str, new_password: &str) -> Result<()> {
3165 self.require(&crate::auth::Permission::Admin)?;
3166 let hash = crate::auth::hash_password(new_password).map_err(MongrelError::Other)?;
3167 let epoch = self.epoch.bump_assigned();
3168 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3169 {
3170 let mut cat = self.catalog.write();
3171 let user = cat
3172 .users
3173 .iter_mut()
3174 .find(|u| u.username == username)
3175 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
3176 user.password_hash = hash;
3177 cat.security_version = cat.security_version.wrapping_add(1);
3178 cat.db_epoch = epoch.0;
3179 }
3180 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3181 self.epoch.publish_in_order(epoch);
3182 _epoch_guard.disarm();
3183 Ok(())
3184 }
3185
3186 pub fn verify_user(
3189 &self,
3190 username: &str,
3191 password: &str,
3192 ) -> Result<Option<crate::auth::UserEntry>> {
3193 let cat = self.catalog.read();
3194 let Some(user) = cat.users.iter().find(|u| u.username == username) else {
3195 return Ok(None);
3196 };
3197 if user.password_hash.is_empty() {
3198 return Ok(None);
3199 }
3200 let ok = crate::auth::verify_password(password, &user.password_hash)
3201 .map_err(MongrelError::Other)?;
3202 if ok {
3203 Ok(Some(user.clone()))
3204 } else {
3205 Ok(None)
3206 }
3207 }
3208
3209 pub fn set_user_admin(&self, username: &str, is_admin: bool) -> Result<()> {
3211 self.require(&crate::auth::Permission::Admin)?;
3212 let epoch = self.epoch.bump_assigned();
3213 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3214 {
3215 let mut cat = self.catalog.write();
3216 let user = cat
3217 .users
3218 .iter_mut()
3219 .find(|u| u.username == username)
3220 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
3221 user.is_admin = is_admin;
3222 cat.security_version = cat.security_version.wrapping_add(1);
3223 cat.db_epoch = epoch.0;
3224 }
3225 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3226 self.epoch.publish_in_order(epoch);
3227 _epoch_guard.disarm();
3228 Ok(())
3229 }
3230
3231 pub fn create_role(&self, name: &str) -> Result<crate::auth::RoleEntry> {
3233 self.require(&crate::auth::Permission::Admin)?;
3234 let epoch = self.epoch.bump_assigned();
3235 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3236 let entry = {
3237 let mut cat = self.catalog.write();
3238 if cat.roles.iter().any(|r| r.name == name) {
3239 return Err(MongrelError::InvalidArgument(format!(
3240 "role {name:?} already exists"
3241 )));
3242 }
3243 let entry = crate::auth::RoleEntry {
3244 name: name.into(),
3245 permissions: Vec::new(),
3246 created_epoch: epoch.0,
3247 };
3248 cat.roles.push(entry.clone());
3249 cat.security_version = cat.security_version.wrapping_add(1);
3250 cat.db_epoch = epoch.0;
3251 entry
3252 };
3253 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3254 self.epoch.publish_in_order(epoch);
3255 _epoch_guard.disarm();
3256 Ok(entry)
3257 }
3258
3259 pub fn drop_role(&self, name: &str) -> Result<()> {
3261 self.require(&crate::auth::Permission::Admin)?;
3262 let epoch = self.epoch.bump_assigned();
3263 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3264 {
3265 let mut cat = self.catalog.write();
3266 let before = cat.roles.len();
3267 cat.roles.retain(|r| r.name != name);
3268 if cat.roles.len() == before {
3269 return Err(MongrelError::NotFound(format!("role {name:?} not found")));
3270 }
3271 for user in &mut cat.users {
3273 user.roles.retain(|r| r != name);
3274 }
3275 cat.security_version = cat.security_version.wrapping_add(1);
3276 cat.db_epoch = epoch.0;
3277 }
3278 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3279 self.epoch.publish_in_order(epoch);
3280 _epoch_guard.disarm();
3281 Ok(())
3282 }
3283
3284 pub fn grant_role(&self, username: &str, role_name: &str) -> Result<()> {
3286 self.require(&crate::auth::Permission::Admin)?;
3287 let epoch = self.epoch.bump_assigned();
3288 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3289 {
3290 let mut cat = self.catalog.write();
3291 if !cat.roles.iter().any(|r| r.name == role_name) {
3292 return Err(MongrelError::NotFound(format!(
3293 "role {role_name:?} not found"
3294 )));
3295 }
3296 let user = cat
3297 .users
3298 .iter_mut()
3299 .find(|u| u.username == username)
3300 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
3301 if !user.roles.contains(&role_name.to_string()) {
3302 user.roles.push(role_name.into());
3303 }
3304 cat.security_version = cat.security_version.wrapping_add(1);
3305 cat.db_epoch = epoch.0;
3306 }
3307 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3308 self.epoch.publish_in_order(epoch);
3309 _epoch_guard.disarm();
3310 Ok(())
3311 }
3312
3313 pub fn revoke_role(&self, username: &str, role_name: &str) -> Result<()> {
3315 self.require(&crate::auth::Permission::Admin)?;
3316 let epoch = self.epoch.bump_assigned();
3317 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3318 {
3319 let mut cat = self.catalog.write();
3320 let user = cat
3321 .users
3322 .iter_mut()
3323 .find(|u| u.username == username)
3324 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
3325 user.roles.retain(|r| r != role_name);
3326 cat.security_version = cat.security_version.wrapping_add(1);
3327 cat.db_epoch = epoch.0;
3328 }
3329 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3330 self.epoch.publish_in_order(epoch);
3331 _epoch_guard.disarm();
3332 Ok(())
3333 }
3334
3335 pub fn grant_permission(
3337 &self,
3338 role_name: &str,
3339 permission: crate::auth::Permission,
3340 ) -> Result<()> {
3341 self.require(&crate::auth::Permission::Admin)?;
3342 let epoch = self.epoch.bump_assigned();
3343 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3344 {
3345 let mut cat = self.catalog.write();
3346 let role = cat
3347 .roles
3348 .iter_mut()
3349 .find(|r| r.name == role_name)
3350 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
3351 merge_permission(&mut role.permissions, permission);
3352 cat.security_version = cat.security_version.wrapping_add(1);
3353 cat.db_epoch = epoch.0;
3354 }
3355 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3356 self.epoch.publish_in_order(epoch);
3357 _epoch_guard.disarm();
3358 Ok(())
3359 }
3360
3361 pub fn revoke_permission(
3363 &self,
3364 role_name: &str,
3365 permission: crate::auth::Permission,
3366 ) -> Result<()> {
3367 self.require(&crate::auth::Permission::Admin)?;
3368 let epoch = self.epoch.bump_assigned();
3369 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3370 {
3371 let mut cat = self.catalog.write();
3372 let role = cat
3373 .roles
3374 .iter_mut()
3375 .find(|r| r.name == role_name)
3376 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
3377 revoke_permission_from(&mut role.permissions, &permission);
3378 cat.security_version = cat.security_version.wrapping_add(1);
3379 cat.db_epoch = epoch.0;
3380 }
3381 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3382 self.epoch.publish_in_order(epoch);
3383 _epoch_guard.disarm();
3384 Ok(())
3385 }
3386
3387 pub fn resolve_principal(&self, username: &str) -> Option<crate::auth::Principal> {
3390 let cat = self.catalog.read();
3391 Self::resolve_principal_from_catalog(&cat, username)
3392 }
3393
3394 fn resolve_principal_from_catalog(
3399 cat: &Catalog,
3400 username: &str,
3401 ) -> Option<crate::auth::Principal> {
3402 let user = cat.users.iter().find(|u| u.username == username)?;
3403 let mut permissions = Vec::new();
3404 for role_name in &user.roles {
3405 if let Some(role) = cat.roles.iter().find(|r| &r.name == role_name) {
3406 permissions.extend(role.permissions.iter().cloned());
3407 }
3408 }
3409 Some(crate::auth::Principal {
3410 username: user.username.clone(),
3411 is_admin: user.is_admin,
3412 roles: user.roles.clone(),
3413 permissions,
3414 })
3415 }
3416
3417 pub fn check_permission(&self, username: &str, permission: &crate::auth::Permission) -> bool {
3419 match self.resolve_principal(username) {
3420 Some(p) => p.has_permission(permission),
3421 None => false,
3422 }
3423 }
3424
3425 pub fn require_auth_enabled(&self) -> bool {
3429 self.catalog.read().require_auth
3430 }
3431
3432 pub fn principal(&self) -> Option<crate::auth::Principal> {
3436 self.principal.read().clone()
3437 }
3438
3439 fn table_auth_checker(&self) -> Option<Arc<dyn crate::auth_state::TableAuthChecker>> {
3445 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
3446 self.auth_state.clone(),
3447 )))
3448 }
3449
3450 pub fn refresh_principal(&self) -> Result<()> {
3463 let username = match self.principal.read().clone() {
3464 Some(p) => p.username,
3465 None => return Ok(()),
3466 };
3467 let cat = catalog::read(&self.root, self.meta_dek.as_ref())?
3471 .ok_or_else(|| MongrelError::NotFound("catalog vanished during refresh".into()))?;
3472 *self.catalog.write() = cat.clone();
3475 match Self::resolve_principal_from_catalog(&cat, &username) {
3476 Some(p) => {
3477 *self.principal.write() = Some(p.clone());
3478 self.auth_state.set_principal(Some(p));
3482 Ok(())
3483 }
3484 None => Err(MongrelError::InvalidCredentials { username }),
3485 }
3486 }
3487
3488 pub fn enable_auth(&self, admin_username: &str, admin_password: &str) -> Result<()> {
3500 let password_hash =
3501 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
3502 let epoch = self.epoch.bump_assigned();
3503 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3504 {
3505 let mut cat = self.catalog.write();
3506 if cat.require_auth {
3507 return Err(MongrelError::InvalidArgument(
3508 "database already has require_auth enabled".into(),
3509 ));
3510 }
3511 if cat.users.iter().any(|u| u.username == admin_username) {
3514 return Err(MongrelError::InvalidArgument(format!(
3515 "user {admin_username:?} already exists"
3516 )));
3517 }
3518 cat.next_user_id = cat.next_user_id.max(1);
3519 let id = cat.next_user_id;
3520 cat.next_user_id += 1;
3521 cat.users.push(crate::auth::UserEntry {
3522 id,
3523 username: admin_username.to_string(),
3524 password_hash,
3525 roles: Vec::new(),
3526 is_admin: true,
3527 created_epoch: epoch.0,
3528 });
3529 cat.require_auth = true;
3530 cat.security_version = cat.security_version.wrapping_add(1);
3531 cat.db_epoch = epoch.0;
3532 }
3533 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3534 *self.principal.write() = Some(crate::auth::Principal {
3537 username: admin_username.to_string(),
3538 is_admin: true,
3539 roles: Vec::new(),
3540 permissions: Vec::new(),
3541 });
3542 self.auth_state.set_require_auth(true);
3543 self.epoch.publish_in_order(epoch);
3544 _epoch_guard.disarm();
3545 Ok(())
3546 }
3547
3548 pub fn disable_auth(&self) -> Result<()> {
3565 let epoch = self.epoch.bump_assigned();
3566 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3567 {
3568 let mut cat = self.catalog.write();
3569 if !cat.require_auth {
3570 return Err(MongrelError::InvalidArgument(
3571 "database does not have require_auth enabled".into(),
3572 ));
3573 }
3574 cat.require_auth = false;
3575 cat.security_version = cat.security_version.wrapping_add(1);
3576 cat.db_epoch = epoch.0;
3577 }
3578 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3579 *self.principal.write() = None;
3581 self.auth_state.set_require_auth(false);
3583 self.epoch.publish_in_order(epoch);
3584 _epoch_guard.disarm();
3585 Ok(())
3586 }
3587
3588 pub fn require(&self, perm: &crate::auth::Permission) -> Result<()> {
3595 if self.read_only && !matches!(perm, crate::auth::Permission::Select { .. }) {
3596 return Err(MongrelError::ReadOnlyReplica);
3597 }
3598 if self.principal.read().is_some() {
3599 self.refresh_principal().map_err(|error| match error {
3600 MongrelError::InvalidCredentials { .. } => MongrelError::AuthRequired,
3601 error => error,
3602 })?;
3603 }
3604 if !self.catalog.read().require_auth {
3605 return Ok(());
3606 }
3607 let guard = self.principal.read();
3608 let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
3609 if p.has_permission(perm) {
3610 Ok(())
3611 } else {
3612 Err(MongrelError::PermissionDenied {
3613 required: perm.clone(),
3614 principal: p.username.clone(),
3615 })
3616 }
3617 }
3618
3619 pub fn require_table(
3624 &self,
3625 table: &str,
3626 perm: crate::auth_state::RequiredPermission,
3627 ) -> Result<()> {
3628 self.require(&perm.into_permission(table))
3629 }
3630
3631 pub fn triggers(&self) -> Vec<StoredTrigger> {
3632 self.catalog
3633 .read()
3634 .triggers
3635 .iter()
3636 .map(|t| t.trigger.clone())
3637 .collect()
3638 }
3639
3640 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
3641 self.catalog
3642 .read()
3643 .triggers
3644 .iter()
3645 .find(|t| t.trigger.name == name)
3646 .map(|t| t.trigger.clone())
3647 }
3648
3649 pub fn create_trigger(&self, mut trigger: StoredTrigger) -> Result<StoredTrigger> {
3650 self.require(&crate::auth::Permission::Ddl)?;
3651 let _g = self.ddl_lock.lock();
3652 trigger.validate()?;
3653 self.validate_trigger_references(&trigger)?;
3654 {
3655 let cat = self.catalog.read();
3656 if cat.triggers.iter().any(|t| t.trigger.name == trigger.name) {
3657 return Err(MongrelError::InvalidArgument(format!(
3658 "trigger {:?} already exists",
3659 trigger.name
3660 )));
3661 }
3662 }
3663 let commit_lock = Arc::clone(&self.commit_lock);
3664 let _c = commit_lock.lock();
3665 let epoch = self.epoch.bump_assigned();
3666 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3667 trigger.created_epoch = epoch.0;
3668 trigger.updated_epoch = epoch.0;
3669 {
3670 let mut cat = self.catalog.write();
3671 cat.triggers.push(TriggerEntry::from(trigger.clone()));
3672 cat.db_epoch = epoch.0;
3673 }
3674 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3675 self.epoch.publish_in_order(epoch);
3676 _epoch_guard.disarm();
3677 Ok(trigger)
3678 }
3679
3680 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
3681 let _g = self.ddl_lock.lock();
3682 trigger.validate()?;
3683 self.validate_trigger_references(&trigger)?;
3684 let commit_lock = Arc::clone(&self.commit_lock);
3685 let _c = commit_lock.lock();
3686 let epoch = self.epoch.bump_assigned();
3687 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3688 let replaced = {
3689 let mut cat = self.catalog.write();
3690 let next = match cat
3691 .triggers
3692 .iter()
3693 .position(|t| t.trigger.name == trigger.name)
3694 {
3695 Some(idx) => {
3696 let next = cat.triggers[idx]
3697 .trigger
3698 .replaced(trigger.clone(), epoch.0)?;
3699 cat.triggers[idx] = TriggerEntry::from(next.clone());
3700 next
3701 }
3702 None => {
3703 let mut next = trigger;
3704 next.created_epoch = epoch.0;
3705 next.updated_epoch = epoch.0;
3706 cat.triggers.push(TriggerEntry::from(next.clone()));
3707 next
3708 }
3709 };
3710 cat.db_epoch = epoch.0;
3711 next
3712 };
3713 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3714 self.epoch.publish_in_order(epoch);
3715 _epoch_guard.disarm();
3716 Ok(replaced)
3717 }
3718
3719 pub fn drop_trigger(&self, name: &str) -> Result<()> {
3720 self.require(&crate::auth::Permission::Ddl)?;
3721 let _g = self.ddl_lock.lock();
3722 let commit_lock = Arc::clone(&self.commit_lock);
3723 let _c = commit_lock.lock();
3724 let epoch = self.epoch.bump_assigned();
3725 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3726 {
3727 let mut cat = self.catalog.write();
3728 let before = cat.triggers.len();
3729 cat.triggers.retain(|t| t.trigger.name != name);
3730 if cat.triggers.len() == before {
3731 return Err(MongrelError::NotFound(format!(
3732 "trigger {name:?} not found"
3733 )));
3734 }
3735 cat.db_epoch = epoch.0;
3736 }
3737 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3738 self.epoch.publish_in_order(epoch);
3739 _epoch_guard.disarm();
3740 Ok(())
3741 }
3742
3743 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
3744 self.catalog.read().external_tables.clone()
3745 }
3746
3747 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
3748 self.catalog
3749 .read()
3750 .external_tables
3751 .iter()
3752 .find(|t| t.name == name)
3753 .cloned()
3754 }
3755
3756 pub fn create_external_table(
3757 &self,
3758 mut entry: ExternalTableEntry,
3759 ) -> Result<ExternalTableEntry> {
3760 self.require(&crate::auth::Permission::Ddl)?;
3761 let _g = self.ddl_lock.lock();
3762 entry.validate()?;
3763 {
3764 let cat = self.catalog.read();
3765 if cat.live(&entry.name).is_some()
3766 || cat.external_tables.iter().any(|t| t.name == entry.name)
3767 {
3768 return Err(MongrelError::InvalidArgument(format!(
3769 "table {:?} already exists",
3770 entry.name
3771 )));
3772 }
3773 }
3774 let commit_lock = Arc::clone(&self.commit_lock);
3775 let _c = commit_lock.lock();
3776 let epoch = self.epoch.bump_assigned();
3777 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3778 entry.created_epoch = epoch.0;
3779 {
3780 let mut cat = self.catalog.write();
3781 cat.external_tables.push(entry.clone());
3782 cat.db_epoch = epoch.0;
3783 }
3784 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3785 self.epoch.publish_in_order(epoch);
3786 _epoch_guard.disarm();
3787 Ok(entry)
3788 }
3789
3790 pub fn drop_external_table(&self, name: &str) -> Result<()> {
3791 self.require(&crate::auth::Permission::Ddl)?;
3792 let _g = self.ddl_lock.lock();
3793 let commit_lock = Arc::clone(&self.commit_lock);
3794 let _c = commit_lock.lock();
3795 let epoch = self.epoch.bump_assigned();
3796 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3797 {
3798 let mut cat = self.catalog.write();
3799 let before = cat.external_tables.len();
3800 cat.external_tables.retain(|t| t.name != name);
3801 if cat.external_tables.len() == before {
3802 return Err(MongrelError::NotFound(format!(
3803 "external table {name:?} not found"
3804 )));
3805 }
3806 cat.db_epoch = epoch.0;
3807 }
3808 let state_dir = self.root.join(VTAB_DIR).join(name);
3809 if state_dir.exists() {
3810 std::fs::remove_dir_all(state_dir)?;
3811 }
3812 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
3813 self.epoch.publish_in_order(epoch);
3814 _epoch_guard.disarm();
3815 Ok(())
3816 }
3817
3818 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
3819 let txn_id = self.alloc_txn_id();
3820 self.commit_transaction_with_external_states(
3821 txn_id,
3822 self.epoch.visible(),
3823 Vec::new(),
3824 vec![(name.to_string(), state.to_vec())],
3825 Vec::new(),
3826 None,
3827 false,
3828 None,
3829 )
3830 .map(|(epoch, _)| epoch)
3831 }
3832
3833 pub fn trigger_config(&self) -> TriggerConfig {
3834 use std::sync::atomic::Ordering;
3835 TriggerConfig {
3836 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
3837 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
3838 max_loop_iterations: self.trigger_max_loop_iterations.load(Ordering::Relaxed),
3839 }
3840 }
3841
3842 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
3843 use std::sync::atomic::Ordering;
3844 if config.max_depth == 0 {
3845 return Err(MongrelError::InvalidArgument(
3846 "trigger max_depth must be greater than 0".into(),
3847 ));
3848 }
3849 self.trigger_recursive
3850 .store(config.recursive_triggers, Ordering::Relaxed);
3851 self.trigger_max_depth
3852 .store(config.max_depth, Ordering::Relaxed);
3853 self.trigger_max_loop_iterations
3854 .store(config.max_loop_iterations, Ordering::Relaxed);
3855 Ok(())
3856 }
3857
3858 pub fn set_recursive_triggers(&self, recursive: bool) {
3859 use std::sync::atomic::Ordering;
3860 self.trigger_recursive.store(recursive, Ordering::Relaxed);
3861 }
3862
3863 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent> {
3867 self.notify.subscribe()
3868 }
3869
3870 pub fn subscribe_change_commits(&self) -> tokio::sync::broadcast::Receiver<()> {
3871 self.change_wake.subscribe()
3872 }
3873
3874 pub fn change_events_since(&self, last_event_id: Option<&str>) -> Result<CdcBatch> {
3879 use crate::wal::Op;
3880
3881 let resume = match last_event_id {
3882 Some(id) => {
3883 let (epoch, index) = id.split_once(':').ok_or_else(|| {
3884 MongrelError::InvalidArgument(format!(
3885 "invalid CDC event id {id:?}; expected <epoch>:<index>"
3886 ))
3887 })?;
3888 Some((
3889 epoch.parse::<u64>().map_err(|error| {
3890 MongrelError::InvalidArgument(format!("invalid CDC epoch: {error}"))
3891 })?,
3892 index.parse::<u32>().map_err(|error| {
3893 MongrelError::InvalidArgument(format!("invalid CDC index: {error}"))
3894 })?,
3895 ))
3896 }
3897 None => None,
3898 };
3899
3900 let mut wal = self.shared_wal.lock();
3901 wal.group_sync()?;
3902 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
3903 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
3904 drop(wal);
3905
3906 let commits: HashMap<u64, (u64, Vec<crate::wal::AddedRun>)> = records
3907 .iter()
3908 .filter_map(|record| match &record.op {
3909 Op::TxnCommit { epoch, added_runs } => {
3910 Some((record.txn_id, (*epoch, added_runs.clone())))
3911 }
3912 _ => None,
3913 })
3914 .collect();
3915 let earliest_epoch = commits.values().map(|(epoch, _)| *epoch).min();
3916 let current_epoch = self.epoch.visible().0;
3917 let gap = resume.is_some_and(|(epoch, _)| {
3918 epoch < current_epoch
3919 && earliest_epoch.map_or(true, |earliest| earliest > epoch.saturating_add(1))
3920 });
3921 if gap {
3922 return Ok(CdcBatch {
3923 events: Vec::new(),
3924 current_epoch,
3925 earliest_epoch,
3926 gap: true,
3927 });
3928 }
3929
3930 let table_names: HashMap<u64, String> = self
3931 .catalog
3932 .read()
3933 .tables
3934 .iter()
3935 .map(|entry| (entry.table_id, entry.name.clone()))
3936 .collect();
3937 let before_images: HashMap<(u64, u64, u64), crate::memtable::Row> = records
3938 .iter()
3939 .filter_map(|record| {
3940 if !commits.contains_key(&record.txn_id) {
3941 return None;
3942 }
3943 let Op::BeforeImage {
3944 table_id,
3945 row_id,
3946 row,
3947 } = &record.op
3948 else {
3949 return None;
3950 };
3951 bincode::deserialize(row)
3952 .ok()
3953 .map(|before| ((record.txn_id, *table_id, row_id.0), before))
3954 })
3955 .collect();
3956 let mut operation_indices: HashMap<u64, u32> = HashMap::new();
3957 let mut events = Vec::new();
3958 for record in &records {
3959 let Some((commit_epoch, _)) = commits.get(&record.txn_id) else {
3960 continue;
3961 };
3962 let event = match &record.op {
3963 Op::Put { table_id, rows } => {
3964 let rows: Vec<crate::memtable::Row> = bincode::deserialize(rows)?;
3965 let data = serde_json::to_value(rows)
3966 .map_err(|error| MongrelError::Other(format!("CDC JSON: {error}")))?;
3967 Some((*table_id, "put", data))
3968 }
3969 Op::Delete { table_id, row_ids } => {
3970 let before = row_ids
3971 .iter()
3972 .filter_map(|row_id| {
3973 before_images
3974 .get(&(record.txn_id, *table_id, row_id.0))
3975 .cloned()
3976 })
3977 .collect::<Vec<_>>();
3978 Some((
3979 *table_id,
3980 "delete",
3981 serde_json::json!({
3982 "row_ids": row_ids.iter().map(|row_id| row_id.0).collect::<Vec<_>>(),
3983 "before": before,
3984 }),
3985 ))
3986 }
3987 Op::TruncateTable { table_id } => {
3988 Some((*table_id, "truncate", serde_json::Value::Null))
3989 }
3990 _ => None,
3991 };
3992 if let Some((table_id, op, data)) = event {
3993 let index = operation_indices.entry(record.txn_id).or_insert(0);
3994 let event_position = (*commit_epoch, *index);
3995 *index = index.saturating_add(1);
3996 if resume.is_some_and(|position| event_position <= position) {
3997 continue;
3998 }
3999 events.push(ChangeEvent {
4000 id: Some(format!("{}:{}", event_position.0, event_position.1)),
4001 channel: "changes".into(),
4002 table_id: Some(table_id),
4003 table: table_names.get(&table_id).cloned().unwrap_or_default(),
4004 op: op.into(),
4005 epoch: *commit_epoch,
4006 txn_id: Some(record.txn_id),
4007 message: None,
4008 data: Some(data),
4009 });
4010 }
4011 if let Op::TxnCommit { added_runs, .. } = &record.op {
4012 for run in added_runs {
4013 let index = operation_indices.entry(record.txn_id).or_insert(0);
4014 let event_position = (*commit_epoch, *index);
4015 *index = index.saturating_add(1);
4016 if resume.is_some_and(|position| event_position <= position) {
4017 continue;
4018 }
4019 let handle = self.tables.read().get(&run.table_id).cloned();
4020 let rows = handle.and_then(|handle| {
4021 let table = handle.lock();
4022 let mut reader = table.open_reader(run.run_id).ok()?;
4023 let mut rows = reader.all_rows().ok()?;
4024 for row in &mut rows {
4025 row.committed_epoch = Epoch(*commit_epoch);
4026 }
4027 Some(rows)
4028 });
4029 let Some(rows) = rows else {
4030 return Ok(CdcBatch {
4035 events: Vec::new(),
4036 current_epoch,
4037 earliest_epoch,
4038 gap: true,
4039 });
4040 };
4041 events.push(ChangeEvent {
4042 id: Some(format!("{}:{}", event_position.0, event_position.1)),
4043 channel: "changes".into(),
4044 table_id: Some(run.table_id),
4045 table: table_names.get(&run.table_id).cloned().unwrap_or_default(),
4046 op: "put_run".into(),
4047 epoch: *commit_epoch,
4048 txn_id: Some(record.txn_id),
4049 message: None,
4050 data: Some(serde_json::json!({
4051 "run_id": run.run_id.to_string(),
4052 "row_count": run.row_count,
4053 "min_row_id": run.min_row_id,
4054 "max_row_id": run.max_row_id,
4055 "rows": rows,
4056 })),
4057 });
4058 }
4059 }
4060 }
4061 Ok(CdcBatch {
4062 events,
4063 current_epoch,
4064 earliest_epoch,
4065 gap: false,
4066 })
4067 }
4068
4069 pub fn notify(&self, channel: &str, message: Option<String>) {
4072 let _ = self.notify.send(ChangeEvent {
4073 id: None,
4074 channel: channel.to_string(),
4075 table_id: None,
4076 table: String::new(),
4077 op: "notify".into(),
4078 epoch: self.epoch.visible().0,
4079 txn_id: None,
4080 message,
4081 data: None,
4082 });
4083 }
4084
4085 pub fn call_procedure(
4086 &self,
4087 name: &str,
4088 args: HashMap<String, crate::Value>,
4089 ) -> Result<ProcedureCallResult> {
4090 self.call_procedure_as(name, args, None)
4091 }
4092
4093 pub fn call_procedure_as(
4094 &self,
4095 name: &str,
4096 args: HashMap<String, crate::Value>,
4097 principal: Option<&crate::auth::Principal>,
4098 ) -> Result<ProcedureCallResult> {
4099 self.require_for(principal, &crate::auth::Permission::All)?;
4103 let procedure = self
4104 .procedure(name)
4105 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
4106 let args = bind_procedure_args(&procedure, args)?;
4107 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
4108 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
4109 if has_writes {
4110 let mut tx = self.begin_as(principal.cloned());
4111 let run = (|| {
4112 for step in &procedure.body.steps {
4113 let output = self.execute_procedure_step(
4114 step,
4115 &args,
4116 &outputs,
4117 Some(&mut tx),
4118 principal,
4119 )?;
4120 outputs.insert(step.id().to_string(), output);
4121 }
4122 eval_return_output(&procedure.body.return_value, &args, &outputs)
4123 })();
4124 match run {
4125 Ok(output) => {
4126 let epoch = tx.commit()?.0;
4127 Ok(ProcedureCallResult {
4128 epoch: Some(epoch),
4129 output,
4130 })
4131 }
4132 Err(e) => {
4133 tx.rollback();
4134 Err(e)
4135 }
4136 }
4137 } else {
4138 for step in &procedure.body.steps {
4139 let output = self.execute_procedure_step(step, &args, &outputs, None, principal)?;
4140 outputs.insert(step.id().to_string(), output);
4141 }
4142 Ok(ProcedureCallResult {
4143 epoch: None,
4144 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
4145 })
4146 }
4147 }
4148
4149 fn execute_procedure_step(
4150 &self,
4151 step: &ProcedureStep,
4152 args: &HashMap<String, crate::Value>,
4153 outputs: &HashMap<String, ProcedureCallOutput>,
4154 tx: Option<&mut crate::txn::Transaction<'_>>,
4155 principal: Option<&crate::auth::Principal>,
4156 ) -> Result<ProcedureCallOutput> {
4157 match step {
4158 ProcedureStep::NativeQuery {
4159 table,
4160 conditions,
4161 projection,
4162 limit,
4163 ..
4164 } => {
4165 let mut q = crate::Query::new();
4166 for condition in conditions {
4167 q = q.and(eval_condition(condition, args, outputs)?);
4168 }
4169 let handle = self.table(table)?;
4170 let rows = handle.lock().query(&q)?;
4171 let mut rows = self.secure_rows_for(table, rows, principal)?;
4172 if let Some(limit) = limit {
4173 rows.truncate(*limit);
4174 }
4175 let projection = projection.as_ref();
4176 Ok(ProcedureCallOutput::Rows(
4177 rows.into_iter()
4178 .map(|row| ProcedureCallRow {
4179 row_id: Some(row.row_id),
4180 columns: match projection {
4181 Some(ids) => row
4182 .columns
4183 .into_iter()
4184 .filter(|(id, _)| ids.contains(id))
4185 .collect(),
4186 None => row.columns,
4187 },
4188 })
4189 .collect(),
4190 ))
4191 }
4192 ProcedureStep::Put {
4193 table,
4194 cells,
4195 returning,
4196 ..
4197 } => {
4198 let tx = tx.ok_or_else(|| {
4199 MongrelError::InvalidArgument(
4200 "write procedure step requires a transaction".into(),
4201 )
4202 })?;
4203 let cells = eval_cells(cells, args, outputs)?;
4204 if *returning {
4205 let out = tx.put_returning(table, cells)?;
4206 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
4207 row_id: None,
4208 columns: out.row.columns.into_iter().collect(),
4209 }))
4210 } else {
4211 tx.put(table, cells)?;
4212 Ok(ProcedureCallOutput::Null)
4213 }
4214 }
4215 ProcedureStep::Upsert {
4216 table,
4217 cells,
4218 update_cells,
4219 returning,
4220 ..
4221 } => {
4222 let tx = tx.ok_or_else(|| {
4223 MongrelError::InvalidArgument(
4224 "write procedure step requires a transaction".into(),
4225 )
4226 })?;
4227 let cells = eval_cells(cells, args, outputs)?;
4228 let action = match update_cells {
4229 Some(update_cells) => {
4230 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
4231 }
4232 None => crate::UpsertAction::DoNothing,
4233 };
4234 let out = tx.upsert(table, cells, action)?;
4235 if *returning {
4236 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
4237 row_id: None,
4238 columns: out.row.columns.into_iter().collect(),
4239 }))
4240 } else {
4241 Ok(ProcedureCallOutput::Null)
4242 }
4243 }
4244 ProcedureStep::DeleteByPk { table, pk, .. } => {
4245 let tx = tx.ok_or_else(|| {
4246 MongrelError::InvalidArgument(
4247 "write procedure step requires a transaction".into(),
4248 )
4249 })?;
4250 let pk = eval_value(pk, args, outputs)?;
4251 let handle = self.table(table)?;
4252 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
4253 MongrelError::NotFound("procedure delete_by_pk target not found".into())
4254 })?;
4255 tx.delete(table, row_id)?;
4256 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
4257 }
4258 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
4259 "DeleteRows procedure step is not supported by the core executor yet".into(),
4260 )),
4261 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
4262 "SqlQuery procedure step must be executed by mongreldb-query".into(),
4263 )),
4264 }
4265 }
4266
4267 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
4268 let cat = self.catalog.read();
4269 for step in &procedure.body.steps {
4270 let Some(table_name) = step.table() else {
4271 continue;
4272 };
4273 let schema = &cat
4274 .live(table_name)
4275 .ok_or_else(|| {
4276 MongrelError::InvalidArgument(format!(
4277 "procedure {:?} references unknown table {table_name:?}",
4278 procedure.name
4279 ))
4280 })?
4281 .schema;
4282 match step {
4283 ProcedureStep::NativeQuery {
4284 conditions,
4285 projection,
4286 ..
4287 } => {
4288 for condition in conditions {
4289 validate_condition_columns(condition, schema)?;
4290 }
4291 if let Some(projection) = projection {
4292 for id in projection {
4293 validate_column_id(*id, schema)?;
4294 }
4295 }
4296 }
4297 ProcedureStep::Put { cells, .. } => {
4298 for cell in cells {
4299 validate_column_id(cell.column_id, schema)?;
4300 }
4301 }
4302 ProcedureStep::Upsert {
4303 cells,
4304 update_cells,
4305 ..
4306 } => {
4307 for cell in cells {
4308 validate_column_id(cell.column_id, schema)?;
4309 }
4310 if let Some(update_cells) = update_cells {
4311 for cell in update_cells {
4312 validate_column_id(cell.column_id, schema)?;
4313 }
4314 }
4315 }
4316 ProcedureStep::DeleteByPk { .. } => {
4317 if schema.primary_key().is_none() {
4318 return Err(MongrelError::InvalidArgument(format!(
4319 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
4320 procedure.name
4321 )));
4322 }
4323 }
4324 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
4325 }
4326 }
4327 Ok(())
4328 }
4329
4330 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
4331 let cat = self.catalog.read();
4332 let target_schema = match &trigger.target {
4333 TriggerTarget::Table(target_name) => cat
4334 .live(target_name)
4335 .ok_or_else(|| {
4336 MongrelError::InvalidArgument(format!(
4337 "trigger {:?} references unknown target table {target_name:?}",
4338 trigger.name
4339 ))
4340 })?
4341 .schema
4342 .clone(),
4343 TriggerTarget::View(_) => Schema {
4344 columns: trigger.target_columns.clone(),
4345 ..Schema::default()
4346 },
4347 };
4348 for col in &trigger.update_of {
4349 if target_schema.column(col).is_none() {
4350 return Err(MongrelError::InvalidArgument(format!(
4351 "trigger {:?} UPDATE OF references unknown column {col:?}",
4352 trigger.name
4353 )));
4354 }
4355 }
4356 if let Some(expr) = &trigger.when {
4357 validate_trigger_expr(expr, &target_schema, trigger.event)?;
4358 }
4359 let mut select_schemas: HashMap<String, &Schema> = HashMap::new();
4360 for step in &trigger.program.steps {
4361 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
4362 {
4363 return Err(MongrelError::InvalidArgument(
4364 "SetNew trigger steps are only valid in BEFORE triggers".into(),
4365 ));
4366 }
4367 validate_trigger_step(
4368 step,
4369 &cat,
4370 &target_schema,
4371 trigger.event,
4372 &mut select_schemas,
4373 )?;
4374 }
4375 Ok(())
4376 }
4377
4378 pub fn begin(&self) -> crate::txn::Transaction<'_> {
4380 self.begin_with_isolation(crate::txn::IsolationLevel::default())
4381 }
4382
4383 pub fn begin_as(
4384 &self,
4385 principal: Option<crate::auth::Principal>,
4386 ) -> crate::txn::Transaction<'_> {
4387 let catalog_bound = principal.as_ref().is_some_and(|principal| {
4388 self.catalog
4389 .read()
4390 .users
4391 .iter()
4392 .any(|user| user.username == principal.username)
4393 });
4394 let txn_id = self.alloc_txn_id();
4395 let read = Snapshot::at(self.epoch.visible());
4396 crate::txn::Transaction::new(self, txn_id, read).with_principal(principal, catalog_bound)
4397 }
4398
4399 pub fn begin_with_isolation(
4401 &self,
4402 level: crate::txn::IsolationLevel,
4403 ) -> crate::txn::Transaction<'_> {
4404 let txn_id = self.alloc_txn_id();
4405 let epoch = match level {
4406 crate::txn::IsolationLevel::ReadCommitted => self.epoch.visible(),
4407 _ => self.epoch.visible(),
4408 };
4409 let read = Snapshot::at(epoch);
4410 crate::txn::Transaction::new(self, txn_id, read)
4411 }
4412
4413 pub fn begin_with_external_trigger_bridge<'a>(
4416 &'a self,
4417 bridge: &'a dyn ExternalTriggerBridge,
4418 ) -> crate::txn::Transaction<'a> {
4419 let txn_id = self.alloc_txn_id();
4420 let read = Snapshot::at(self.epoch.visible());
4421 crate::txn::Transaction::new(self, txn_id, read).with_external_trigger_bridge(bridge)
4422 }
4423
4424 pub fn begin_with_external_trigger_bridge_as<'a>(
4425 &'a self,
4426 bridge: &'a dyn ExternalTriggerBridge,
4427 principal: Option<crate::auth::Principal>,
4428 ) -> crate::txn::Transaction<'a> {
4429 let catalog_bound = principal.as_ref().is_some_and(|principal| {
4430 self.catalog
4431 .read()
4432 .users
4433 .iter()
4434 .any(|user| user.username == principal.username)
4435 });
4436 let txn_id = self.alloc_txn_id();
4437 let read = Snapshot::at(self.epoch.visible());
4438 crate::txn::Transaction::new(self, txn_id, read)
4439 .with_external_trigger_bridge(bridge)
4440 .with_principal(principal, catalog_bound)
4441 }
4442
4443 pub fn transaction<T>(
4445 &self,
4446 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
4447 ) -> Result<T> {
4448 let mut tx = self.begin();
4449 match f(&mut tx) {
4450 Ok(out) => {
4451 tx.commit()?;
4452 Ok(out)
4453 }
4454 Err(e) => {
4455 tx.rollback();
4456 Err(e)
4457 }
4458 }
4459 }
4460
4461 pub fn transaction_with_row_ids<T>(
4462 &self,
4463 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
4464 ) -> Result<(T, Vec<RowId>)> {
4465 let mut tx = self.begin();
4466 match f(&mut tx) {
4467 Ok(output) => {
4468 let (_, row_ids) = tx.commit_with_row_ids()?;
4469 Ok((output, row_ids))
4470 }
4471 Err(error) => {
4472 tx.rollback();
4473 Err(error)
4474 }
4475 }
4476 }
4477
4478 pub fn transaction_for_current_principal<T>(
4479 &self,
4480 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
4481 ) -> Result<T> {
4482 if self.principal.read().is_some() {
4483 self.refresh_principal()?;
4484 }
4485 let mut transaction = self.begin_as(self.principal.read().clone());
4486 match f(&mut transaction) {
4487 Ok(output) => {
4488 transaction.commit()?;
4489 Ok(output)
4490 }
4491 Err(error) => {
4492 transaction.rollback();
4493 Err(error)
4494 }
4495 }
4496 }
4497
4498 pub fn transaction_for_current_principal_with_epoch<T>(
4499 &self,
4500 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
4501 ) -> Result<(Epoch, T)> {
4502 if self.principal.read().is_some() {
4503 self.refresh_principal()?;
4504 }
4505 let mut transaction = self.begin_as(self.principal.read().clone());
4506 match f(&mut transaction) {
4507 Ok(output) => {
4508 let epoch = transaction.commit()?;
4509 Ok((epoch, output))
4510 }
4511 Err(error) => {
4512 transaction.rollback();
4513 Err(error)
4514 }
4515 }
4516 }
4517
4518 pub fn transaction_with_row_ids_for_current_principal<T>(
4519 &self,
4520 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
4521 ) -> Result<(T, Vec<RowId>)> {
4522 if self.principal.read().is_some() {
4523 self.refresh_principal()?;
4524 }
4525 let mut transaction = self.begin_as(self.principal.read().clone());
4526 match f(&mut transaction) {
4527 Ok(output) => {
4528 let (_, row_ids) = transaction.commit_with_row_ids()?;
4529 Ok((output, row_ids))
4530 }
4531 Err(error) => {
4532 transaction.rollback();
4533 Err(error)
4534 }
4535 }
4536 }
4537
4538 pub fn transaction_with_external_trigger_bridge<'a, T>(
4541 &'a self,
4542 bridge: &'a dyn ExternalTriggerBridge,
4543 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
4544 ) -> Result<T> {
4545 let mut tx = self.begin_with_external_trigger_bridge(bridge);
4546 match f(&mut tx) {
4547 Ok(out) => {
4548 tx.commit()?;
4549 Ok(out)
4550 }
4551 Err(e) => {
4552 tx.rollback();
4553 Err(e)
4554 }
4555 }
4556 }
4557
4558 pub fn transaction_with_external_trigger_bridge_as<'a, T>(
4559 &'a self,
4560 bridge: &'a dyn ExternalTriggerBridge,
4561 principal: Option<crate::auth::Principal>,
4562 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
4563 ) -> Result<T> {
4564 let mut tx = self.begin_with_external_trigger_bridge_as(bridge, principal);
4565 match f(&mut tx) {
4566 Ok(output) => {
4567 tx.commit()?;
4568 Ok(output)
4569 }
4570 Err(error) => {
4571 tx.rollback();
4572 Err(error)
4573 }
4574 }
4575 }
4576
4577 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
4580 self.active_txns.register(epoch)
4581 }
4582
4583 fn fill_auto_increment_for_staging(
4584 &self,
4585 staging: &mut [(u64, crate::txn::Staged)],
4586 ) -> Result<()> {
4587 let tables = self.tables.read();
4588 for (table_id, staged) in staging {
4589 if let crate::txn::Staged::Put(cells) = staged {
4590 if let Some(handle) = tables.get(table_id) {
4591 let mut t = handle.lock();
4592 t.fill_auto_inc(cells)?;
4593 }
4594 }
4595 }
4596 Ok(())
4597 }
4598
4599 fn expand_table_triggers(
4600 &self,
4601 staging: &mut Vec<(u64, crate::txn::Staged)>,
4602 read_epoch: Epoch,
4603 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
4604 external_states: &mut Vec<(String, Vec<u8>)>,
4605 ) -> Result<()> {
4606 let mut external_writes = Vec::new();
4607 let config = self.trigger_config();
4608 if config.recursive_triggers {
4609 let chunk = std::mem::take(staging);
4610 let stacks = vec![Vec::new(); chunk.len()];
4611 *staging = self.expand_trigger_chunk(
4612 chunk,
4613 stacks,
4614 read_epoch,
4615 0,
4616 config.max_depth,
4617 &mut external_writes,
4618 &config,
4619 )?;
4620 self.apply_external_trigger_writes(
4621 external_writes,
4622 external_trigger_bridge,
4623 external_states,
4624 staging,
4625 )?;
4626 return Ok(());
4627 }
4628
4629 let mut expansion = self.expand_table_triggers_once(staging, read_epoch, None, &config)?;
4630 if !expansion.before.is_empty() {
4631 let mut final_staging = expansion.before;
4632 final_staging.extend(filter_ignored_staging(
4633 std::mem::take(staging),
4634 &expansion.ignored_indices,
4635 ));
4636 *staging = final_staging;
4637 } else if !expansion.ignored_indices.is_empty() {
4638 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
4639 }
4640 staging.append(&mut expansion.after);
4641 external_writes.append(&mut expansion.before_external);
4642 external_writes.append(&mut expansion.after_external);
4643 self.apply_external_trigger_writes(
4644 external_writes,
4645 external_trigger_bridge,
4646 external_states,
4647 staging,
4648 )?;
4649 Ok(())
4650 }
4651
4652 #[allow(clippy::too_many_arguments)]
4653 fn expand_trigger_chunk(
4654 &self,
4655 mut chunk: Vec<(u64, crate::txn::Staged)>,
4656 stacks: Vec<Vec<String>>,
4657 read_epoch: Epoch,
4658 depth: u32,
4659 max_depth: u32,
4660 external_writes: &mut Vec<ExternalTriggerWrite>,
4661 config: &TriggerConfig,
4662 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
4663 if chunk.is_empty() {
4664 return Ok(Vec::new());
4665 }
4666 self.fill_auto_increment_for_staging(&mut chunk)?;
4667 let expansion =
4668 self.expand_table_triggers_once(&mut chunk, read_epoch, Some(&stacks), config)?;
4669 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
4670 let stack = expansion
4671 .before_stacks
4672 .first()
4673 .or_else(|| expansion.after_stacks.first())
4674 .cloned()
4675 .unwrap_or_default();
4676 return Err(MongrelError::Conflict(format!(
4677 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
4678 Self::format_trigger_stack(&stack)
4679 )));
4680 }
4681
4682 let mut out = Vec::new();
4683 external_writes.extend(expansion.before_external);
4684 out.extend(self.expand_trigger_chunk(
4685 expansion.before,
4686 expansion.before_stacks,
4687 read_epoch,
4688 depth + 1,
4689 max_depth,
4690 external_writes,
4691 config,
4692 )?);
4693 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
4694 external_writes.extend(expansion.after_external);
4695 out.extend(self.expand_trigger_chunk(
4696 expansion.after,
4697 expansion.after_stacks,
4698 read_epoch,
4699 depth + 1,
4700 max_depth,
4701 external_writes,
4702 config,
4703 )?);
4704 Ok(out)
4705 }
4706
4707 fn apply_external_trigger_writes(
4708 &self,
4709 writes: Vec<ExternalTriggerWrite>,
4710 bridge: Option<&dyn ExternalTriggerBridge>,
4711 external_states: &mut Vec<(String, Vec<u8>)>,
4712 staging: &mut Vec<(u64, crate::txn::Staged)>,
4713 ) -> Result<()> {
4714 if writes.is_empty() {
4715 return Ok(());
4716 }
4717 let bridge = bridge.ok_or_else(|| {
4718 MongrelError::InvalidArgument(
4719 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
4720 )
4721 })?;
4722 for write in writes {
4723 let table = write.table().to_string();
4724 let entry = self.external_table(&table).ok_or_else(|| {
4725 MongrelError::NotFound(format!("external table {table:?} not found"))
4726 })?;
4727 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
4728 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
4729 external_states.push((table, result.state));
4730 for base_write in result.base_writes {
4731 match base_write {
4732 ExternalTriggerBaseWrite::Put { table, cells } => {
4733 let table_id = self.table_id(&table)?;
4734 staging.push((table_id, crate::txn::Staged::Put(cells)));
4735 }
4736 ExternalTriggerBaseWrite::Delete { table, row_id } => {
4737 let table_id = self.table_id(&table)?;
4738 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
4739 }
4740 }
4741 }
4742 }
4743 dedup_external_states_in_place(external_states);
4744 Ok(())
4745 }
4746
4747 fn expand_table_triggers_once(
4748 &self,
4749 staging: &mut Vec<(u64, crate::txn::Staged)>,
4750 read_epoch: Epoch,
4751 trigger_stacks: Option<&[Vec<String>]>,
4752 config: &TriggerConfig,
4753 ) -> Result<TriggerExpansion> {
4754 let triggers: Vec<StoredTrigger> = self
4755 .catalog
4756 .read()
4757 .triggers
4758 .iter()
4759 .filter(|entry| {
4760 entry.trigger.enabled
4761 && matches!(
4762 entry.trigger.timing,
4763 TriggerTiming::Before | TriggerTiming::After
4764 )
4765 && matches!(entry.trigger.target, TriggerTarget::Table(_))
4766 })
4767 .map(|entry| entry.trigger.clone())
4768 .collect();
4769 if triggers.is_empty() || staging.is_empty() {
4770 return Ok(TriggerExpansion::default());
4771 }
4772
4773 let before_triggers = triggers
4774 .iter()
4775 .filter(|trigger| trigger.timing == TriggerTiming::Before)
4776 .cloned()
4777 .collect::<Vec<_>>();
4778 let after_triggers = triggers
4779 .iter()
4780 .filter(|trigger| trigger.timing == TriggerTiming::After)
4781 .cloned()
4782 .collect::<Vec<_>>();
4783
4784 let mut before_added = Vec::new();
4785 let mut before_stacks = Vec::new();
4786 let mut before_external = Vec::new();
4787 let mut ignored_indices = std::collections::BTreeSet::new();
4788 if !before_triggers.is_empty() {
4789 let before_events =
4790 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks)?;
4791 let mut out = TriggerProgramOutput {
4792 added: &mut before_added,
4793 added_stacks: &mut before_stacks,
4794 added_external: &mut before_external,
4795 ignored_indices: &mut ignored_indices,
4796 };
4797 self.execute_triggers_for_events(
4798 &before_triggers,
4799 &before_events,
4800 Some(staging),
4801 &mut out,
4802 config,
4803 read_epoch,
4804 )?;
4805 }
4806
4807 let after_events = if after_triggers.is_empty() {
4808 Vec::new()
4809 } else {
4810 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks)?
4811 .into_iter()
4812 .filter(|event| {
4813 !event
4814 .op_indices
4815 .iter()
4816 .any(|idx| ignored_indices.contains(idx))
4817 })
4818 .collect()
4819 };
4820
4821 let mut after_added = Vec::new();
4822 let mut after_stacks = Vec::new();
4823 let mut after_external = Vec::new();
4824 let mut out = TriggerProgramOutput {
4825 added: &mut after_added,
4826 added_stacks: &mut after_stacks,
4827 added_external: &mut after_external,
4828 ignored_indices: &mut ignored_indices,
4829 };
4830 self.execute_triggers_for_events(
4831 &after_triggers,
4832 &after_events,
4833 None,
4834 &mut out,
4835 config,
4836 read_epoch,
4837 )?;
4838 Ok(TriggerExpansion {
4839 before: before_added,
4840 before_stacks,
4841 before_external,
4842 after: after_added,
4843 after_stacks,
4844 after_external,
4845 ignored_indices,
4846 })
4847 }
4848
4849 fn execute_triggers_for_events(
4850 &self,
4851 triggers: &[StoredTrigger],
4852 events: &[WriteEvent],
4853 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
4854 out: &mut TriggerProgramOutput<'_>,
4855 config: &TriggerConfig,
4856 read_epoch: Epoch,
4857 ) -> Result<()> {
4858 for event in events {
4859 for trigger in triggers {
4860 if event
4861 .op_indices
4862 .iter()
4863 .any(|idx| out.ignored_indices.contains(idx))
4864 {
4865 break;
4866 }
4867 let matches = {
4868 let cat = self.catalog.read();
4869 trigger_matches_event(trigger, event, &cat)?
4870 };
4871 if !matches {
4872 continue;
4873 }
4874 if let Some(when) = &trigger.when {
4875 if !eval_trigger_expr(when, event)? {
4876 continue;
4877 }
4878 }
4879 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
4880 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
4881 return Err(MongrelError::Conflict(format!(
4882 "trigger recursion cycle detected; trigger stack: {}",
4883 Self::format_trigger_stack(&trigger_stack)
4884 )));
4885 }
4886 let outcome = match staging.as_mut() {
4887 Some(staging) => self.execute_trigger_program(
4888 trigger,
4889 event,
4890 Some(&mut **staging),
4891 out,
4892 &trigger_stack,
4893 config,
4894 read_epoch,
4895 )?,
4896 None => self.execute_trigger_program(
4897 trigger,
4898 event,
4899 None,
4900 out,
4901 &trigger_stack,
4902 config,
4903 read_epoch,
4904 )?,
4905 };
4906 if outcome == TriggerProgramOutcome::Ignore {
4907 out.ignored_indices.extend(event.op_indices.iter().copied());
4908 break;
4909 }
4910 }
4911 }
4912 Ok(())
4913 }
4914
4915 fn trigger_events_for_staging(
4916 &self,
4917 staging: &[(u64, crate::txn::Staged)],
4918 read_epoch: Epoch,
4919 trigger_stacks: Option<&[Vec<String>]>,
4920 ) -> Result<Vec<WriteEvent>> {
4921 use crate::txn::Staged;
4922 use std::collections::{HashMap, VecDeque};
4923
4924 let snapshot = Snapshot::at(read_epoch);
4925 let cat = self.catalog.read();
4926 let mut table_names = HashMap::new();
4927 let mut table_schemas = HashMap::new();
4928 for entry in cat
4929 .tables
4930 .iter()
4931 .filter(|entry| matches!(entry.state, TableState::Live))
4932 {
4933 table_names.insert(entry.table_id, entry.name.clone());
4934 table_schemas.insert(entry.table_id, entry.schema.clone());
4935 }
4936 drop(cat);
4937
4938 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
4939 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
4940 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
4941
4942 for (idx, (table_id, staged)) in staging.iter().enumerate() {
4943 let Some(schema) = table_schemas.get(table_id) else {
4944 continue;
4945 };
4946 let Some(pk) = schema.primary_key() else {
4947 continue;
4948 };
4949 match staged {
4950 Staged::Delete(row_id) => {
4951 let handle = self.table_by_id(*table_id)?;
4952 let Some(row) = handle.lock().get(*row_id, snapshot) else {
4953 continue;
4954 };
4955 let Some(pk_value) = row.columns.get(&pk.id) else {
4956 continue;
4957 };
4958 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
4959 delete_by_key
4960 .entry((*table_id, pk_value.encode_key()))
4961 .or_default()
4962 .push_back(idx);
4963 }
4964 Staged::Put(cells) => {
4965 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
4966 put_by_key
4967 .entry((*table_id, value.encode_key()))
4968 .or_default()
4969 .push_back(idx);
4970 }
4971 }
4972 Staged::Update(row_id, _) => {
4973 let handle = self.table_by_id(*table_id)?;
4974 let row = handle.lock().get(*row_id, snapshot);
4975 if let Some(row) = row {
4976 old_rows.insert(idx, TriggerRowImage::from_row(row));
4977 }
4978 }
4979 Staged::Truncate => {}
4980 }
4981 }
4982
4983 let mut paired_delete = std::collections::HashSet::new();
4984 let mut paired_put = std::collections::HashSet::new();
4985 let mut events = Vec::new();
4986
4987 for (key, deletes) in delete_by_key.iter_mut() {
4988 let Some(puts) = put_by_key.get_mut(key) else {
4989 continue;
4990 };
4991 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
4992 paired_delete.insert(delete_idx);
4993 paired_put.insert(put_idx);
4994 let (table_id, _) = &staging[put_idx];
4995 let Some(table_name) = table_names.get(table_id).cloned() else {
4996 continue;
4997 };
4998 let old = old_rows.get(&delete_idx).cloned();
4999 let new = match &staging[put_idx].1 {
5000 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
5001 _ => None,
5002 };
5003 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
5004 events.push(WriteEvent {
5005 table: table_name,
5006 kind: TriggerEvent::Update,
5007 old,
5008 new,
5009 changed_columns,
5010 op_indices: vec![delete_idx, put_idx],
5011 put_idx: Some(put_idx),
5012 trigger_stack: Self::trigger_stack_for_indices(
5013 trigger_stacks,
5014 &[delete_idx, put_idx],
5015 ),
5016 });
5017 }
5018 }
5019
5020 for (idx, (table_id, staged)) in staging.iter().enumerate() {
5021 let Some(table_name) = table_names.get(table_id).cloned() else {
5022 continue;
5023 };
5024 match staged {
5025 Staged::Put(cells) if !paired_put.contains(&idx) => {
5026 let new = Some(TriggerRowImage::from_cells(cells));
5027 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
5028 events.push(WriteEvent {
5029 table: table_name,
5030 kind: TriggerEvent::Insert,
5031 old: None,
5032 new,
5033 changed_columns,
5034 op_indices: vec![idx],
5035 put_idx: Some(idx),
5036 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
5037 });
5038 }
5039 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
5040 let old = match old_rows.get(&idx).cloned() {
5041 Some(old) => Some(old),
5042 None => {
5043 let handle = self.table_by_id(*table_id)?;
5044 let row = handle.lock().get(*row_id, snapshot);
5045 row.map(TriggerRowImage::from_row)
5046 }
5047 };
5048 let Some(old) = old else {
5049 continue;
5050 };
5051 let changed_columns = old.columns.keys().copied().collect();
5052 events.push(WriteEvent {
5053 table: table_name,
5054 kind: TriggerEvent::Delete,
5055 old: Some(old),
5056 new: None,
5057 changed_columns,
5058 op_indices: vec![idx],
5059 put_idx: None,
5060 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
5061 });
5062 }
5063 Staged::Update(_, cells) => {
5064 let old = old_rows.get(&idx).cloned();
5065 let new = Some(TriggerRowImage::from_cells(cells));
5066 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
5067 events.push(WriteEvent {
5068 table: table_name,
5069 kind: TriggerEvent::Update,
5070 old,
5071 new,
5072 changed_columns,
5073 op_indices: vec![idx],
5074 put_idx: Some(idx),
5075 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
5076 });
5077 }
5078 Staged::Truncate => {}
5079 _ => {}
5080 }
5081 }
5082
5083 Ok(events)
5084 }
5085
5086 #[allow(clippy::too_many_arguments)]
5087 fn execute_trigger_program(
5088 &self,
5089 trigger: &StoredTrigger,
5090 event: &WriteEvent,
5091 staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
5092 out: &mut TriggerProgramOutput<'_>,
5093 trigger_stack: &[String],
5094 config: &TriggerConfig,
5095 read_epoch: Epoch,
5096 ) -> Result<TriggerProgramOutcome> {
5097 let mut event = event.clone();
5098 let mut select_results: HashMap<String, Vec<TriggerRowImage>> = HashMap::new();
5099 self.execute_trigger_steps(
5100 trigger,
5101 &trigger.program.steps,
5102 &mut event,
5103 staging,
5104 out,
5105 trigger_stack,
5106 config,
5107 &mut select_results,
5108 0,
5109 None,
5110 read_epoch,
5111 )
5112 }
5113
5114 #[allow(clippy::too_many_arguments)]
5115 fn execute_trigger_steps(
5116 &self,
5117 trigger: &StoredTrigger,
5118 steps: &[TriggerStep],
5119 event: &mut WriteEvent,
5120 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
5121 out: &mut TriggerProgramOutput<'_>,
5122 trigger_stack: &[String],
5123 config: &TriggerConfig,
5124 select_results: &mut HashMap<String, Vec<TriggerRowImage>>,
5125 depth: u32,
5126 selected: Option<&TriggerRowImage>,
5127 read_epoch: Epoch,
5128 ) -> Result<TriggerProgramOutcome> {
5129 let _ = depth;
5130 for step in steps {
5131 match step {
5132 TriggerStep::SetNew { cells } => {
5133 if trigger.timing != TriggerTiming::Before {
5134 return Err(MongrelError::InvalidArgument(
5135 "SetNew trigger step is only valid in BEFORE triggers".into(),
5136 ));
5137 }
5138 let put_idx = event.put_idx.ok_or_else(|| {
5139 MongrelError::InvalidArgument(
5140 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
5141 )
5142 })?;
5143 let staging = staging.as_deref_mut().ok_or_else(|| {
5144 MongrelError::InvalidArgument(
5145 "SetNew trigger step requires mutable trigger staging".into(),
5146 )
5147 })?;
5148 let row_cells = match staging.get_mut(put_idx).map(|(_, op)| op) {
5149 Some(crate::txn::Staged::Put(cells))
5150 | Some(crate::txn::Staged::Update(_, cells)) => cells,
5151 _ => {
5152 return Err(MongrelError::InvalidArgument(
5153 "SetNew trigger step target row is not mutable".into(),
5154 ))
5155 }
5156 };
5157 for (column_id, value) in eval_trigger_cells(cells, event, selected)? {
5158 row_cells.retain(|(id, _)| *id != column_id);
5159 row_cells.push((column_id, value.clone()));
5160 if let Some(new) = &mut event.new {
5161 new.columns.insert(column_id, value);
5162 }
5163 }
5164 row_cells.sort_by_key(|(id, _)| *id);
5165 }
5166 TriggerStep::Insert { table, cells } => {
5167 let cells = eval_trigger_cells(cells, event, selected)?;
5168 if let Ok(table_id) = self.table_id(table) {
5169 out.added.push((table_id, crate::txn::Staged::Put(cells)));
5170 out.added_stacks.push(trigger_stack.to_vec());
5171 } else if self.external_table(table).is_some() {
5172 out.added_external.push(ExternalTriggerWrite::Insert {
5173 table: table.clone(),
5174 cells,
5175 });
5176 } else {
5177 return Err(MongrelError::NotFound(format!(
5178 "trigger {:?} insert target {table:?} not found",
5179 trigger.name
5180 )));
5181 }
5182 }
5183 TriggerStep::UpdateByPk { table, pk, cells } => {
5184 let pk = eval_trigger_value(pk, event, selected)?;
5185 let cells = eval_trigger_cells(cells, event, selected)?;
5186 if self.external_table(table).is_some() {
5187 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
5188 table: table.clone(),
5189 pk,
5190 cells,
5191 });
5192 } else {
5193 let row_id = self
5194 .table(table)?
5195 .lock()
5196 .lookup_pk(&pk.encode_key())
5197 .ok_or_else(|| {
5198 MongrelError::NotFound(format!(
5199 "trigger {:?} update target not found",
5200 trigger.name
5201 ))
5202 })?;
5203 let handle = self.table(table)?;
5204 let snapshot = Snapshot::at(self.epoch.visible());
5205 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
5206 MongrelError::NotFound(format!(
5207 "trigger {:?} update target not visible",
5208 trigger.name
5209 ))
5210 })?;
5211 let mut merged = old.columns;
5212 for (column_id, value) in cells {
5213 merged.insert(column_id, value);
5214 }
5215 out.added.push((
5216 self.table_id(table)?,
5217 crate::txn::Staged::Update(row_id, merged.into_iter().collect()),
5218 ));
5219 out.added_stacks.push(trigger_stack.to_vec());
5220 }
5221 }
5222 TriggerStep::DeleteByPk { table, pk } => {
5223 let pk = eval_trigger_value(pk, event, selected)?;
5224 if self.external_table(table).is_some() {
5225 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
5226 table: table.clone(),
5227 pk,
5228 });
5229 } else {
5230 let row_id = self
5231 .table(table)?
5232 .lock()
5233 .lookup_pk(&pk.encode_key())
5234 .ok_or_else(|| {
5235 MongrelError::NotFound(format!(
5236 "trigger {:?} delete target not found",
5237 trigger.name
5238 ))
5239 })?;
5240 out.added
5241 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
5242 out.added_stacks.push(trigger_stack.to_vec());
5243 }
5244 }
5245 TriggerStep::Select {
5246 id,
5247 table,
5248 conditions,
5249 } => {
5250 let schema = self.table(table)?.lock().schema().clone();
5251 let snapshot = Snapshot::at(read_epoch);
5252 let rows = self.table(table)?.lock().visible_rows(snapshot)?;
5253 let mut matched = Vec::new();
5254 for row in rows {
5255 let image = TriggerRowImage::from_row(row);
5256 let passes = conditions
5257 .iter()
5258 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
5259 .collect::<Result<Vec<_>>>()?
5260 .into_iter()
5261 .all(|b| b);
5262 if passes {
5263 matched.push(image);
5264 }
5265 }
5266 if let Some(pk) = schema.primary_key() {
5267 matched.sort_by(|a, b| {
5268 let av = a.columns.get(&pk.id).unwrap_or(&Value::Null);
5269 let bv = b.columns.get(&pk.id).unwrap_or(&Value::Null);
5270 value_order(av, bv).unwrap_or(std::cmp::Ordering::Equal)
5271 });
5272 }
5273 select_results.insert(id.clone(), matched);
5274 }
5275 TriggerStep::Foreach { id, steps } => {
5276 let rows = select_results.get(id).ok_or_else(|| {
5277 MongrelError::InvalidArgument(format!(
5278 "trigger {:?} foreach references unknown select id {id:?}",
5279 trigger.name
5280 ))
5281 })?;
5282 if rows.len() > config.max_loop_iterations as usize {
5283 return Err(MongrelError::InvalidArgument(format!(
5284 "trigger {:?} foreach exceeded max_loop_iterations ({})",
5285 trigger.name, config.max_loop_iterations
5286 )));
5287 }
5288 for row in rows.clone() {
5289 let result = self.execute_trigger_steps(
5290 trigger,
5291 steps,
5292 event,
5293 staging.as_deref_mut(),
5294 out,
5295 trigger_stack,
5296 config,
5297 select_results,
5298 depth + 1,
5299 Some(&row),
5300 read_epoch,
5301 )?;
5302 if result == TriggerProgramOutcome::Ignore {
5303 return Ok(TriggerProgramOutcome::Ignore);
5304 }
5305 }
5306 }
5307 TriggerStep::DeleteWhere { table, conditions } => {
5308 let schema = self.table(table)?.lock().schema().clone();
5309 let snapshot = Snapshot::at(read_epoch);
5310 let rows = self.table(table)?.lock().visible_rows(snapshot)?;
5311 let table_id = self.table_id(table)?;
5312 let mut to_delete = Vec::new();
5313 for row in rows {
5314 let image = TriggerRowImage::from_row(row.clone());
5315 let passes = conditions
5316 .iter()
5317 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
5318 .collect::<Result<Vec<_>>>()?
5319 .into_iter()
5320 .all(|b| b);
5321 if passes {
5322 to_delete.push((table_id, row.row_id));
5323 }
5324 }
5325 for (table_id, row_id) in to_delete {
5326 out.added
5327 .push((table_id, crate::txn::Staged::Delete(row_id)));
5328 out.added_stacks.push(trigger_stack.to_vec());
5329 }
5330 }
5331 TriggerStep::UpdateWhere {
5332 table,
5333 conditions,
5334 cells,
5335 } => {
5336 let schema = self.table(table)?.lock().schema().clone();
5337 let snapshot = Snapshot::at(read_epoch);
5338 let rows = self.table(table)?.lock().visible_rows(snapshot)?;
5339 let table_id = self.table_id(table)?;
5340 let mut to_update = Vec::new();
5341 for row in rows {
5342 let image = TriggerRowImage::from_row(row.clone());
5343 let passes = conditions
5344 .iter()
5345 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
5346 .collect::<Result<Vec<_>>>()?
5347 .into_iter()
5348 .all(|b| b);
5349 if passes {
5350 let new_cells = cells
5351 .iter()
5352 .map(|cell| {
5353 Ok((
5354 cell.column_id,
5355 eval_trigger_value(&cell.value, event, Some(&image))?,
5356 ))
5357 })
5358 .collect::<Result<Vec<_>>>()?;
5359 let mut merged = row.columns.clone();
5360 for (column_id, value) in new_cells {
5361 merged.insert(column_id, value);
5362 }
5363 to_update.push((table_id, row.row_id, merged));
5364 }
5365 }
5366 for (table_id, row_id, merged) in to_update {
5367 out.added.push((
5368 table_id,
5369 crate::txn::Staged::Update(row_id, merged.into_iter().collect()),
5370 ));
5371 out.added_stacks.push(trigger_stack.to_vec());
5372 }
5373 }
5374 TriggerStep::Raise { action, message } => match action {
5375 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
5376 TriggerRaiseAction::Abort
5377 | TriggerRaiseAction::Fail
5378 | TriggerRaiseAction::Rollback => {
5379 let message = eval_trigger_value(message, event, selected)?;
5380 return Err(MongrelError::Conflict(format!(
5381 "trigger {:?} raised: {}; trigger stack: {}",
5382 trigger.name,
5383 trigger_message(message),
5384 Self::format_trigger_stack(trigger_stack)
5385 )));
5386 }
5387 },
5388 }
5389 }
5390 Ok(TriggerProgramOutcome::Continue)
5391 }
5392
5393 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
5394 let Some(stacks) = stacks else {
5395 return Vec::new();
5396 };
5397 let mut out = Vec::new();
5398 for idx in indices {
5399 let Some(stack) = stacks.get(*idx) else {
5400 continue;
5401 };
5402 for name in stack {
5403 if !out.iter().any(|existing| existing == name) {
5404 out.push(name.clone());
5405 }
5406 }
5407 }
5408 out
5409 }
5410
5411 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
5412 let mut out = stack.to_vec();
5413 out.push(trigger_name.to_string());
5414 out
5415 }
5416
5417 fn format_trigger_stack(stack: &[String]) -> String {
5418 if stack.is_empty() {
5419 "<root>".into()
5420 } else {
5421 stack.join(" -> ")
5422 }
5423 }
5424
5425 fn validate_constraints(
5440 &self,
5441 staging: &mut Vec<(u64, crate::txn::Staged)>,
5442 read_epoch: Epoch,
5443 ) -> Result<()> {
5444 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
5445 use crate::memtable::Row;
5446 use crate::txn::Staged;
5447 use std::collections::HashSet;
5448
5449 let snapshot = Snapshot::at(read_epoch);
5450 let cat = self.catalog.read();
5451
5452 let live: Vec<(u64, &str, &crate::schema::Schema)> = cat
5454 .tables
5455 .iter()
5456 .filter(|e| matches!(e.state, TableState::Live))
5457 .map(|e| (e.table_id, e.name.as_str(), &e.schema))
5458 .collect();
5459
5460 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
5462 if !any_constraints {
5463 return Ok(());
5464 }
5465
5466 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
5468 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
5469 if let Some(r) = rows_cache.get(&table_id) {
5470 return Ok(r.clone());
5471 }
5472 let handle = self.table_by_id(table_id)?;
5473 let rows = handle.lock().visible_rows(snapshot)?;
5474 rows_cache.insert(table_id, rows.clone());
5475 Ok(rows)
5476 };
5477
5478 let mut processed_updates = HashSet::new();
5483 type PendingUpdate = (usize, u64, crate::rowid::RowId, Vec<(u16, Value)>);
5484 loop {
5485 let updates: Vec<PendingUpdate> = staging
5486 .iter()
5487 .enumerate()
5488 .filter_map(|(index, (table_id, op))| match op {
5489 Staged::Update(row_id, cells) if !processed_updates.contains(&index) => {
5490 Some((index, *table_id, *row_id, cells.clone()))
5491 }
5492 _ => None,
5493 })
5494 .collect();
5495 if updates.is_empty() {
5496 break;
5497 }
5498 let mut new_ops = Vec::new();
5499 for (index, table_id, row_id, new_cells) in updates {
5500 processed_updates.insert(index);
5501 let Some(tname) = live
5502 .iter()
5503 .find(|(id, _, _)| *id == table_id)
5504 .map(|(_, name, _)| *name)
5505 else {
5506 continue;
5507 };
5508 let Some(old_row) = self.table_by_id(table_id)?.lock().get(row_id, snapshot) else {
5509 continue;
5510 };
5511 let new_map: HashMap<u16, Value> = new_cells.iter().cloned().collect();
5512 for (child_id, _child_name, child_schema) in &live {
5513 for fk in &child_schema.constraints.foreign_keys {
5514 if fk.ref_table != tname {
5515 continue;
5516 }
5517 let Some(old_key) = encode_composite_key(&fk.ref_columns, &old_row.columns)
5518 else {
5519 continue;
5520 };
5521 if encode_composite_key(&fk.ref_columns, &new_map).as_deref()
5522 == Some(old_key.as_slice())
5523 {
5524 continue;
5525 }
5526 if fk.on_update == FkAction::Restrict {
5527 continue;
5528 }
5529 let child_rows = load_rows(*child_id)?;
5530 for child in child_rows {
5531 if encode_composite_key(&fk.columns, &child.columns).as_deref()
5532 != Some(old_key.as_slice())
5533 {
5534 continue;
5535 }
5536 if staging.iter().any(|(id, op)| {
5537 *id == *child_id
5538 && matches!(op, Staged::Delete(id) if *id == child.row_id)
5539 }) {
5540 continue;
5541 }
5542 let mut cells: Vec<(u16, Value)> = child
5543 .columns
5544 .iter()
5545 .map(|(column_id, value)| (*column_id, value.clone()))
5546 .collect();
5547 for (child_column, parent_column) in
5548 fk.columns.iter().zip(&fk.ref_columns)
5549 {
5550 cells.retain(|(column_id, _)| column_id != child_column);
5551 let value = match fk.on_update {
5552 FkAction::Cascade => {
5553 new_map.get(parent_column).cloned().unwrap_or(Value::Null)
5554 }
5555 FkAction::SetNull => Value::Null,
5556 FkAction::Restrict => unreachable!(),
5557 };
5558 cells.push((*child_column, value));
5559 }
5560 cells.sort_by_key(|(column_id, _)| *column_id);
5561 if let Some(existing_index) = staging.iter().position(|(id, op)| {
5562 *id == *child_id
5563 && matches!(op, Staged::Update(id, _) if *id == child.row_id)
5564 }) {
5565 if let Staged::Update(_, existing) = &mut staging[existing_index].1
5566 {
5567 if *existing != cells {
5568 *existing = cells;
5569 processed_updates.remove(&existing_index);
5570 }
5571 }
5572 } else {
5573 new_ops.push((*child_id, Staged::Update(child.row_id, cells)));
5574 }
5575 }
5576 }
5577 }
5578 }
5579 staging.extend(new_ops);
5580 }
5581
5582 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
5587 loop {
5588 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
5589 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
5590 .iter()
5591 .filter_map(|(t, op)| match op {
5592 Staged::Delete(rid) => Some((*t, *rid)),
5593 _ => None,
5594 })
5595 .collect();
5596 for (table_id, rid) in deletes {
5597 if !cascaded.insert((table_id, rid.0)) {
5598 continue;
5599 }
5600 let Some(tname) = live
5601 .iter()
5602 .find(|(t, _, _)| *t == table_id)
5603 .map(|(_, n, _)| *n)
5604 else {
5605 continue;
5606 };
5607 let parent_handle = self.table_by_id(table_id)?;
5608 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
5609 continue;
5610 };
5611 for (child_id, _child_name, child_schema) in &live {
5612 for fk in &child_schema.constraints.foreign_keys {
5613 if fk.ref_table != tname {
5614 continue;
5615 }
5616 let Some(parent_key) =
5617 encode_composite_key(&fk.ref_columns, &parent_row.columns)
5618 else {
5619 continue;
5620 };
5621 let key_preserved = staging.iter().any(|(t, op)| {
5630 if *t != table_id {
5631 return false;
5632 }
5633 let Staged::Put(cells) = op else {
5634 return false;
5635 };
5636 let map: HashMap<u16, crate::memtable::Value> =
5637 cells.iter().cloned().collect();
5638 encode_composite_key(&fk.ref_columns, &map).as_deref()
5639 == Some(parent_key.as_slice())
5640 });
5641 if key_preserved {
5642 continue;
5643 }
5644 match fk.on_delete {
5645 FkAction::Restrict => continue,
5646 FkAction::Cascade => {
5647 let child_rows = load_rows(*child_id)?;
5648 for cr in &child_rows {
5649 if !cascaded.contains(&(*child_id, cr.row_id.0))
5650 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
5651 == Some(parent_key.as_slice())
5652 {
5653 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
5654 }
5655 }
5656 }
5657 FkAction::SetNull => {
5658 let child_rows = load_rows(*child_id)?;
5659 for cr in &child_rows {
5660 if !cascaded.contains(&(*child_id, cr.row_id.0))
5661 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
5662 == Some(parent_key.as_slice())
5663 {
5664 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
5667 .columns
5668 .iter()
5669 .map(|(k, v)| (*k, v.clone()))
5670 .collect();
5671 for cid in &fk.columns {
5672 cells.retain(|(k, _)| k != cid);
5673 cells.push((*cid, crate::memtable::Value::Null));
5674 }
5675 new_ops.push((*child_id, Staged::Update(cr.row_id, cells)));
5676 }
5677 }
5678 }
5679 }
5680 }
5681 }
5682 }
5683 if new_ops.is_empty() {
5684 break;
5685 }
5686 staging.extend(new_ops);
5687 }
5688
5689 let staged_deletes: HashSet<(u64, u64)> = staging
5693 .iter()
5694 .filter_map(|(t, op)| match op {
5695 Staged::Delete(rid) | Staged::Update(rid, _) => Some((*t, rid.0)),
5696 _ => None,
5697 })
5698 .collect();
5699
5700 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
5702
5703 for (table_id, op) in staging.iter() {
5705 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id).copied()
5706 else {
5707 continue;
5708 };
5709 let cells_map: HashMap<u16, crate::memtable::Value>;
5710 match op {
5711 Staged::Put(cells) | Staged::Update(_, cells) => {
5712 cells_map = cells.iter().cloned().collect();
5713
5714 if !schema.constraints.checks.is_empty() {
5716 validate_checks(&schema.constraints.checks, &cells_map)?;
5717 }
5718
5719 for uc in &schema.constraints.uniques {
5721 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
5722 continue; };
5724 let marker = (*table_id, uc.id, key.clone());
5725 if !seen_unique.insert(marker) {
5726 return Err(MongrelError::Conflict(format!(
5727 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
5728 uc.name
5729 )));
5730 }
5731 let rows = load_rows(*table_id)?;
5732 for r in &rows {
5733 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
5736 continue;
5737 }
5738 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
5739 if theirs == key {
5740 return Err(MongrelError::Conflict(format!(
5741 "UNIQUE constraint '{}' on table '{tname}' violated",
5742 uc.name
5743 )));
5744 }
5745 }
5746 }
5747 }
5748
5749 for fk in &schema.constraints.foreign_keys {
5751 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
5752 continue; };
5754 let Some(parent_id) = cat
5755 .tables
5756 .iter()
5757 .find(|t| t.name == fk.ref_table)
5758 .map(|t| t.table_id)
5759 else {
5760 return Err(MongrelError::InvalidArgument(format!(
5761 "FOREIGN KEY '{}' references unknown table '{}'",
5762 fk.name, fk.ref_table
5763 )));
5764 };
5765 let parent_rows = load_rows(parent_id)?;
5766 let mut found = false;
5767 for r in &parent_rows {
5768 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
5769 continue;
5770 }
5771 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
5772 if pkey == child_key {
5773 found = true;
5774 break;
5775 }
5776 }
5777 }
5778 if !found {
5785 for (st_table, st_op) in staging.iter() {
5786 if *st_table != parent_id {
5787 continue;
5788 }
5789 if let Staged::Put(pcells) | Staged::Update(_, pcells) = st_op {
5790 let pmap: HashMap<u16, crate::memtable::Value> =
5791 pcells.iter().cloned().collect();
5792 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &pmap)
5793 {
5794 if pkey == child_key {
5795 found = true;
5796 break;
5797 }
5798 }
5799 }
5800 }
5801 }
5802 if !found {
5803 return Err(MongrelError::Conflict(format!(
5804 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
5805 fk.name, fk.ref_table
5806 )));
5807 }
5808 }
5809
5810 if let Staged::Update(row_id, _) = op {
5815 let parent_handle = self.table_by_id(*table_id)?;
5816 let Some(old_parent) = parent_handle.lock().get(*row_id, snapshot) else {
5817 continue;
5818 };
5819 for (child_id, child_name, child_schema) in &live {
5820 for fk in &child_schema.constraints.foreign_keys {
5821 if fk.ref_table != tname || fk.on_update != FkAction::Restrict {
5822 continue;
5823 }
5824 let Some(old_key) =
5825 encode_composite_key(&fk.ref_columns, &old_parent.columns)
5826 else {
5827 continue;
5828 };
5829 if encode_composite_key(&fk.ref_columns, &cells_map).as_deref()
5830 == Some(old_key.as_slice())
5831 {
5832 continue;
5833 }
5834 for child in load_rows(*child_id)? {
5835 if encode_composite_key(&fk.columns, &child.columns).as_deref()
5836 != Some(old_key.as_slice())
5837 {
5838 continue;
5839 }
5840 let replacement = staging.iter().find_map(|(id, op)| {
5841 if *id != *child_id {
5842 return None;
5843 }
5844 match op {
5845 Staged::Delete(id) if *id == child.row_id => Some(None),
5846 Staged::Update(id, cells) if *id == child.row_id => {
5847 let map: HashMap<u16, Value> =
5848 cells.iter().cloned().collect();
5849 Some(encode_composite_key(&fk.columns, &map))
5850 }
5851 _ => None,
5852 }
5853 });
5854 if replacement.is_some_and(|key| {
5855 key.as_deref() != Some(old_key.as_slice())
5856 }) {
5857 continue;
5858 }
5859 return Err(MongrelError::Conflict(format!(
5860 "FOREIGN KEY '{}' on table '{child_name}' restricts update (parent key referenced)",
5861 fk.name
5862 )));
5863 }
5864 }
5865 }
5866 }
5867 }
5868 Staged::Delete(rid) => {
5869 let parent_handle = self.table_by_id(*table_id)?;
5873 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
5874 continue;
5875 };
5876 for (child_id, child_name, child_schema) in &live {
5877 for fk in &child_schema.constraints.foreign_keys {
5878 if fk.ref_table != tname || fk.on_delete != FkAction::Restrict {
5879 continue;
5880 }
5881 let Some(parent_key) =
5882 encode_composite_key(&fk.ref_columns, &parent_row.columns)
5883 else {
5884 continue;
5885 };
5886 let child_rows = load_rows(*child_id)?;
5887 for r in &child_rows {
5888 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
5891 continue;
5892 }
5893 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
5894 if ck == parent_key {
5895 return Err(MongrelError::Conflict(format!(
5896 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
5897 fk.name
5898 )));
5899 }
5900 }
5901 }
5902 }
5903 }
5904 }
5905 Staged::Truncate => {
5906 for (child_id, child_name, child_schema) in &live {
5910 for fk in &child_schema.constraints.foreign_keys {
5911 if fk.ref_table != tname {
5912 continue;
5913 }
5914 let child_rows = load_rows(*child_id)?;
5915 if child_rows
5916 .iter()
5917 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
5918 {
5919 return Err(MongrelError::Conflict(format!(
5920 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
5921 fk.name
5922 )));
5923 }
5924 }
5925 }
5926 }
5927 }
5928 }
5929 Ok(())
5930 }
5931
5932 fn validate_write_permissions(
5933 &self,
5934 staging: &[(u64, crate::txn::Staged)],
5935 principal: Option<&crate::auth::Principal>,
5936 ) -> Result<()> {
5937 use crate::txn::Staged;
5938
5939 let table_names = self
5940 .catalog
5941 .read()
5942 .tables
5943 .iter()
5944 .filter(|entry| matches!(entry.state, TableState::Live))
5945 .map(|entry| (entry.table_id, entry.name.clone()))
5946 .collect::<HashMap<_, _>>();
5947 for (table_id, operation) in staging {
5948 let Some(table) = table_names.get(table_id) else {
5949 continue;
5950 };
5951 match operation {
5952 Staged::Put(cells) => self.require_columns_for(
5953 table,
5954 crate::auth::ColumnOperation::Insert,
5955 &cells.iter().map(|(column, _)| *column).collect::<Vec<_>>(),
5956 principal,
5957 )?,
5958 Staged::Update(_, cells) => self.require_columns_for(
5959 table,
5960 crate::auth::ColumnOperation::Update,
5961 &cells.iter().map(|(column, _)| *column).collect::<Vec<_>>(),
5962 principal,
5963 )?,
5964 Staged::Delete(_) => self.require_for(
5965 principal,
5966 &crate::auth::Permission::Delete {
5967 table: table.clone(),
5968 },
5969 )?,
5970 Staged::Truncate => self.require_for(principal, &crate::auth::Permission::Admin)?,
5971 }
5972 }
5973 Ok(())
5974 }
5975
5976 fn validate_security_writes(
5977 &self,
5978 staging: &[(u64, crate::txn::Staged)],
5979 read_epoch: Epoch,
5980 explicit_principal: Option<&crate::auth::Principal>,
5981 ) -> Result<()> {
5982 use crate::security::PolicyCommand;
5983 use crate::txn::Staged;
5984
5985 let catalog = self.catalog.read();
5986 if catalog.security.rls_tables.is_empty() {
5987 return Ok(());
5988 }
5989 let security = catalog.security.clone();
5990 let table_names = catalog
5991 .tables
5992 .iter()
5993 .filter(|entry| matches!(entry.state, TableState::Live))
5994 .map(|entry| (entry.table_id, entry.name.clone()))
5995 .collect::<HashMap<_, _>>();
5996 drop(catalog);
5997 if !staging.iter().any(|(table_id, _)| {
5998 table_names
5999 .get(table_id)
6000 .is_some_and(|table| security.rls_enabled(table))
6001 }) {
6002 return Ok(());
6003 }
6004 let cached = self.principal.read().clone();
6005 let principal = explicit_principal
6006 .or(cached.as_ref())
6007 .ok_or(MongrelError::AuthRequired)?;
6008
6009 for (table_id, operation) in staging {
6010 let Some(table) = table_names.get(table_id) else {
6011 continue;
6012 };
6013 if !security.rls_enabled(table) || principal.is_admin {
6014 continue;
6015 }
6016 let denied = |command| MongrelError::PermissionDenied {
6017 required: match command {
6018 PolicyCommand::Insert => crate::auth::Permission::Insert {
6019 table: table.clone(),
6020 },
6021 PolicyCommand::Update => crate::auth::Permission::Update {
6022 table: table.clone(),
6023 },
6024 PolicyCommand::Delete | PolicyCommand::All | PolicyCommand::Select => {
6025 crate::auth::Permission::Delete {
6026 table: table.clone(),
6027 }
6028 }
6029 },
6030 principal: principal.username.clone(),
6031 };
6032 match operation {
6033 Staged::Put(cells) => {
6034 let mut row = crate::memtable::Row::new(RowId(0), Epoch(read_epoch.0));
6035 row.columns.extend(cells.iter().cloned());
6036 if !security.row_allowed(table, PolicyCommand::Insert, &row, principal, true) {
6037 return Err(denied(PolicyCommand::Insert));
6038 }
6039 }
6040 Staged::Update(row_id, cells) => {
6041 let old = self
6042 .table_by_id(*table_id)?
6043 .lock()
6044 .get(*row_id, Snapshot::at(read_epoch))
6045 .ok_or_else(|| {
6046 MongrelError::NotFound(format!("row {} not found", row_id.0))
6047 })?;
6048 if !security.row_allowed(table, PolicyCommand::Update, &old, principal, false) {
6049 return Err(denied(PolicyCommand::Update));
6050 }
6051 let mut new = crate::memtable::Row::new(*row_id, Epoch(read_epoch.0));
6052 new.columns.extend(cells.iter().cloned());
6053 if !security.row_allowed(table, PolicyCommand::Update, &new, principal, true) {
6054 return Err(denied(PolicyCommand::Update));
6055 }
6056 }
6057 Staged::Delete(row_id) => {
6058 let old = self
6059 .table_by_id(*table_id)?
6060 .lock()
6061 .get(*row_id, Snapshot::at(read_epoch))
6062 .ok_or_else(|| {
6063 MongrelError::NotFound(format!("row {} not found", row_id.0))
6064 })?;
6065 if !security.row_allowed(table, PolicyCommand::Delete, &old, principal, false) {
6066 return Err(denied(PolicyCommand::Delete));
6067 }
6068 }
6069 Staged::Truncate => return Err(denied(PolicyCommand::Delete)),
6070 }
6071 }
6072 Ok(())
6073 }
6074
6075 #[allow(clippy::too_many_arguments)]
6082 pub(crate) fn commit_transaction_with_external_states(
6083 &self,
6084 txn_id: u64,
6085 read_epoch: Epoch,
6086 mut staging: Vec<(u64, crate::txn::Staged)>,
6087 external_states: Vec<(String, Vec<u8>)>,
6088 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
6089 mut security_principal: Option<crate::auth::Principal>,
6090 principal_catalog_bound: bool,
6091 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
6092 ) -> Result<(Epoch, Vec<RowId>)> {
6093 use crate::memtable::Row;
6094 use crate::txn::{Staged, StagedOp, WriteKey};
6095 use crate::wal::Op;
6096 use std::collections::hash_map::DefaultHasher;
6097 use std::hash::{Hash, Hasher};
6098 use std::sync::atomic::Ordering;
6099
6100 if self.read_only {
6101 return Err(MongrelError::ReadOnlyReplica);
6102 }
6103 if principal_catalog_bound {
6104 let fresh = catalog::read(&self.root, self.meta_dek.as_ref())?
6105 .ok_or_else(|| MongrelError::NotFound("catalog vanished during write".into()))?;
6106 *self.catalog.write() = fresh;
6107 }
6108 let security_version = {
6109 let catalog = self.catalog.read();
6110 if principal_catalog_bound {
6111 let username = security_principal
6112 .as_ref()
6113 .map(|principal| principal.username.as_str())
6114 .ok_or(MongrelError::AuthRequired)?;
6115 security_principal = Self::resolve_principal_from_catalog(&catalog, username);
6116 if security_principal.is_none() {
6117 return Err(MongrelError::AuthRequired);
6118 }
6119 }
6120 catalog.security_version
6121 };
6122 let _replication_guard = self.replication_barrier.read();
6123 if self.poisoned.load(Ordering::Relaxed) {
6124 return Err(MongrelError::Other(
6125 "database poisoned by fsync error".into(),
6126 ));
6127 }
6128 let mut external_states = dedup_external_states(external_states);
6129 if !external_states.is_empty() {
6130 let cat = self.catalog.read();
6131 for (name, _) in &external_states {
6132 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
6133 return Err(MongrelError::NotFound(format!(
6134 "external table {name:?} not found"
6135 )));
6136 }
6137 }
6138 }
6139 let prepared_materialized_views = {
6140 let mut deduplicated = HashMap::new();
6141 for definition in materialized_view_updates {
6142 if definition.name.is_empty() || definition.query.trim().is_empty() {
6143 return Err(MongrelError::InvalidArgument(
6144 "materialized view name and query must not be empty".into(),
6145 ));
6146 }
6147 deduplicated.insert(definition.name.clone(), definition);
6148 }
6149 let catalog = self.catalog.read();
6150 let mut prepared = Vec::with_capacity(deduplicated.len());
6151 for definition in deduplicated.into_values() {
6152 let table_id = catalog
6153 .live(&definition.name)
6154 .ok_or_else(|| {
6155 MongrelError::NotFound(format!(
6156 "materialized view table {:?} not found",
6157 definition.name
6158 ))
6159 })?
6160 .table_id;
6161 prepared.push((table_id, definition));
6162 }
6163 prepared.sort_by(|left, right| left.1.name.cmp(&right.1.name));
6164 prepared
6165 };
6166
6167 self.fill_auto_increment_for_staging(&mut staging)?;
6170 self.expand_table_triggers(
6171 &mut staging,
6172 read_epoch,
6173 external_trigger_bridge,
6174 &mut external_states,
6175 )?;
6176 self.fill_auto_increment_for_staging(&mut staging)?;
6177 external_states = dedup_external_states(external_states);
6178
6179 self.validate_write_permissions(&staging, security_principal.as_ref())?;
6180
6181 self.validate_constraints(&mut staging, read_epoch)?;
6186 self.validate_security_writes(&staging, read_epoch, security_principal.as_ref())?;
6187 let mut normalized = Vec::with_capacity(staging.len() * 2);
6188 for (table_id, op) in staging {
6189 match op {
6190 crate::txn::Staged::Update(row_id, cells) => {
6191 normalized.push((table_id, crate::txn::Staged::Delete(row_id)));
6192 normalized.push((table_id, crate::txn::Staged::Put(cells)));
6193 }
6194 op => normalized.push((table_id, op)),
6195 }
6196 }
6197 staging = normalized;
6198 let has_changes = !staging.is_empty()
6199 || !external_states.is_empty()
6200 || !prepared_materialized_views.is_empty();
6201 let truncated_tables: HashSet<u64> = staging
6202 .iter()
6203 .filter_map(|(table_id, op)| matches!(op, Staged::Truncate).then_some(*table_id))
6204 .collect();
6205
6206 let write_keys = {
6207 let cat = self.catalog.read();
6208 let mut keys: Vec<WriteKey> = Vec::new();
6209 for (table_id, staged) in &staging {
6210 match staged {
6211 Staged::Put(cells) => {
6212 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
6213 for col in &entry.schema.columns {
6214 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
6215 if let Some((_, val)) =
6216 cells.iter().find(|(id, _)| *id == col.id)
6217 {
6218 let mut h = DefaultHasher::new();
6219 val.encode_key().hash(&mut h);
6220 keys.push(WriteKey::Unique {
6221 table_id: *table_id,
6222 index_id: 0,
6223 key_hash: h.finish(),
6224 });
6225 }
6226 }
6227 }
6228 for uc in &entry.schema.constraints.uniques {
6235 if let Some(key_bytes) = crate::constraint::encode_composite_key(
6236 &uc.columns,
6237 &cells.iter().cloned().collect(),
6238 ) {
6239 let mut h = DefaultHasher::new();
6240 key_bytes.hash(&mut h);
6241 keys.push(WriteKey::Unique {
6242 table_id: *table_id,
6243 index_id: uc.id | 0x8000,
6244 key_hash: h.finish(),
6245 });
6246 }
6247 }
6248 }
6249 }
6250 Staged::Delete(rid) => keys.push(WriteKey::Row {
6251 table_id: *table_id,
6252 row_id: rid.0,
6253 }),
6254 Staged::Truncate => keys.push(WriteKey::Table {
6255 table_id: *table_id,
6256 }),
6257 Staged::Update(_, _) => unreachable!("updates normalized before prepare"),
6258 }
6259 }
6260 for (name, _) in &external_states {
6261 let mut h = DefaultHasher::new();
6262 name.hash(&mut h);
6263 keys.push(WriteKey::Unique {
6264 table_id: EXTERNAL_TABLE_ID,
6265 index_id: 0,
6266 key_hash: h.finish(),
6267 });
6268 }
6269 keys
6270 };
6271
6272 let min_active = self.active_txns.min_read_epoch();
6274 if min_active < u64::MAX {
6275 self.conflicts.prune_below(Epoch(min_active));
6276 }
6277
6278 if self.conflicts.conflicts(&write_keys, read_epoch) {
6282 return Err(MongrelError::Conflict(
6283 "write-write conflict (pre-validate, first-committer-wins)".into(),
6284 ));
6285 }
6286 let pre_validate_version = self.conflicts.version();
6287
6288 let mut spilled: Vec<SpilledRun> = Vec::new();
6292 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
6293 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
6297 {
6298 let mut table_bytes: HashMap<u64, usize> = HashMap::new();
6299 for (table_id, staged) in &staging {
6300 if let Staged::Put(cells) = staged {
6301 *table_bytes.entry(*table_id).or_default() += cells.len() * 16;
6302 }
6303 }
6304 let tables = self.tables.read();
6305 for (&table_id, &bytes) in &table_bytes {
6306 if bytes as u64
6307 <= self
6308 .spill_threshold
6309 .load(std::sync::atomic::Ordering::Relaxed)
6310 {
6311 continue;
6312 }
6313 let Some(handle) = tables.get(&table_id) else {
6314 continue;
6315 };
6316 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
6317 let mut t = handle.lock();
6318 let tdir = t.table_dir().to_path_buf();
6319 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
6320 std::fs::create_dir_all(&txn_dir)?;
6321 let run_id = t.alloc_run_id() as u128;
6322 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
6323
6324 let mut rows: Vec<Row> = Vec::new();
6325 for (tid, staged) in &staging {
6326 if *tid != table_id {
6327 continue;
6328 }
6329 if let Staged::Put(cells) = staged {
6330 t.validate_cells_not_null(cells)?;
6331 let row_id = t.alloc_row_id();
6332 let mut row = Row::new(row_id, Epoch(0));
6333 for (c, v) in cells {
6334 row.columns.insert(*c, v.clone());
6335 }
6336 rows.push(row);
6337 }
6338 }
6339 let schema = t.schema_ref().clone();
6340 let kek = t.kek_ref().cloned();
6341 let specs = t.indexable_column_specs();
6342 drop(t);
6343
6344 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
6345 .uniform_epoch(true);
6346 if let Some(ref kek) = kek {
6347 writer = writer.with_encryption(kek.as_ref(), specs);
6348 }
6349 let header = writer.write(&pending_path, &rows)?;
6350 let row_count = header.row_count;
6351 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
6352 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
6353
6354 spilled.push(SpilledRun {
6355 table_id,
6356 run_id,
6357 pending_path,
6358 rows,
6359 row_count,
6360 min_rid,
6361 max_rid,
6362 });
6363 spilled_tables.insert(table_id);
6364 }
6365 }
6366
6367 if spill_guard.is_some() {
6369 if let Some(hook) = self.spill_hook.lock().as_ref() {
6370 hook();
6371 }
6372 }
6373
6374 let mut prebuilt: Vec<Option<Row>> = Vec::with_capacity(staging.len());
6385 let mut delete_images: Vec<Option<Row>> = Vec::with_capacity(staging.len());
6386 {
6387 let tables = self.tables.read();
6388 for (table_id, staged) in &staging {
6389 match staged {
6390 Staged::Put(cells) if !spilled_tables.contains(table_id) => {
6391 let handle = tables.get(table_id).ok_or_else(|| {
6392 MongrelError::NotFound(format!("table {table_id} not mounted"))
6393 })?;
6394 let mut t = handle.lock();
6395 t.validate_cells_not_null(cells)?;
6396 let row_id = t.alloc_row_id();
6397 drop(t);
6398 let mut row = Row::new(row_id, Epoch(0));
6399 for (c, v) in cells {
6400 row.columns.insert(*c, v.clone());
6401 }
6402 prebuilt.push(Some(row));
6403 delete_images.push(None);
6404 }
6405 Staged::Delete(row_id) => {
6406 let before = tables.get(table_id).and_then(|handle| {
6407 handle.lock().get(*row_id, Snapshot::at(read_epoch))
6408 });
6409 prebuilt.push(None);
6410 delete_images.push(before);
6411 }
6412 Staged::Put(_) | Staged::Truncate => {
6413 prebuilt.push(None);
6414 delete_images.push(None);
6415 }
6416 Staged::Update(_, _) => unreachable!("updates normalized before prepare"),
6417 }
6418 }
6419 }
6420
6421 let mut spilled_row_ids: HashMap<u64, VecDeque<RowId>> = spilled
6422 .iter()
6423 .map(|run| {
6424 (
6425 run.table_id,
6426 run.rows.iter().map(|row| row.row_id).collect(),
6427 )
6428 })
6429 .collect();
6430 let committed_row_ids = staging
6431 .iter()
6432 .enumerate()
6433 .filter_map(|(index, (table_id, staged))| {
6434 if !matches!(staged, Staged::Put(_)) {
6435 return None;
6436 }
6437 prebuilt[index].as_ref().map(|row| row.row_id).or_else(|| {
6438 spilled_row_ids
6439 .get_mut(table_id)
6440 .and_then(VecDeque::pop_front)
6441 })
6442 })
6443 .collect();
6444
6445 let mut prepared_external = Vec::with_capacity(external_states.len());
6446 for (name, state) in &external_states {
6447 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
6448 prepared_external.push((name.clone(), state.clone(), pending));
6449 }
6450
6451 let added_runs: Vec<crate::wal::AddedRun> = spilled
6453 .iter()
6454 .map(|s| crate::wal::AddedRun {
6455 table_id: s.table_id,
6456 run_id: s.run_id,
6457 row_count: s.row_count,
6458 level: 0,
6459 min_row_id: s.min_rid,
6460 max_row_id: s.max_rid,
6461 content_hash: [0u8; 32],
6462 })
6463 .collect();
6464 let (new_epoch, mut _epoch_guard, applies, committed_materialized_views, commit_seq) = {
6465 let mut wal = self.shared_wal.lock();
6466
6467 let current_security_version = catalog::read(&self.root, self.meta_dek.as_ref())?
6468 .ok_or_else(|| MongrelError::NotFound("catalog vanished during write".into()))?
6469 .security_version;
6470 if current_security_version != security_version {
6471 return Err(MongrelError::Conflict(
6472 "security policy changed during write".into(),
6473 ));
6474 }
6475
6476 if self.conflicts.version() != pre_validate_version
6481 && self.conflicts.conflicts(&write_keys, read_epoch)
6482 {
6483 drop(wal);
6487 for s in &spilled {
6488 if let Some(parent) = s.pending_path.parent() {
6489 let _ = std::fs::remove_dir_all(parent);
6490 }
6491 }
6492 for (_, _, pending) in &prepared_external {
6493 let _ = std::fs::remove_file(pending);
6494 }
6495 return Err(MongrelError::Conflict(
6496 "write-write conflict (sequencer delta re-check)".into(),
6497 ));
6498 }
6499
6500 let new_epoch = self.epoch.bump_assigned();
6501 let _epoch_guard = EpochGuard::new(self.epoch.as_ref(), new_epoch);
6502 let mut applies: Vec<(u64, Vec<StagedOp>)> = Vec::new();
6503 let mut committed_materialized_views = Vec::new();
6504
6505 for (idx, (table_id, staged)) in staging.iter().enumerate() {
6506 if spilled_tables.contains(table_id) && matches!(staged, Staged::Put(_)) {
6509 continue;
6510 }
6511 let mut ops = Vec::new();
6512 match staged {
6513 Staged::Put(_) => {
6514 let mut row = prebuilt[idx].take().expect("prebuilt put row");
6516 row.committed_epoch = new_epoch;
6517 let payload = bincode::serialize(&vec![row.clone()])
6518 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
6519 wal.append(
6520 txn_id,
6521 *table_id,
6522 Op::Put {
6523 table_id: *table_id,
6524 rows: payload,
6525 },
6526 )?;
6527 ops.push(StagedOp::Put(row));
6528 }
6529 Staged::Delete(rid) => {
6530 if let Some(before) = &delete_images[idx] {
6531 wal.append(
6532 txn_id,
6533 *table_id,
6534 Op::BeforeImage {
6535 table_id: *table_id,
6536 row_id: *rid,
6537 row: bincode::serialize(before).map_err(|error| {
6538 MongrelError::Other(format!(
6539 "before-image serialize: {error}"
6540 ))
6541 })?,
6542 },
6543 )?;
6544 }
6545 wal.append(
6546 txn_id,
6547 *table_id,
6548 Op::Delete {
6549 table_id: *table_id,
6550 row_ids: vec![*rid],
6551 },
6552 )?;
6553 ops.push(StagedOp::Delete(*rid));
6554 }
6555 Staged::Truncate => {
6556 wal.append(
6557 txn_id,
6558 *table_id,
6559 Op::TruncateTable {
6560 table_id: *table_id,
6561 },
6562 )?;
6563 ops.push(StagedOp::Truncate);
6564 }
6565 Staged::Update(_, _) => unreachable!("updates normalized before sequencer"),
6566 }
6567 applies.push((*table_id, ops));
6568 }
6569
6570 for (name, state, _) in &prepared_external {
6571 wal.append(
6572 txn_id,
6573 EXTERNAL_TABLE_ID,
6574 Op::ExternalTableState {
6575 name: name.clone(),
6576 state: state.clone(),
6577 },
6578 )?;
6579 }
6580
6581 for (table_id, definition) in &prepared_materialized_views {
6582 let mut definition = definition.clone();
6583 definition.last_refresh_epoch = new_epoch.0;
6584 wal.append(
6585 txn_id,
6586 *table_id,
6587 Op::Ddl(crate::wal::DdlOp::SetMaterializedView {
6588 name: definition.name.clone(),
6589 definition_json: crate::wal::DdlOp::encode_materialized_view(&definition)?,
6590 }),
6591 )?;
6592 committed_materialized_views.push(definition);
6593 }
6594
6595 let commit_seq = wal.append_commit(txn_id, new_epoch, &added_runs)?;
6596
6597 self.conflicts.record(&write_keys, new_epoch);
6602 (
6603 new_epoch,
6604 _epoch_guard,
6605 applies,
6606 committed_materialized_views,
6607 commit_seq,
6608 )
6609 };
6610
6611 self.group
6613 .await_durable(&self.shared_wal, commit_seq)
6614 .inspect_err(|_| {
6615 self.poisoned.store(true, Ordering::Relaxed);
6616 })?;
6617
6618 {
6620 let tables = self.tables.read();
6621 for (table_id, ops) in applies {
6625 if let Some(handle) = tables.get(&table_id) {
6626 let mut t = handle.lock();
6627 for op in ops {
6628 match op {
6629 StagedOp::Put(row) => t.apply_put_rows(vec![row])?,
6630 StagedOp::Delete(rid) => t.apply_delete(rid, new_epoch),
6631 StagedOp::Truncate => t.apply_truncate(new_epoch)?,
6632 }
6633 }
6634 t.invalidate_pending_cache();
6635 t.persist_manifest(new_epoch)?;
6636 }
6637 }
6638 for s in &spilled {
6639 if let Some(handle) = tables.get(&s.table_id) {
6640 let mut t = handle.lock();
6641 let dest = t.run_path(s.run_id as u64);
6642 std::fs::rename(&s.pending_path, &dest)?;
6643 if let Some(parent) = s.pending_path.parent() {
6644 let _ = std::fs::remove_dir_all(parent);
6645 }
6646 t.link_run(crate::manifest::RunRef {
6647 run_id: s.run_id,
6648 level: 0,
6649 epoch_created: new_epoch.0,
6650 row_count: s.row_count,
6651 });
6652 t.apply_run_metadata(&s.rows)?;
6653 if truncated_tables.contains(&s.table_id) {
6654 t.set_flushed_epoch(new_epoch);
6659 }
6660 t.invalidate_pending_cache();
6661 t.persist_manifest(new_epoch)?;
6662 }
6663 }
6664 }
6665 for (name, _, pending) in &prepared_external {
6666 publish_external_state_file(&self.root, name, pending)?;
6667 }
6668 if !committed_materialized_views.is_empty() {
6669 {
6670 let mut catalog = self.catalog.write();
6671 for definition in committed_materialized_views {
6672 if let Some(existing) = catalog
6673 .materialized_views
6674 .iter_mut()
6675 .find(|existing| existing.name == definition.name)
6676 {
6677 *existing = definition;
6678 } else {
6679 catalog.materialized_views.push(definition);
6680 }
6681 }
6682 catalog.db_epoch = catalog.db_epoch.max(new_epoch.0);
6683 }
6684 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
6685 }
6686
6687 self.epoch.publish_in_order(new_epoch);
6688 if has_changes {
6689 let _ = self.change_wake.send(());
6690 }
6691 _epoch_guard.disarm();
6692 Ok((new_epoch, committed_row_ids))
6693 }
6694
6695 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
6698 let e = self.epoch.visible();
6699 let g = self.snapshots.register(e);
6700 (Snapshot::at(e), g)
6701 }
6702
6703 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
6706 let e = self.epoch.visible();
6707 let g = self.snapshots.register_owned(e);
6708 (Snapshot::at(e), g)
6709 }
6710
6711 pub fn set_history_retention_epochs(&self, epochs: u64) -> Result<()> {
6716 let _guard = self.ddl_lock.lock();
6717 let current = self.epoch.visible();
6718 let (old_epochs, old_start) = self.snapshots.history_config();
6719 let earliest_already_guaranteed = if old_epochs == 0 {
6720 current
6721 } else {
6722 Epoch(old_start.0.max(current.0.saturating_sub(old_epochs)))
6723 };
6724 let start = if epochs == 0 {
6725 current
6726 } else {
6727 earliest_already_guaranteed
6728 };
6729 write_history_retention(&self.root, epochs, start)?;
6730 self.snapshots.configure_history(epochs, start);
6731 Ok(())
6732 }
6733
6734 pub fn history_retention_epochs(&self) -> u64 {
6735 self.snapshots.history_config().0
6736 }
6737
6738 pub fn earliest_retained_epoch(&self) -> Epoch {
6739 let current = self.epoch.visible();
6740 self.snapshots.history_floor(current).unwrap_or(current)
6741 }
6742
6743 pub fn snapshot_at_owned(&self, epoch: Epoch) -> Result<(Snapshot, OwnedSnapshotGuard)> {
6746 let current = self.epoch.visible();
6747 if epoch > current {
6748 return Err(MongrelError::InvalidArgument(format!(
6749 "epoch {} is in the future; current epoch is {}",
6750 epoch.0, current.0
6751 )));
6752 }
6753 let earliest = self.earliest_retained_epoch();
6754 if epoch < earliest {
6755 return Err(MongrelError::InvalidArgument(format!(
6756 "epoch {} is no longer retained; earliest available epoch is {}",
6757 epoch.0, earliest.0
6758 )));
6759 }
6760 let guard = self.snapshots.register_owned(epoch);
6761 Ok((Snapshot::at(epoch), guard))
6762 }
6763
6764 pub fn table_names(&self) -> Vec<String> {
6766 self.catalog
6767 .read()
6768 .tables
6769 .iter()
6770 .filter(|t| matches!(t.state, TableState::Live))
6771 .map(|t| t.name.clone())
6772 .collect()
6773 }
6774
6775 pub fn close(&self) -> Result<()> {
6781 for name in self.table_names() {
6782 if let Ok(handle) = self.table(&name) {
6783 if let Err(e) = handle.lock().close() {
6784 eprintln!("[close] flush failed for {name}: {e}");
6785 }
6786 }
6787 }
6788 Ok(())
6789 }
6790
6791 pub fn compact(&self) -> Result<(usize, usize)> {
6798 self.require(&crate::auth::Permission::Ddl)?;
6799 let mut compacted = 0;
6800 let mut skipped = 0;
6801 for name in self.table_names() {
6802 let Ok(handle) = self.table(&name) else {
6803 continue;
6804 };
6805 {
6806 let mut t = handle.lock();
6807 let before = t.run_count();
6808 if before < 2 && !t.should_compact() {
6809 skipped += 1;
6810 continue;
6811 }
6812 match t.compact() {
6813 Ok(()) => {
6814 let after = t.run_count();
6815 compacted += 1;
6816 eprintln!("[compact] {name}: {before} -> {after} runs");
6817 }
6818 Err(e) => {
6819 eprintln!("[compact] {name}: compaction failed: {e}");
6820 skipped += 1;
6821 }
6822 }
6823 }
6824 }
6825 Ok((compacted, skipped))
6826 }
6827
6828 pub fn compact_table(&self, name: &str) -> Result<bool> {
6831 self.require(&crate::auth::Permission::Ddl)?;
6832 let handle = self.table(name)?;
6833 let mut t = handle.lock();
6834 let before = t.run_count();
6835 if before < 2 {
6836 return Ok(false);
6837 }
6838 t.compact()?;
6839 Ok(t.run_count() < before)
6840 }
6841
6842 pub fn table(&self, name: &str) -> Result<TableHandle> {
6844 let cat = self.catalog.read();
6845 let entry = cat
6846 .live(name)
6847 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
6848 let id = entry.table_id;
6849 drop(cat);
6850 self.tables
6851 .read()
6852 .get(&id)
6853 .cloned()
6854 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
6855 }
6856
6857 pub fn has_ttl_tables(&self) -> bool {
6860 self.tables
6861 .read()
6862 .values()
6863 .any(|table| table.lock().ttl().is_some())
6864 }
6865
6866 fn table_by_id(&self, id: u64) -> Result<TableHandle> {
6869 self.tables
6870 .read()
6871 .get(&id)
6872 .cloned()
6873 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
6874 }
6875
6876 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
6882 use crate::wal::DdlOp;
6883 use std::sync::atomic::Ordering;
6884
6885 self.require(&crate::auth::Permission::Ddl)?;
6886 if self.poisoned.load(Ordering::Relaxed) {
6887 return Err(MongrelError::Other(
6888 "database poisoned by fsync error".into(),
6889 ));
6890 }
6891
6892 let _g = self.ddl_lock.lock();
6893 {
6894 let cat = self.catalog.read();
6895 if cat.live(name).is_some() {
6896 return Err(MongrelError::InvalidArgument(format!(
6897 "table {name:?} already exists"
6898 )));
6899 }
6900 }
6901
6902 let commit_lock = Arc::clone(&self.commit_lock);
6905 let _c = commit_lock.lock();
6906 let table_id = {
6907 let mut cat = self.catalog.write();
6908 let id = cat.next_table_id;
6909 cat.next_table_id += 1;
6910 id
6911 };
6912 let epoch = self.epoch.bump_assigned();
6913 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6914 let txn_id = self.alloc_txn_id();
6915
6916 let mut schema = schema;
6920 schema.schema_id = table_id;
6921 schema.validate_auto_increment()?;
6928 schema.validate_defaults()?;
6929 schema.validate_ai()?;
6930 for index in &schema.indexes {
6931 index.validate_options()?;
6932 }
6933 for constraint in &schema.constraints.checks {
6934 constraint.expr.validate()?;
6935 }
6936
6937 let schema_json = DdlOp::encode_schema(&schema)?;
6940 let commit_seq = {
6941 let mut wal = self.shared_wal.lock();
6942 wal.append(
6943 txn_id,
6944 table_id,
6945 crate::wal::Op::Ddl(DdlOp::CreateTable {
6946 table_id,
6947 name: name.to_string(),
6948 schema_json,
6949 }),
6950 )?;
6951 wal.append_commit(txn_id, epoch, &[])?
6952 };
6953 self.group
6954 .await_durable(&self.shared_wal, commit_seq)
6955 .inspect_err(|_| {
6956 self.poisoned.store(true, Ordering::Relaxed);
6957 })?;
6958
6959 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
6961 std::fs::create_dir_all(&tdir)?;
6962 let ctx = SharedCtx {
6963 epoch: Arc::clone(&self.epoch),
6964 page_cache: Arc::clone(&self.page_cache),
6965 decoded_cache: Arc::clone(&self.decoded_cache),
6966 snapshots: Arc::clone(&self.snapshots),
6967 kek: self.kek.clone(),
6968 commit_lock: Arc::clone(&self.commit_lock),
6969 shared: Some(crate::engine::SharedWalCtx {
6970 wal: Arc::clone(&self.shared_wal),
6971 group: Arc::clone(&self.group),
6972 poisoned: Arc::clone(&self.poisoned),
6973 txn_ids: Arc::clone(&self.next_txn_id),
6974 change_wake: self.change_wake.clone(),
6975 }),
6976 table_name: Some(name.to_string()),
6977 auth: self.table_auth_checker(),
6978 read_only: self.read_only,
6979 };
6980 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
6981
6982 {
6985 let mut cat = self.catalog.write();
6986 cat.tables.push(CatalogEntry {
6987 table_id,
6988 name: name.to_string(),
6989 schema,
6990 state: TableState::Live,
6991 created_epoch: epoch.0,
6992 });
6993 }
6994 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
6995 self.tables
6996 .write()
6997 .insert(table_id, TableHandle::new(table));
6998
6999 self.epoch.publish_in_order(epoch);
7000 _epoch_guard.disarm();
7001 Ok(table_id)
7002 }
7003
7004 pub fn drop_table(&self, name: &str) -> Result<()> {
7006 use crate::wal::DdlOp;
7007 use std::sync::atomic::Ordering;
7008
7009 self.require(&crate::auth::Permission::Ddl)?;
7010 if self.poisoned.load(Ordering::Relaxed) {
7011 return Err(MongrelError::Other(
7012 "database poisoned by fsync error".into(),
7013 ));
7014 }
7015
7016 let _g = self.ddl_lock.lock();
7017 let table_id = {
7018 let cat = self.catalog.read();
7019 cat.live(name)
7020 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
7021 .table_id
7022 };
7023
7024 let commit_lock = Arc::clone(&self.commit_lock);
7025 let _c = commit_lock.lock();
7026 let epoch = self.epoch.bump_assigned();
7027 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7028 let txn_id = self.alloc_txn_id();
7029 let commit_seq = {
7030 let mut wal = self.shared_wal.lock();
7031 wal.append(
7032 txn_id,
7033 table_id,
7034 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
7035 )?;
7036 wal.append_commit(txn_id, epoch, &[])?
7037 };
7038 self.group
7039 .await_durable(&self.shared_wal, commit_seq)
7040 .inspect_err(|_| {
7041 self.poisoned.store(true, Ordering::Relaxed);
7042 })?;
7043
7044 {
7045 let mut cat = self.catalog.write();
7046 let entry = cat
7047 .tables
7048 .iter_mut()
7049 .find(|t| t.table_id == table_id)
7050 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
7051 entry.state = TableState::Dropped { at_epoch: epoch.0 };
7052 cat.triggers.retain(|trigger| {
7053 !matches!(
7054 &trigger.trigger.target,
7055 TriggerTarget::Table(target) if target == name
7056 )
7057 });
7058 cat.materialized_views
7059 .retain(|definition| definition.name != name);
7060 cat.security.rls_tables.retain(|table| table != name);
7061 cat.security.policies.retain(|policy| policy.table != name);
7062 cat.security.masks.retain(|mask| mask.table != name);
7063 for role in &mut cat.roles {
7064 role.permissions
7065 .retain(|permission| permission_table(permission) != Some(name));
7066 }
7067 cat.security_version = cat.security_version.wrapping_add(1);
7068 }
7069 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
7070 self.tables.write().remove(&table_id);
7071
7072 self.epoch.publish_in_order(epoch);
7073 _epoch_guard.disarm();
7074 Ok(())
7075 }
7076
7077 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
7086 use crate::wal::DdlOp;
7087 use std::sync::atomic::Ordering;
7088
7089 self.require(&crate::auth::Permission::Ddl)?;
7090 if self.poisoned.load(Ordering::Relaxed) {
7091 return Err(MongrelError::Other(
7092 "database poisoned by fsync error".into(),
7093 ));
7094 }
7095
7096 if name == new_name {
7099 return Ok(());
7100 }
7101 if new_name.is_empty() {
7102 return Err(MongrelError::InvalidArgument(
7103 "rename_table: new name must not be empty".into(),
7104 ));
7105 }
7106
7107 let _g = self.ddl_lock.lock();
7108 let table_id = {
7109 let cat = self.catalog.read();
7110 let src = cat
7111 .live(name)
7112 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
7113 if cat.live(new_name).is_some() {
7117 return Err(MongrelError::InvalidArgument(format!(
7118 "rename_table: a table named {new_name:?} already exists"
7119 )));
7120 }
7121 src.table_id
7122 };
7123
7124 let commit_lock = Arc::clone(&self.commit_lock);
7125 let _c = commit_lock.lock();
7126 let epoch = self.epoch.bump_assigned();
7127 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7128 let txn_id = self.alloc_txn_id();
7129 let commit_seq = {
7130 let mut wal = self.shared_wal.lock();
7131 wal.append(
7132 txn_id,
7133 table_id,
7134 crate::wal::Op::Ddl(DdlOp::RenameTable {
7135 table_id,
7136 new_name: new_name.to_string(),
7137 }),
7138 )?;
7139 wal.append_commit(txn_id, epoch, &[])?
7140 };
7141 self.group
7142 .await_durable(&self.shared_wal, commit_seq)
7143 .inspect_err(|_| {
7144 self.poisoned.store(true, Ordering::Relaxed);
7145 })?;
7146
7147 {
7148 let mut cat = self.catalog.write();
7149 let entry = cat
7150 .tables
7151 .iter_mut()
7152 .find(|t| t.table_id == table_id)
7153 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
7154 entry.name = new_name.to_string();
7155 for trigger in &mut cat.triggers {
7156 if matches!(
7157 &trigger.trigger.target,
7158 TriggerTarget::Table(target) if target == name
7159 ) {
7160 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
7161 }
7162 }
7163 if let Some(definition) = cat
7164 .materialized_views
7165 .iter_mut()
7166 .find(|definition| definition.name == name)
7167 {
7168 definition.name = new_name.to_string();
7169 }
7170 for table in &mut cat.security.rls_tables {
7171 if table == name {
7172 *table = new_name.to_string();
7173 }
7174 }
7175 for policy in &mut cat.security.policies {
7176 if policy.table == name {
7177 policy.table = new_name.to_string();
7178 }
7179 }
7180 for mask in &mut cat.security.masks {
7181 if mask.table == name {
7182 mask.table = new_name.to_string();
7183 }
7184 }
7185 for role in &mut cat.roles {
7186 for permission in &mut role.permissions {
7187 rename_permission_table(permission, name, new_name);
7188 }
7189 }
7190 cat.security_version = cat.security_version.wrapping_add(1);
7191 }
7192 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
7193 if let Some(table) = self.tables.read().get(&table_id) {
7196 table.lock().set_catalog_name(new_name.to_string());
7197 }
7198 self.epoch.publish_in_order(epoch);
7199 _epoch_guard.disarm();
7200 Ok(())
7201 }
7202
7203 pub fn alter_column(
7204 &self,
7205 table_name: &str,
7206 column_name: &str,
7207 change: AlterColumn,
7208 ) -> Result<ColumnDef> {
7209 use crate::wal::DdlOp;
7210 use std::sync::atomic::Ordering;
7211
7212 self.require(&crate::auth::Permission::Ddl)?;
7213 if self.poisoned.load(Ordering::Relaxed) {
7214 return Err(MongrelError::Other(
7215 "database poisoned by fsync error".into(),
7216 ));
7217 }
7218
7219 let _g = self.ddl_lock.lock();
7220 let table_id = {
7221 let cat = self.catalog.read();
7222 cat.live(table_name)
7223 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
7224 .table_id
7225 };
7226 let handle =
7227 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
7228 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
7229 })?;
7230
7231 let backfill = {
7237 let table = handle.lock();
7238 let old = table
7239 .schema()
7240 .column(column_name)
7241 .cloned()
7242 .ok_or_else(|| MongrelError::Schema(format!("unknown column {column_name}")))?;
7243 let next_flags = change.flags.unwrap_or(old.flags);
7244 if old.flags.contains(crate::schema::ColumnFlags::NULLABLE)
7245 && !next_flags.contains(crate::schema::ColumnFlags::NULLABLE)
7246 && old.default_value.is_some()
7247 {
7248 let snapshot = Snapshot::at(self.epoch.visible());
7249 let mut updates = Vec::new();
7250 for row in table.visible_rows(snapshot)? {
7251 if row
7252 .columns
7253 .get(&old.id)
7254 .is_some_and(|value| !matches!(value, Value::Null))
7255 {
7256 continue;
7257 }
7258 let mut cells: Vec<(u16, Value)> = row.columns.into_iter().collect();
7259 table.apply_defaults(&mut cells)?;
7260 updates.push((table_id, crate::txn::Staged::Update(row.row_id, cells)));
7261 }
7262 updates
7263 } else {
7264 Vec::new()
7265 }
7266 };
7267 if !backfill.is_empty() {
7268 self.commit_transaction_with_external_states(
7269 self.alloc_txn_id(),
7270 self.epoch.visible(),
7271 backfill,
7272 Vec::new(),
7273 Vec::new(),
7274 None,
7275 false,
7276 None,
7277 )?;
7278 }
7279 let mut table = handle.lock();
7280 let column = table.prepare_alter_column(column_name, &change)?;
7281 let renamed_column = (column.name != column_name).then(|| column.name.clone());
7282 if table
7283 .schema()
7284 .columns
7285 .iter()
7286 .find(|c| c.id == column.id)
7287 .is_some_and(|c| c == &column)
7288 {
7289 return Ok(column);
7290 }
7291
7292 let commit_lock = Arc::clone(&self.commit_lock);
7293 let _c = commit_lock.lock();
7294 let epoch = self.epoch.bump_assigned();
7295 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7296 let txn_id = self.alloc_txn_id();
7297 let column_json = DdlOp::encode_column(&column)?;
7298 let commit_seq = {
7299 let mut wal = self.shared_wal.lock();
7300 wal.append(
7301 txn_id,
7302 table_id,
7303 crate::wal::Op::Ddl(DdlOp::AlterTable {
7304 table_id,
7305 column_json,
7306 }),
7307 )?;
7308 wal.append_commit(txn_id, epoch, &[])?
7309 };
7310 self.group
7311 .await_durable(&self.shared_wal, commit_seq)
7312 .inspect_err(|_| {
7313 self.poisoned.store(true, Ordering::Relaxed);
7314 })?;
7315
7316 table.apply_altered_column(column.clone())?;
7317 let schema = table.schema().clone();
7318 drop(table);
7319
7320 {
7321 let mut cat = self.catalog.write();
7322 let entry = cat
7323 .tables
7324 .iter_mut()
7325 .find(|t| t.table_id == table_id)
7326 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
7327 entry.schema = schema;
7328 if let Some(new_column_name) = renamed_column {
7329 for trigger in &mut cat.triggers {
7330 if matches!(
7331 &trigger.trigger.target,
7332 TriggerTarget::Table(target) if target == table_name
7333 ) {
7334 trigger.trigger = trigger.trigger.renamed_update_column(
7335 column_name,
7336 new_column_name.clone(),
7337 epoch.0,
7338 )?;
7339 }
7340 }
7341 for role in &mut cat.roles {
7342 for permission in &mut role.permissions {
7343 rename_permission_column(
7344 permission,
7345 table_name,
7346 column_name,
7347 &new_column_name,
7348 );
7349 }
7350 }
7351 cat.security_version = cat.security_version.wrapping_add(1);
7352 }
7353 }
7354 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
7355
7356 self.epoch.publish_in_order(epoch);
7357 _epoch_guard.disarm();
7358 Ok(column)
7359 }
7360
7361 pub fn set_table_ttl(
7364 &self,
7365 table_name: &str,
7366 column_name: &str,
7367 duration_nanos: u64,
7368 ) -> Result<crate::manifest::TtlPolicy> {
7369 let policy = self.replace_table_ttl(table_name, Some((column_name, duration_nanos)))?;
7370 Ok(policy.expect("set TTL produces a policy"))
7371 }
7372
7373 pub fn clear_table_ttl(&self, table_name: &str) -> Result<()> {
7374 self.replace_table_ttl(table_name, None)?;
7375 Ok(())
7376 }
7377
7378 fn replace_table_ttl(
7379 &self,
7380 table_name: &str,
7381 requested: Option<(&str, u64)>,
7382 ) -> Result<Option<crate::manifest::TtlPolicy>> {
7383 use crate::wal::DdlOp;
7384 use std::sync::atomic::Ordering;
7385
7386 self.require(&crate::auth::Permission::Ddl)?;
7387 if self.poisoned.load(Ordering::Relaxed) {
7388 return Err(MongrelError::Other(
7389 "database poisoned by fsync error".into(),
7390 ));
7391 }
7392
7393 let _g = self.ddl_lock.lock();
7394 let table_id = {
7395 let cat = self.catalog.read();
7396 cat.live(table_name)
7397 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
7398 .table_id
7399 };
7400 let handle =
7401 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
7402 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
7403 })?;
7404 let mut table = handle.lock();
7405 let policy = match requested {
7406 Some((column, duration)) => Some(table.prepare_ttl_policy(column, duration)?),
7407 None => None,
7408 };
7409 if table.ttl() == policy {
7410 return Ok(policy);
7411 }
7412
7413 let commit_lock = Arc::clone(&self.commit_lock);
7414 let _c = commit_lock.lock();
7415 let epoch = self.epoch.bump_assigned();
7416 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
7417 let txn_id = self.alloc_txn_id();
7418 let policy_json = DdlOp::encode_ttl(policy)?;
7419 let commit_seq = {
7420 let mut wal = self.shared_wal.lock();
7421 wal.append(
7422 txn_id,
7423 table_id,
7424 crate::wal::Op::Ddl(DdlOp::SetTtl {
7425 table_id,
7426 policy_json,
7427 }),
7428 )?;
7429 wal.append_commit(txn_id, epoch, &[])?
7430 };
7431 self.group
7432 .await_durable(&self.shared_wal, commit_seq)
7433 .inspect_err(|_| {
7434 self.poisoned.store(true, Ordering::Relaxed);
7435 })?;
7436
7437 table.apply_ttl_policy_at(policy, epoch)?;
7438 self.epoch.publish_in_order(epoch);
7439 _epoch_guard.disarm();
7440 Ok(policy)
7441 }
7442
7443 pub fn gc(&self) -> Result<usize> {
7449 self.require(&crate::auth::Permission::Ddl)?;
7450 let min_active = self.snapshots.min_active(self.epoch.visible()).0;
7451 let mut reclaimed = 0;
7452
7453 let cat = self.catalog.read();
7455 for entry in &cat.tables {
7456 if let TableState::Dropped { at_epoch } = entry.state {
7457 if at_epoch <= min_active {
7458 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
7459 if tdir.exists() {
7460 std::fs::remove_dir_all(&tdir)?;
7461 reclaimed += 1;
7462 }
7463 }
7464 }
7465 }
7466 drop(cat);
7467
7468 let cat = self.catalog.read();
7473 for entry in &cat.tables {
7474 if !matches!(entry.state, TableState::Live) {
7475 continue;
7476 }
7477 let txn_dir = self
7478 .root
7479 .join(TABLES_DIR)
7480 .join(entry.table_id.to_string())
7481 .join("_txn");
7482 if !txn_dir.exists() {
7483 continue;
7484 }
7485 for sub in std::fs::read_dir(&txn_dir)? {
7486 let sub = sub?;
7487 let name = sub.file_name();
7488 let Some(name) = name.to_str() else { continue };
7489 let is_active = name
7491 .parse::<u64>()
7492 .map(|id| self.active_spills.is_active(id))
7493 .unwrap_or(false);
7494 if is_active {
7495 continue;
7496 }
7497 std::fs::remove_dir_all(sub.path())?;
7498 reclaimed += 1;
7499 }
7500 }
7501 drop(cat);
7502
7503 let external_names = {
7504 let cat = self.catalog.read();
7505 cat.external_tables
7506 .iter()
7507 .map(|entry| entry.name.clone())
7508 .collect::<std::collections::HashSet<_>>()
7509 };
7510 let vtab_dir = self.root.join(VTAB_DIR);
7511 if vtab_dir.exists() {
7512 for entry in std::fs::read_dir(&vtab_dir)? {
7513 let entry = entry?;
7514 let name = entry.file_name();
7515 let Some(name) = name.to_str() else { continue };
7516 if external_names.contains(name) {
7517 continue;
7518 }
7519 let path = entry.path();
7520 if path.is_dir() {
7521 std::fs::remove_dir_all(path)?;
7522 } else {
7523 std::fs::remove_file(path)?;
7524 }
7525 reclaimed += 1;
7526 }
7527 }
7528
7529 let tables = self.tables.read();
7533 for (table_id, handle) in tables.iter() {
7534 let backup_pinned: HashSet<u128> = self
7535 .backup_pins
7536 .lock()
7537 .keys()
7538 .filter_map(|(pinned_table, run_id)| {
7539 (*pinned_table == *table_id).then_some(*run_id)
7540 })
7541 .collect();
7542 reclaimed += handle
7543 .lock()
7544 .reap_retiring(Epoch(min_active), &backup_pinned)?;
7545 }
7546
7547 let all_durable = self.active_spills.is_idle()
7555 && tables.values().all(|h| {
7556 let g = h.lock();
7557 g.memtable_len() == 0 && g.mutable_run_len() == 0
7558 });
7559 drop(tables);
7560 if all_durable {
7561 let retain = self
7562 .replication_wal_retention_segments
7563 .load(std::sync::atomic::Ordering::Relaxed);
7564 reclaimed += self
7565 .shared_wal
7566 .lock()
7567 .gc_segments_retain_recent(u64::MAX, retain)?;
7568 }
7569
7570 Ok(reclaimed)
7571 }
7572
7573 pub fn checkpoint(&self) -> Result<()> {
7591 self.close()?;
7593
7594 let _ = self.compact()?;
7596
7597 self.gc()?;
7600
7601 {
7608 let wal = self.shared_wal.lock();
7609 let active = wal.active_segment_no();
7610 drop(wal);
7611 let wal_dir = self.root.join("_wal");
7613 if wal_dir.exists() {
7614 for entry in std::fs::read_dir(&wal_dir)? {
7615 let entry = entry?;
7616 let path = entry.path();
7617 if path.extension().is_some_and(|ext| ext == "wal") {
7618 let _ = std::fs::remove_file(&path);
7619 }
7620 }
7621 }
7622 let _ = active; }
7624
7625 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
7627
7628 let tables = self.tables.read();
7631 let visible = self.epoch.visible();
7632 for handle in tables.values() {
7633 handle.lock().persist_manifest(visible)?;
7634 }
7635
7636 Ok(())
7637 }
7638 fn alloc_txn_id(&self) -> u64 {
7639 let mut g = self.next_txn_id.lock();
7640 let id = *g;
7641 *g = g.wrapping_add(1);
7642 id
7643 }
7644
7645 pub fn set_spill_threshold(&self, bytes: u64) {
7649 self.spill_threshold
7650 .store(bytes, std::sync::atomic::Ordering::Relaxed);
7651 }
7652
7653 #[doc(hidden)]
7657 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
7658 *self.spill_hook.lock() = Some(Box::new(f));
7659 }
7660
7661 #[doc(hidden)]
7664 pub fn __set_backup_hook(&self, f: impl Fn() + Send + Sync + 'static) {
7665 *self.backup_hook.lock() = Some(Box::new(f));
7666 }
7667
7668 #[doc(hidden)]
7672 pub fn __wal_group_sync_count(&self) -> u64 {
7673 self.shared_wal.lock().group_sync_count()
7674 }
7675
7676 #[doc(hidden)]
7679 pub fn __poison(&self) {
7680 self.poisoned
7681 .store(true, std::sync::atomic::Ordering::Relaxed);
7682 }
7683
7684 pub fn check(&self) -> Vec<CheckIssue> {
7697 let mut issues = Vec::new();
7698 let cat = self.catalog.read();
7699 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
7700 for entry in &cat.tables {
7701 if !matches!(entry.state, TableState::Live) {
7702 continue;
7703 }
7704 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
7705 let mut err = |sev: &str, desc: String| {
7706 issues.push(CheckIssue {
7707 table_id: entry.table_id,
7708 table_name: entry.name.clone(),
7709 severity: sev.into(),
7710 description: desc,
7711 });
7712 };
7713 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
7714 Ok(m) => m,
7715 Err(e) => {
7716 err("error", format!("manifest read failed: {e}"));
7717 continue;
7718 }
7719 };
7720 if m.flushed_epoch > m.current_epoch {
7721 err(
7722 "error",
7723 format!(
7724 "flushed_epoch {} exceeds current_epoch {} (impossible)",
7725 m.flushed_epoch, m.current_epoch
7726 ),
7727 );
7728 }
7729
7730 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
7731 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
7732 for rr in &m.runs {
7733 referenced.insert(rr.run_id);
7734 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
7735 if !run_path.exists() {
7736 err("error", format!("missing run file: r-{}.sr", rr.run_id));
7737 continue;
7738 }
7739 match crate::sorted_run::RunReader::open(
7740 &run_path,
7741 entry.schema.clone(),
7742 self.kek.clone(),
7743 ) {
7744 Ok(reader) => {
7745 if reader.row_count() as u64 != rr.row_count {
7746 err(
7747 "error",
7748 format!(
7749 "run r-{} row count mismatch: manifest {} vs run {}",
7750 rr.run_id,
7751 rr.row_count,
7752 reader.row_count()
7753 ),
7754 );
7755 }
7756 }
7757 Err(e) => {
7758 err(
7759 "error",
7760 format!("run r-{} integrity check failed: {e}", rr.run_id),
7761 );
7762 }
7763 }
7764 }
7765
7766 for r in &m.retiring {
7770 referenced.insert(r.run_id);
7771 }
7772
7773 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
7775 for ent in rd.flatten() {
7776 let p = ent.path();
7777 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
7778 continue;
7779 }
7780 let run_id = p
7781 .file_stem()
7782 .and_then(|s| s.to_str())
7783 .and_then(|s| s.strip_prefix("r-"))
7784 .and_then(|s| s.parse::<u128>().ok());
7785 if let Some(id) = run_id {
7786 if !referenced.contains(&id) {
7787 err(
7788 "warning",
7789 format!("orphan run file r-{id}.sr not referenced by the manifest"),
7790 );
7791 }
7792 }
7793 }
7794 }
7795 }
7796
7797 let external_names = cat
7798 .external_tables
7799 .iter()
7800 .map(|entry| entry.name.clone())
7801 .collect::<std::collections::HashSet<_>>();
7802 let vtab_dir = self.root.join(VTAB_DIR);
7803 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
7804 for entry in entries.flatten() {
7805 let name = entry.file_name();
7806 let Some(name) = name.to_str() else { continue };
7807 if !external_names.contains(name) {
7808 issues.push(CheckIssue {
7809 table_id: EXTERNAL_TABLE_ID,
7810 table_name: name.to_string(),
7811 severity: "warning".into(),
7812 description: format!(
7813 "orphan external table state entry {:?} not referenced by the catalog",
7814 entry.path()
7815 ),
7816 });
7817 }
7818 }
7819 }
7820
7821 for (seg, msg) in self.shared_wal.lock().verify_segments() {
7828 issues.push(CheckIssue {
7829 table_id: WAL_TABLE_ID,
7830 table_name: "<wal>".into(),
7831 severity: "error".into(),
7832 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
7833 });
7834 }
7835 issues
7836 }
7837
7838 pub fn doctor(&self) -> Result<Vec<u64>> {
7842 let _ddl = self.ddl_lock.lock();
7845 let issues = self.check();
7846 let bad_tables: std::collections::HashSet<u64> = issues
7851 .iter()
7852 .filter(|i| {
7853 i.severity == "error"
7854 && i.table_id != WAL_TABLE_ID
7855 && i.table_id != EXTERNAL_TABLE_ID
7856 })
7857 .map(|i| i.table_id)
7858 .collect();
7859 if bad_tables.is_empty() {
7860 return Ok(Vec::new());
7861 }
7862
7863 let qdir = self.root.join("_quarantine");
7864 std::fs::create_dir_all(&qdir)?;
7865 let mut quarantined = Vec::new();
7866 for &table_id in &bad_tables {
7867 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
7868 if tdir.exists() {
7869 let dest = qdir.join(table_id.to_string());
7870 std::fs::rename(&tdir, &dest)?;
7871 }
7872 {
7873 let mut cat = self.catalog.write();
7874 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
7875 entry.state = TableState::Dropped {
7876 at_epoch: self.epoch.visible().0,
7877 };
7878 }
7879 }
7880 self.tables.write().remove(&table_id);
7882 quarantined.push(table_id);
7883 }
7884 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
7885 Ok(quarantined)
7886 }
7887
7888 #[allow(dead_code)]
7890 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
7891 self.kek.as_ref()
7892 }
7893
7894 #[allow(dead_code)]
7896 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
7897 &self.epoch
7898 }
7899
7900 #[allow(dead_code)]
7902 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
7903 &self.snapshots
7904 }
7905}
7906
7907fn external_state_dir(root: &Path, name: &str) -> PathBuf {
7908 root.join(VTAB_DIR).join(name)
7909}
7910
7911fn filter_ignored_staging(
7912 staging: Vec<(u64, crate::txn::Staged)>,
7913 ignored_indices: &std::collections::BTreeSet<usize>,
7914) -> Vec<(u64, crate::txn::Staged)> {
7915 if ignored_indices.is_empty() {
7916 return staging;
7917 }
7918 staging
7919 .into_iter()
7920 .enumerate()
7921 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
7922 .collect()
7923}
7924
7925fn external_state_file(root: &Path, name: &str) -> PathBuf {
7926 external_state_dir(root, name).join("state.json")
7927}
7928
7929fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
7930 let path = external_state_file(root, name);
7931 match std::fs::read(path) {
7932 Ok(bytes) => Ok(bytes),
7933 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
7934 Err(e) => Err(e.into()),
7935 }
7936}
7937
7938fn current_external_state_bytes(
7939 root: &Path,
7940 external_states: &[(String, Vec<u8>)],
7941 name: &str,
7942) -> Result<Vec<u8>> {
7943 for (table, state) in external_states.iter().rev() {
7944 if table == name {
7945 return Ok(state.clone());
7946 }
7947 }
7948 read_external_state_file(root, name)
7949}
7950
7951fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
7952 let mut out = external_states;
7953 dedup_external_states_in_place(&mut out);
7954 out
7955}
7956
7957fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
7958 let mut seen = std::collections::HashSet::new();
7959 let mut out = Vec::with_capacity(external_states.len());
7960 for (name, state) in std::mem::take(external_states).into_iter().rev() {
7961 if seen.insert(name.clone()) {
7962 out.push((name, state));
7963 }
7964 }
7965 out.reverse();
7966 *external_states = out;
7967}
7968
7969fn prepare_external_state_file(
7970 root: &Path,
7971 name: &str,
7972 state: &[u8],
7973 txn_id: u64,
7974) -> Result<PathBuf> {
7975 let dir = external_state_dir(root, name);
7976 std::fs::create_dir_all(&dir)?;
7977 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
7978 {
7979 let mut file = std::fs::File::create(&pending)?;
7980 file.write_all(state)?;
7981 file.sync_all()?;
7982 }
7983 Ok(pending)
7984}
7985
7986fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
7987 let path = external_state_file(root, name);
7988 std::fs::rename(pending, &path)?;
7989 if let Ok(dir) = std::fs::File::open(external_state_dir(root, name)) {
7990 let _ = dir.sync_all();
7991 }
7992 Ok(())
7993}
7994
7995fn write_external_state_file(root: &Path, name: &str, state: &[u8]) -> Result<()> {
7996 let pending = prepare_external_state_file(root, name, state, 0)?;
7997 publish_external_state_file(root, name, &pending)
7998}
7999
8000fn recover_shared_wal(
8009 root: &Path,
8010 tables: &HashMap<u64, TableHandle>,
8011 epoch: &EpochAuthority,
8012 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
8013) -> Result<()> {
8014 use crate::memtable::Row;
8015 use crate::rowid::RowId;
8016 use crate::wal::{Op, SharedWal};
8017
8018 let records = SharedWal::replay_with_dek(root, wal_dek)?;
8019
8020 let mut committed: HashMap<u64, u64> = HashMap::new();
8022 let mut spilled_to_link: Vec<(
8023 u64, u64, Vec<crate::wal::AddedRun>,
8026 )> = Vec::new();
8027 for r in &records {
8028 if let Op::TxnCommit {
8029 epoch: ce,
8030 ref added_runs,
8031 } = r.op
8032 {
8033 committed.insert(r.txn_id, ce);
8034 if !added_runs.is_empty() {
8035 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
8036 }
8037 }
8038 }
8039 let truncated_transactions: HashSet<(u64, u64)> = records
8040 .iter()
8041 .filter_map(|record| {
8042 committed.get(&record.txn_id)?;
8043 match record.op {
8044 Op::TruncateTable { table_id } => Some((record.txn_id, table_id)),
8045 _ => None,
8046 }
8047 })
8048 .collect();
8049
8050 type TableStage = (Vec<Row>, Vec<(RowId, Epoch)>, Option<Epoch>, Epoch);
8052 let mut stage: HashMap<u64, TableStage> = HashMap::new();
8053 let mut max_epoch = epoch.visible().0;
8054 for r in records {
8055 let Some(&ce) = committed.get(&r.txn_id) else {
8056 continue; };
8058 let commit_epoch = Epoch(ce);
8059 max_epoch = max_epoch.max(ce);
8060 match r.op {
8061 Op::Put { table_id, rows } => {
8062 let skip = tables
8064 .get(&table_id)
8065 .map(|h| h.lock().flushed_epoch() >= ce)
8066 .unwrap_or(true);
8067 if skip {
8068 continue;
8069 }
8070 let rows: Vec<Row> = match bincode::deserialize(&rows) {
8071 Ok(v) => v,
8072 Err(_) => continue,
8073 };
8074 let rows: Vec<Row> = rows
8077 .into_iter()
8078 .map(|mut row| {
8079 row.committed_epoch = commit_epoch;
8080 row
8081 })
8082 .collect();
8083 let entry = stage
8084 .entry(table_id)
8085 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
8086 entry.0.extend(rows);
8087 entry.3 = commit_epoch;
8088 }
8089 Op::Delete { table_id, row_ids } => {
8090 let skip = tables
8091 .get(&table_id)
8092 .map(|h| h.lock().flushed_epoch() >= ce)
8093 .unwrap_or(true);
8094 if skip {
8095 continue;
8096 }
8097 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
8098 let entry = stage
8099 .entry(table_id)
8100 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
8101 entry.1.extend(dels);
8102 entry.3 = commit_epoch;
8103 }
8104 Op::TruncateTable { table_id } => {
8105 let skip = tables
8106 .get(&table_id)
8107 .map(|h| h.lock().flushed_epoch() >= ce)
8108 .unwrap_or(true);
8109 if skip {
8110 continue;
8111 }
8112 stage.insert(
8113 table_id,
8114 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
8115 );
8116 }
8117 Op::ExternalTableState { name, state } => {
8118 write_external_state_file(root, &name, &state)?;
8119 }
8120 Op::Flush { .. }
8121 | Op::TxnCommit { .. }
8122 | Op::TxnAbort
8123 | Op::Ddl(_)
8124 | Op::BeforeImage { .. }
8125 | Op::CommitTimestamp { .. } => {}
8126 }
8127 }
8128 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
8129 let Some(handle) = tables.get(&table_id) else {
8130 continue;
8131 };
8132 let mut t = handle.lock();
8133 if let Some(epoch) = truncate_epoch {
8134 t.apply_truncate(epoch)?;
8135 }
8136 t.recover_apply(rows, deletes)?;
8137 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
8141 t.live_count = rows.len() as u64;
8142 t.persist_manifest(table_epoch)?;
8143 }
8144
8145 for (txn_id, ce, added_runs) in &spilled_to_link {
8149 for ar in added_runs {
8150 let Some(handle) = tables.get(&ar.table_id) else {
8151 continue;
8152 };
8153 let mut t = handle.lock();
8154 let dest = t.run_path(ar.run_id as u64);
8155 if !dest.exists() {
8156 let pending = root
8157 .join(TABLES_DIR)
8158 .join(ar.table_id.to_string())
8159 .join("_txn")
8160 .join(txn_id.to_string())
8161 .join(format!("r-{}.sr", ar.run_id));
8162 if pending.exists() {
8163 if let Some(parent) = pending.parent() {
8164 std::fs::rename(&pending, &dest)?;
8165 let _ = std::fs::remove_dir_all(parent);
8166 }
8167 }
8168 }
8169 if t.run_path(ar.run_id as u64).exists() {
8175 let linked = t.recover_spilled_run(crate::manifest::RunRef {
8176 run_id: ar.run_id,
8177 level: ar.level,
8178 epoch_created: *ce,
8179 row_count: ar.row_count,
8180 });
8181 let replaced = truncated_transactions.contains(&(*txn_id, ar.table_id));
8182 if replaced {
8183 t.set_flushed_epoch(Epoch(*ce));
8184 }
8185 if linked || replaced {
8186 t.persist_manifest(Epoch(*ce))?;
8187 }
8188 }
8189 }
8190 }
8191
8192 epoch.advance_recovered(Epoch(max_epoch));
8193 Ok(())
8194}
8195
8196fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
8197 match condition {
8198 ProcedureCondition::Pk { .. } => {
8199 if schema.primary_key().is_none() {
8200 return Err(MongrelError::InvalidArgument(
8201 "procedure condition Pk references a table without a primary key".into(),
8202 ));
8203 }
8204 }
8205 ProcedureCondition::BitmapEq { column_id, .. }
8206 | ProcedureCondition::BitmapIn { column_id, .. }
8207 | ProcedureCondition::Range { column_id, .. }
8208 | ProcedureCondition::RangeF64 { column_id, .. }
8209 | ProcedureCondition::IsNull { column_id }
8210 | ProcedureCondition::IsNotNull { column_id }
8211 | ProcedureCondition::FmContains { column_id, .. } => {
8212 validate_column_id(*column_id, schema)?;
8213 }
8214 }
8215 Ok(())
8216}
8217
8218fn bind_procedure_args(
8219 procedure: &StoredProcedure,
8220 mut args: HashMap<String, crate::Value>,
8221) -> Result<HashMap<String, crate::Value>> {
8222 let mut out = HashMap::new();
8223 for param in &procedure.params {
8224 let value = match args.remove(¶m.name) {
8225 Some(value) => value,
8226 None => param.default.clone().ok_or_else(|| {
8227 MongrelError::InvalidArgument(format!(
8228 "missing required procedure parameter {:?}",
8229 param.name
8230 ))
8231 })?,
8232 };
8233 if !param.nullable && matches!(value, crate::Value::Null) {
8234 return Err(MongrelError::InvalidArgument(format!(
8235 "procedure parameter {:?} must not be NULL",
8236 param.name
8237 )));
8238 }
8239 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty.clone()) {
8240 return Err(MongrelError::InvalidArgument(format!(
8241 "procedure parameter {:?} has wrong type",
8242 param.name
8243 )));
8244 }
8245 out.insert(param.name.clone(), value);
8246 }
8247 if let Some(extra) = args.keys().next() {
8248 return Err(MongrelError::InvalidArgument(format!(
8249 "unknown procedure parameter {extra:?}"
8250 )));
8251 }
8252 Ok(out)
8253}
8254
8255fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
8256 matches!(
8257 (value, ty),
8258 (crate::Value::Bool(_), crate::TypeId::Bool)
8259 | (crate::Value::Int64(_), crate::TypeId::Int8)
8260 | (crate::Value::Int64(_), crate::TypeId::Int16)
8261 | (crate::Value::Int64(_), crate::TypeId::Int32)
8262 | (crate::Value::Int64(_), crate::TypeId::Int64)
8263 | (crate::Value::Int64(_), crate::TypeId::UInt8)
8264 | (crate::Value::Int64(_), crate::TypeId::UInt16)
8265 | (crate::Value::Int64(_), crate::TypeId::UInt32)
8266 | (crate::Value::Int64(_), crate::TypeId::UInt64)
8267 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
8268 | (crate::Value::Int64(_), crate::TypeId::Date32)
8269 | (crate::Value::Float64(_), crate::TypeId::Float32)
8270 | (crate::Value::Float64(_), crate::TypeId::Float64)
8271 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
8272 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
8273 )
8274}
8275
8276fn eval_cells(
8277 cells: &[crate::procedure::ProcedureCell],
8278 args: &HashMap<String, crate::Value>,
8279 outputs: &HashMap<String, ProcedureCallOutput>,
8280) -> Result<Vec<(u16, crate::Value)>> {
8281 cells
8282 .iter()
8283 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
8284 .collect()
8285}
8286
8287fn eval_condition(
8288 condition: &ProcedureCondition,
8289 args: &HashMap<String, crate::Value>,
8290 outputs: &HashMap<String, ProcedureCallOutput>,
8291) -> Result<crate::Condition> {
8292 Ok(match condition {
8293 ProcedureCondition::Pk { value } => {
8294 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
8295 }
8296 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
8297 column_id: *column_id,
8298 value: eval_value(value, args, outputs)?.encode_key(),
8299 },
8300 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
8301 column_id: *column_id,
8302 values: values
8303 .iter()
8304 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
8305 .collect::<Result<Vec<_>>>()?,
8306 },
8307 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
8308 column_id: *column_id,
8309 lo: expect_i64(eval_value(lo, args, outputs)?)?,
8310 hi: expect_i64(eval_value(hi, args, outputs)?)?,
8311 },
8312 ProcedureCondition::RangeF64 {
8313 column_id,
8314 lo,
8315 lo_inclusive,
8316 hi,
8317 hi_inclusive,
8318 } => crate::Condition::RangeF64 {
8319 column_id: *column_id,
8320 lo: expect_f64(eval_value(lo, args, outputs)?)?,
8321 lo_inclusive: *lo_inclusive,
8322 hi: expect_f64(eval_value(hi, args, outputs)?)?,
8323 hi_inclusive: *hi_inclusive,
8324 },
8325 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
8326 column_id: *column_id,
8327 },
8328 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
8329 column_id: *column_id,
8330 },
8331 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
8332 column_id: *column_id,
8333 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
8334 },
8335 })
8336}
8337
8338fn eval_value(
8339 value: &ProcedureValue,
8340 args: &HashMap<String, crate::Value>,
8341 outputs: &HashMap<String, ProcedureCallOutput>,
8342) -> Result<crate::Value> {
8343 match value {
8344 ProcedureValue::Literal(value) => Ok(value.clone()),
8345 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
8346 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
8347 }),
8348 ProcedureValue::StepScalar(id) => match outputs.get(id) {
8349 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
8350 _ => Err(MongrelError::InvalidArgument(format!(
8351 "procedure step {id:?} did not return a scalar"
8352 ))),
8353 },
8354 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
8355 Err(MongrelError::InvalidArgument(
8356 "row-valued procedure reference cannot be used as a scalar".into(),
8357 ))
8358 }
8359 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
8360 "structured procedure value cannot be used as a scalar cell".into(),
8361 )),
8362 }
8363}
8364
8365fn eval_return_output(
8366 value: &ProcedureValue,
8367 args: &HashMap<String, crate::Value>,
8368 outputs: &HashMap<String, ProcedureCallOutput>,
8369) -> Result<ProcedureCallOutput> {
8370 match value {
8371 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
8372 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
8373 args.get(name).cloned().ok_or_else(|| {
8374 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
8375 })?,
8376 )),
8377 ProcedureValue::StepRows(id)
8378 | ProcedureValue::StepRow(id)
8379 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
8380 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
8381 }),
8382 ProcedureValue::Object(fields) => {
8383 let mut out = Vec::with_capacity(fields.len());
8384 for (name, value) in fields {
8385 out.push((name.clone(), eval_return_output(value, args, outputs)?));
8386 }
8387 Ok(ProcedureCallOutput::Object(out))
8388 }
8389 ProcedureValue::Array(values) => {
8390 let mut out = Vec::with_capacity(values.len());
8391 for value in values {
8392 out.push(eval_return_output(value, args, outputs)?);
8393 }
8394 Ok(ProcedureCallOutput::Array(out))
8395 }
8396 }
8397}
8398
8399fn expect_i64(value: crate::Value) -> Result<i64> {
8400 match value {
8401 crate::Value::Int64(value) => Ok(value),
8402 _ => Err(MongrelError::InvalidArgument(
8403 "procedure value must be Int64".into(),
8404 )),
8405 }
8406}
8407
8408fn expect_f64(value: crate::Value) -> Result<f64> {
8409 match value {
8410 crate::Value::Float64(value) => Ok(value),
8411 _ => Err(MongrelError::InvalidArgument(
8412 "procedure value must be Float64".into(),
8413 )),
8414 }
8415}
8416
8417fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
8418 match value {
8419 crate::Value::Bytes(value) => Ok(value),
8420 _ => Err(MongrelError::InvalidArgument(
8421 "procedure value must be Bytes".into(),
8422 )),
8423 }
8424}
8425
8426fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
8427 if schema.columns.iter().any(|c| c.id == column_id) {
8428 Ok(())
8429 } else {
8430 Err(MongrelError::InvalidArgument(format!(
8431 "unknown column id {column_id}"
8432 )))
8433 }
8434}
8435
8436fn trigger_matches_event(
8437 trigger: &StoredTrigger,
8438 event: &WriteEvent,
8439 cat: &Catalog,
8440) -> Result<bool> {
8441 if trigger.event != event.kind {
8442 return Ok(false);
8443 }
8444 let TriggerTarget::Table(target) = &trigger.target else {
8445 return Ok(false);
8446 };
8447 if target != &event.table {
8448 return Ok(false);
8449 }
8450 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
8451 let schema = &cat
8452 .live(target)
8453 .ok_or_else(|| {
8454 MongrelError::InvalidArgument(format!(
8455 "trigger {:?} references unknown table {target:?}",
8456 trigger.name
8457 ))
8458 })?
8459 .schema;
8460 let mut watched = Vec::with_capacity(trigger.update_of.len());
8461 for name in &trigger.update_of {
8462 let col = schema.column(name).ok_or_else(|| {
8463 MongrelError::InvalidArgument(format!(
8464 "trigger {:?} references unknown UPDATE OF column {name:?}",
8465 trigger.name
8466 ))
8467 })?;
8468 watched.push(col.id);
8469 }
8470 if !event
8471 .changed_columns
8472 .iter()
8473 .any(|column_id| watched.contains(column_id))
8474 {
8475 return Ok(false);
8476 }
8477 }
8478 Ok(true)
8479}
8480
8481fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
8482 let mut ids = std::collections::BTreeSet::new();
8483 if let Some(old) = old {
8484 ids.extend(old.columns.keys().copied());
8485 }
8486 if let Some(new) = new {
8487 ids.extend(new.columns.keys().copied());
8488 }
8489 ids.into_iter()
8490 .filter(|id| {
8491 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
8492 })
8493 .collect()
8494}
8495
8496fn eval_trigger_cells(
8497 cells: &[crate::trigger::TriggerCell],
8498 event: &WriteEvent,
8499 selected: Option<&TriggerRowImage>,
8500) -> Result<Vec<(u16, Value)>> {
8501 cells
8502 .iter()
8503 .map(|cell| {
8504 Ok((
8505 cell.column_id,
8506 eval_trigger_value(&cell.value, event, selected)?,
8507 ))
8508 })
8509 .collect()
8510}
8511
8512fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
8513 match expr {
8514 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
8515 Value::Bool(value) => Ok(value),
8516 Value::Null => Ok(false),
8517 other => Err(MongrelError::InvalidArgument(format!(
8518 "trigger WHEN value must be boolean, got {other:?}"
8519 ))),
8520 },
8521 TriggerExpr::Eq { left, right } => Ok(values_equal(
8522 &eval_trigger_value(left, event, None)?,
8523 &eval_trigger_value(right, event, None)?,
8524 )),
8525 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
8526 &eval_trigger_value(left, event, None)?,
8527 &eval_trigger_value(right, event, None)?,
8528 )),
8529 TriggerExpr::Lt { left, right } => match value_order(
8530 &eval_trigger_value(left, event, None)?,
8531 &eval_trigger_value(right, event, None)?,
8532 ) {
8533 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
8534 None => Ok(false),
8535 },
8536 TriggerExpr::Lte { left, right } => match value_order(
8537 &eval_trigger_value(left, event, None)?,
8538 &eval_trigger_value(right, event, None)?,
8539 ) {
8540 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
8541 None => Ok(false),
8542 },
8543 TriggerExpr::Gt { left, right } => match value_order(
8544 &eval_trigger_value(left, event, None)?,
8545 &eval_trigger_value(right, event, None)?,
8546 ) {
8547 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
8548 None => Ok(false),
8549 },
8550 TriggerExpr::Gte { left, right } => match value_order(
8551 &eval_trigger_value(left, event, None)?,
8552 &eval_trigger_value(right, event, None)?,
8553 ) {
8554 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
8555 None => Ok(false),
8556 },
8557 TriggerExpr::IsNull(value) => Ok(matches!(
8558 eval_trigger_value(value, event, None)?,
8559 Value::Null
8560 )),
8561 TriggerExpr::IsNotNull(value) => Ok(!matches!(
8562 eval_trigger_value(value, event, None)?,
8563 Value::Null
8564 )),
8565 TriggerExpr::And { left, right } => {
8566 if !eval_trigger_expr(left, event)? {
8567 Ok(false)
8568 } else {
8569 Ok(eval_trigger_expr(right, event)?)
8570 }
8571 }
8572 TriggerExpr::Or { left, right } => {
8573 if eval_trigger_expr(left, event)? {
8574 Ok(true)
8575 } else {
8576 Ok(eval_trigger_expr(right, event)?)
8577 }
8578 }
8579 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
8580 }
8581}
8582
8583fn eval_trigger_condition(
8584 condition: &TriggerCondition,
8585 event: &WriteEvent,
8586 selected: &TriggerRowImage,
8587 schema: &Schema,
8588) -> Result<bool> {
8589 match condition {
8590 TriggerCondition::Pk { value } => {
8591 let pk = schema.primary_key().ok_or_else(|| {
8592 MongrelError::InvalidArgument(
8593 "trigger condition Pk references a table without a primary key".into(),
8594 )
8595 })?;
8596 let lhs = eval_trigger_value(value, event, Some(selected))?;
8597 Ok(values_equal(
8598 &lhs,
8599 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
8600 ))
8601 }
8602 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
8603 selected.columns.get(column_id).unwrap_or(&Value::Null),
8604 &eval_trigger_value(value, event, Some(selected))?,
8605 )),
8606 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
8607 selected.columns.get(column_id).unwrap_or(&Value::Null),
8608 &eval_trigger_value(value, event, Some(selected))?,
8609 )),
8610 TriggerCondition::Lt { column_id, value } => match value_order(
8611 selected.columns.get(column_id).unwrap_or(&Value::Null),
8612 &eval_trigger_value(value, event, Some(selected))?,
8613 ) {
8614 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
8615 None => Ok(false),
8616 },
8617 TriggerCondition::Lte { column_id, value } => match value_order(
8618 selected.columns.get(column_id).unwrap_or(&Value::Null),
8619 &eval_trigger_value(value, event, Some(selected))?,
8620 ) {
8621 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
8622 None => Ok(false),
8623 },
8624 TriggerCondition::Gt { column_id, value } => match value_order(
8625 selected.columns.get(column_id).unwrap_or(&Value::Null),
8626 &eval_trigger_value(value, event, Some(selected))?,
8627 ) {
8628 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
8629 None => Ok(false),
8630 },
8631 TriggerCondition::Gte { column_id, value } => match value_order(
8632 selected.columns.get(column_id).unwrap_or(&Value::Null),
8633 &eval_trigger_value(value, event, Some(selected))?,
8634 ) {
8635 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
8636 None => Ok(false),
8637 },
8638 TriggerCondition::IsNull { column_id } => Ok(matches!(
8639 selected.columns.get(column_id),
8640 None | Some(Value::Null)
8641 )),
8642 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
8643 selected.columns.get(column_id),
8644 None | Some(Value::Null)
8645 )),
8646 TriggerCondition::And { left, right } => {
8647 if !eval_trigger_condition(left, event, selected, schema)? {
8648 Ok(false)
8649 } else {
8650 Ok(eval_trigger_condition(right, event, selected, schema)?)
8651 }
8652 }
8653 TriggerCondition::Or { left, right } => {
8654 if eval_trigger_condition(left, event, selected, schema)? {
8655 Ok(true)
8656 } else {
8657 Ok(eval_trigger_condition(right, event, selected, schema)?)
8658 }
8659 }
8660 TriggerCondition::Not(condition) => {
8661 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
8662 }
8663 }
8664}
8665
8666fn eval_trigger_value(
8667 value: &TriggerValue,
8668 event: &WriteEvent,
8669 selected: Option<&TriggerRowImage>,
8670) -> Result<Value> {
8671 match value {
8672 TriggerValue::Literal(value) => Ok(value.clone()),
8673 TriggerValue::NewColumn(column_id) => event
8674 .new
8675 .as_ref()
8676 .and_then(|row| row.columns.get(column_id))
8677 .cloned()
8678 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
8679 TriggerValue::OldColumn(column_id) => event
8680 .old
8681 .as_ref()
8682 .and_then(|row| row.columns.get(column_id))
8683 .cloned()
8684 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
8685 TriggerValue::SelectedColumn(column_id) => selected
8686 .and_then(|row| row.columns.get(column_id))
8687 .cloned()
8688 .ok_or_else(|| {
8689 MongrelError::InvalidArgument("SELECTED column is not available".into())
8690 }),
8691 }
8692}
8693
8694fn values_equal(left: &Value, right: &Value) -> bool {
8695 match (left, right) {
8696 (Value::Null, Value::Null) => true,
8697 (Value::Bool(a), Value::Bool(b)) => a == b,
8698 (Value::Int64(a), Value::Int64(b)) => a == b,
8699 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
8700 (Value::Bytes(a), Value::Bytes(b)) => a == b,
8701 (Value::Embedding(a), Value::Embedding(b)) => {
8702 a.len() == b.len()
8703 && a.iter()
8704 .zip(b.iter())
8705 .all(|(a, b)| a.to_bits() == b.to_bits())
8706 }
8707 _ => false,
8708 }
8709}
8710
8711fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
8712 match (left, right) {
8713 (Value::Null, _) | (_, Value::Null) => None,
8714 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
8715 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
8716 (Value::Int64(a), Value::Float64(b)) => {
8719 let af = *a as f64;
8720 Some(af.total_cmp(b))
8721 }
8722 (Value::Float64(a), Value::Int64(b)) => {
8725 let bf = *b as f64;
8726 Some(a.total_cmp(&bf))
8727 }
8728 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
8729 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
8730 (Value::Embedding(_), Value::Embedding(_)) => None,
8731 _ => None,
8732 }
8733}
8734
8735fn trigger_message(value: Value) -> String {
8736 match value {
8737 Value::Null => "NULL".into(),
8738 Value::Bool(value) => value.to_string(),
8739 Value::Int64(value) => value.to_string(),
8740 Value::Float64(value) => value.to_string(),
8741 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
8742 Value::Embedding(value) => format!("{value:?}"),
8743 Value::Decimal(value) => value.to_string(),
8744 Value::Interval {
8745 months,
8746 days,
8747 nanos,
8748 } => format!("{months}m {days}d {nanos}ns"),
8749 Value::Uuid(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
8750 Value::Json(b) => String::from_utf8_lossy(&b).into_owned(),
8751 }
8752}
8753
8754fn validate_trigger_step<'a>(
8755 step: &TriggerStep,
8756 cat: &'a Catalog,
8757 target_schema: &Schema,
8758 event: TriggerEvent,
8759 select_schemas: &mut HashMap<String, &'a Schema>,
8760) -> Result<()> {
8761 match step {
8762 TriggerStep::SetNew { cells } => {
8763 if event == TriggerEvent::Delete {
8764 return Err(MongrelError::InvalidArgument(
8765 "SetNew trigger step is not valid for DELETE triggers".into(),
8766 ));
8767 }
8768 for cell in cells {
8769 validate_column_id(cell.column_id, target_schema)?;
8770 validate_trigger_value(&cell.value, target_schema, event)?;
8771 }
8772 }
8773 TriggerStep::Insert { table, cells } => {
8774 let schema = trigger_write_schema(cat, table, "insert")?;
8775 for cell in cells {
8776 validate_column_id(cell.column_id, schema)?;
8777 validate_trigger_value(&cell.value, target_schema, event)?;
8778 }
8779 }
8780 TriggerStep::UpdateByPk { table, pk, cells } => {
8781 let schema = trigger_write_schema(cat, table, "update")?;
8782 if schema.primary_key().is_none() {
8783 return Err(MongrelError::InvalidArgument(format!(
8784 "trigger update_by_pk references table {table:?} without a primary key"
8785 )));
8786 }
8787 validate_trigger_value(pk, target_schema, event)?;
8788 for cell in cells {
8789 validate_column_id(cell.column_id, schema)?;
8790 validate_trigger_value(&cell.value, target_schema, event)?;
8791 }
8792 }
8793 TriggerStep::DeleteByPk { table, pk } => {
8794 let schema = trigger_write_schema(cat, table, "delete")?;
8795 if schema.primary_key().is_none() {
8796 return Err(MongrelError::InvalidArgument(format!(
8797 "trigger delete_by_pk references table {table:?} without a primary key"
8798 )));
8799 }
8800 validate_trigger_value(pk, target_schema, event)?;
8801 }
8802 TriggerStep::Select {
8803 id,
8804 table,
8805 conditions,
8806 } => {
8807 let schema = trigger_read_schema(cat, table)?;
8808 for condition in conditions {
8809 validate_trigger_condition(condition, schema, target_schema, event)?;
8810 }
8811 if select_schemas.contains_key(id) {
8812 return Err(MongrelError::InvalidArgument(format!(
8813 "duplicate select id {id:?} in trigger program"
8814 )));
8815 }
8816 select_schemas.insert(id.clone(), schema);
8817 }
8818 TriggerStep::Foreach { id, steps } => {
8819 if !select_schemas.contains_key(id) {
8820 return Err(MongrelError::InvalidArgument(format!(
8821 "foreach references unknown select id {id:?}"
8822 )));
8823 }
8824 let mut inner_select_schemas = select_schemas.clone();
8825 for step in steps {
8826 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
8827 }
8828 }
8829 TriggerStep::DeleteWhere { table, conditions } => {
8830 let schema = trigger_write_schema(cat, table, "delete")?;
8831 for condition in conditions {
8832 validate_trigger_condition(condition, schema, target_schema, event)?;
8833 }
8834 }
8835 TriggerStep::UpdateWhere {
8836 table,
8837 conditions,
8838 cells,
8839 } => {
8840 let schema = trigger_write_schema(cat, table, "update")?;
8841 for condition in conditions {
8842 validate_trigger_condition(condition, schema, target_schema, event)?;
8843 }
8844 for cell in cells {
8845 validate_column_id(cell.column_id, schema)?;
8846 validate_trigger_value(&cell.value, target_schema, event)?;
8847 }
8848 }
8849 TriggerStep::Raise { message, .. } => {
8850 validate_trigger_value(message, target_schema, event)?
8851 }
8852 }
8853 Ok(())
8854}
8855
8856fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
8857 if let Some(entry) = cat.live(table) {
8858 return Ok(&entry.schema);
8859 }
8860 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
8861 let allowed = match op {
8862 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
8863 "update" | "delete" => entry.capabilities.writable,
8864 _ => false,
8865 };
8866 if !allowed {
8867 return Err(MongrelError::InvalidArgument(format!(
8868 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
8869 entry.module
8870 )));
8871 }
8872 if !entry.capabilities.transaction_safe {
8873 return Err(MongrelError::InvalidArgument(format!(
8874 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
8875 entry.module
8876 )));
8877 }
8878 return Ok(&entry.declared_schema);
8879 }
8880 Err(MongrelError::InvalidArgument(format!(
8881 "trigger references unknown table {table:?}"
8882 )))
8883}
8884
8885fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
8886 if let Some(entry) = cat.live(table) {
8887 return Ok(&entry.schema);
8888 }
8889 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
8890 if entry.capabilities.trigger_safe {
8891 return Ok(&entry.declared_schema);
8892 }
8893 return Err(MongrelError::InvalidArgument(format!(
8894 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
8895 entry.module
8896 )));
8897 }
8898 Err(MongrelError::InvalidArgument(format!(
8899 "trigger references unknown table {table:?}"
8900 )))
8901}
8902
8903fn validate_trigger_condition(
8904 condition: &TriggerCondition,
8905 schema: &Schema,
8906 target_schema: &Schema,
8907 event: TriggerEvent,
8908) -> Result<()> {
8909 match condition {
8910 TriggerCondition::Pk { value } => {
8911 if schema.primary_key().is_none() {
8912 return Err(MongrelError::InvalidArgument(
8913 "trigger condition Pk references a table without a primary key".into(),
8914 ));
8915 }
8916 validate_trigger_value(value, target_schema, event)
8917 }
8918 TriggerCondition::Eq { column_id, value }
8919 | TriggerCondition::NotEq { column_id, value }
8920 | TriggerCondition::Lt { column_id, value }
8921 | TriggerCondition::Lte { column_id, value }
8922 | TriggerCondition::Gt { column_id, value }
8923 | TriggerCondition::Gte { column_id, value } => {
8924 validate_column_id(*column_id, schema)?;
8925 validate_trigger_value(value, target_schema, event)
8926 }
8927 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
8928 validate_column_id(*column_id, schema)
8929 }
8930 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
8931 validate_trigger_condition(left, schema, target_schema, event)?;
8932 validate_trigger_condition(right, schema, target_schema, event)
8933 }
8934 TriggerCondition::Not(condition) => {
8935 validate_trigger_condition(condition, schema, target_schema, event)
8936 }
8937 }
8938}
8939
8940fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
8941 match expr {
8942 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
8943 validate_trigger_value(value, schema, event)
8944 }
8945 TriggerExpr::Eq { left, right }
8946 | TriggerExpr::NotEq { left, right }
8947 | TriggerExpr::Lt { left, right }
8948 | TriggerExpr::Lte { left, right }
8949 | TriggerExpr::Gt { left, right }
8950 | TriggerExpr::Gte { left, right } => {
8951 validate_trigger_value(left, schema, event)?;
8952 validate_trigger_value(right, schema, event)
8953 }
8954 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
8955 validate_trigger_expr(left, schema, event)?;
8956 validate_trigger_expr(right, schema, event)
8957 }
8958 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
8959 }
8960}
8961
8962fn validate_trigger_value(
8963 value: &TriggerValue,
8964 schema: &Schema,
8965 event: TriggerEvent,
8966) -> Result<()> {
8967 match value {
8968 TriggerValue::Literal(_) => Ok(()),
8969 TriggerValue::NewColumn(id) => {
8970 if event == TriggerEvent::Delete {
8971 return Err(MongrelError::InvalidArgument(
8972 "DELETE triggers cannot reference NEW".into(),
8973 ));
8974 }
8975 validate_column_id(*id, schema)
8976 }
8977 TriggerValue::OldColumn(id) => {
8978 if event == TriggerEvent::Insert {
8979 return Err(MongrelError::InvalidArgument(
8980 "INSERT triggers cannot reference OLD".into(),
8981 ));
8982 }
8983 validate_column_id(*id, schema)
8984 }
8985 TriggerValue::SelectedColumn(_) => Ok(()),
8989 }
8990}
8991
8992fn recover_ddl_from_wal(
8998 root: &Path,
8999 cat: &mut Catalog,
9000 meta_dek: Option<&[u8; META_DEK_LEN]>,
9001 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
9002) -> Result<()> {
9003 use crate::wal::{DdlOp, Op, SharedWal};
9004
9005 let records = match SharedWal::replay_with_dek(root, wal_dek) {
9006 Ok(r) => r,
9007 Err(_) => return Ok(()),
9008 };
9009
9010 let mut committed: HashMap<u64, u64> = HashMap::new();
9011 for r in &records {
9012 if let Op::TxnCommit { epoch: ce, .. } = r.op {
9013 committed.insert(r.txn_id, ce);
9014 }
9015 }
9016
9017 let mut changed = false;
9018 for r in records {
9019 let Some(&ce) = committed.get(&r.txn_id) else {
9020 continue;
9021 };
9022 match r.op {
9023 Op::Ddl(DdlOp::CreateTable {
9024 table_id,
9025 ref name,
9026 ref schema_json,
9027 }) => {
9028 if cat.tables.iter().any(|t| t.table_id == table_id) {
9029 continue;
9030 }
9031 let schema = DdlOp::decode_schema(schema_json)?;
9032 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
9033 if !tdir.exists() {
9034 std::fs::create_dir_all(tdir.join(crate::engine::WAL_DIR))?;
9035 std::fs::create_dir_all(tdir.join(crate::engine::RUNS_DIR))?;
9036 crate::engine::write_schema(&tdir, &schema)?;
9037 let mut m = crate::manifest::Manifest::new(table_id, schema.schema_id);
9043 crate::manifest::write_atomic(&tdir, &mut m, meta_dek)?;
9044 }
9045 cat.tables.push(CatalogEntry {
9046 table_id,
9047 name: name.clone(),
9048 schema,
9049 state: TableState::Live,
9050 created_epoch: ce,
9051 });
9052 cat.next_table_id = cat.next_table_id.max(table_id + 1);
9053 changed = true;
9054 }
9055 Op::Ddl(DdlOp::DropTable { table_id }) => {
9056 let mut dropped_name = None;
9057 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
9058 dropped_name = Some(entry.name.clone());
9059 if matches!(entry.state, TableState::Live) {
9060 entry.state = TableState::Dropped { at_epoch: ce };
9061 changed = true;
9062 }
9063 }
9064 if let Some(name) = dropped_name {
9065 let before = cat.materialized_views.len();
9066 cat.materialized_views
9067 .retain(|definition| definition.name != name);
9068 changed |= before != cat.materialized_views.len();
9069 cat.security.rls_tables.retain(|table| table != &name);
9070 cat.security.policies.retain(|policy| policy.table != name);
9071 cat.security.masks.retain(|mask| mask.table != name);
9072 for role in &mut cat.roles {
9073 role.permissions
9074 .retain(|permission| permission_table(permission) != Some(&name));
9075 }
9076 cat.security_version = cat.security_version.wrapping_add(1);
9077 }
9078 }
9079 Op::Ddl(DdlOp::RenameTable {
9080 table_id,
9081 ref new_name,
9082 }) => {
9083 let mut old_name = None;
9084 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
9085 if entry.name != *new_name {
9086 old_name = Some(entry.name.clone());
9087 entry.name = new_name.clone();
9088 changed = true;
9089 }
9090 }
9091 if let Some(old_name) = old_name {
9092 if let Some(definition) = cat
9093 .materialized_views
9094 .iter_mut()
9095 .find(|definition| definition.name == old_name)
9096 {
9097 definition.name = new_name.clone();
9098 }
9099 for table in &mut cat.security.rls_tables {
9100 if *table == old_name {
9101 *table = new_name.clone();
9102 }
9103 }
9104 for policy in &mut cat.security.policies {
9105 if policy.table == old_name {
9106 policy.table = new_name.clone();
9107 }
9108 }
9109 for mask in &mut cat.security.masks {
9110 if mask.table == old_name {
9111 mask.table = new_name.clone();
9112 }
9113 }
9114 for role in &mut cat.roles {
9115 for permission in &mut role.permissions {
9116 rename_permission_table(permission, &old_name, new_name);
9117 }
9118 }
9119 cat.security_version = cat.security_version.wrapping_add(1);
9120 }
9121 }
9125 Op::Ddl(DdlOp::AlterTable {
9126 table_id,
9127 ref column_json,
9128 }) => {
9129 let column = DdlOp::decode_column(column_json)?;
9130 let mut renamed = None;
9131 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
9132 renamed = entry
9133 .schema
9134 .columns
9135 .iter()
9136 .find(|existing| existing.id == column.id && existing.name != column.name)
9137 .map(|existing| {
9138 (
9139 entry.name.clone(),
9140 existing.name.clone(),
9141 column.name.clone(),
9142 )
9143 });
9144 if apply_recovered_column_def(&mut entry.schema, column) {
9145 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
9146 if tdir.exists() {
9147 crate::engine::write_schema(&tdir, &entry.schema)?;
9148 }
9149 changed = true;
9150 }
9151 }
9152 if let Some((table, old_name, new_name)) = renamed {
9153 for role in &mut cat.roles {
9154 for permission in &mut role.permissions {
9155 rename_permission_column(permission, &table, &old_name, &new_name);
9156 }
9157 }
9158 cat.security_version = cat.security_version.wrapping_add(1);
9159 }
9160 }
9161 Op::Ddl(DdlOp::SetTtl {
9162 table_id,
9163 ref policy_json,
9164 }) => {
9165 let policy = DdlOp::decode_ttl(policy_json)?;
9166 if let Some(policy) = policy {
9167 let valid = cat
9168 .tables
9169 .iter()
9170 .find(|entry| entry.table_id == table_id)
9171 .and_then(|entry| {
9172 entry
9173 .schema
9174 .columns
9175 .iter()
9176 .find(|column| column.id == policy.column_id)
9177 })
9178 .is_some_and(|column| {
9179 column.ty == TypeId::TimestampNanos
9180 && policy.duration_nanos > 0
9181 && policy.duration_nanos <= i64::MAX as u64
9182 });
9183 if !valid {
9184 return Err(MongrelError::Schema(format!(
9185 "invalid recovered TTL policy for table id {table_id}"
9186 )));
9187 }
9188 }
9189 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
9190 if tdir.exists() {
9191 let mut manifest = crate::manifest::read(&tdir, meta_dek)?;
9192 if manifest.ttl != policy || manifest.current_epoch < ce {
9193 manifest.ttl = policy;
9194 manifest.current_epoch = manifest.current_epoch.max(ce);
9195 crate::manifest::write_atomic(&tdir, &mut manifest, meta_dek)?;
9196 }
9197 }
9198 }
9199 Op::Ddl(DdlOp::SetMaterializedView {
9200 ref name,
9201 ref definition_json,
9202 }) => {
9203 let definition = DdlOp::decode_materialized_view(definition_json)?;
9204 if definition.name != *name {
9205 return Err(MongrelError::Schema(format!(
9206 "materialized view WAL name mismatch: {name:?}"
9207 )));
9208 }
9209 if cat.live(name).is_some() {
9210 if let Some(existing) = cat
9211 .materialized_views
9212 .iter_mut()
9213 .find(|existing| existing.name == *name)
9214 {
9215 if *existing != definition {
9216 *existing = definition;
9217 changed = true;
9218 }
9219 } else {
9220 cat.materialized_views.push(definition);
9221 changed = true;
9222 }
9223 }
9224 }
9225 Op::Ddl(DdlOp::SetSecurityCatalog { ref security_json }) => {
9226 let security = DdlOp::decode_security(security_json)?;
9227 validate_security_catalog(cat, &security)?;
9228 if cat.security != security {
9229 cat.security = security;
9230 cat.security_version = cat.security_version.wrapping_add(1);
9231 changed = true;
9232 }
9233 }
9234 _ => {}
9235 }
9236 }
9237
9238 if changed {
9239 catalog::write_atomic(root, cat, meta_dek)?;
9240 }
9241 Ok(())
9242}
9243
9244fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> bool {
9245 match schema.columns.iter_mut().find(|c| c.id == column.id) {
9246 Some(existing) if *existing == column => false,
9247 Some(existing) => {
9248 *existing = column;
9249 schema.schema_id = schema.schema_id.saturating_add(1);
9250 true
9251 }
9252 None => {
9253 schema.columns.push(column);
9254 schema.schema_id = schema.schema_id.saturating_add(1);
9255 true
9256 }
9257 }
9258}
9259
9260fn permission_table(permission: &crate::auth::Permission) -> Option<&str> {
9261 use crate::auth::Permission;
9262 match permission {
9263 Permission::Select { table }
9264 | Permission::Insert { table }
9265 | Permission::Update { table }
9266 | Permission::Delete { table }
9267 | Permission::SelectColumns { table, .. }
9268 | Permission::InsertColumns { table, .. }
9269 | Permission::UpdateColumns { table, .. } => Some(table),
9270 Permission::All | Permission::Ddl | Permission::Admin => None,
9271 }
9272}
9273
9274fn rename_permission_table(permission: &mut crate::auth::Permission, old: &str, new: &str) {
9275 use crate::auth::Permission;
9276 let table = match permission {
9277 Permission::Select { table }
9278 | Permission::Insert { table }
9279 | Permission::Update { table }
9280 | Permission::Delete { table }
9281 | Permission::SelectColumns { table, .. }
9282 | Permission::InsertColumns { table, .. }
9283 | Permission::UpdateColumns { table, .. } => Some(table),
9284 Permission::All | Permission::Ddl | Permission::Admin => None,
9285 };
9286 if let Some(table) = table.filter(|table| table.as_str() == old) {
9287 *table = new.to_string();
9288 }
9289}
9290
9291fn rename_permission_column(
9292 permission: &mut crate::auth::Permission,
9293 target_table: &str,
9294 old: &str,
9295 new: &str,
9296) {
9297 use crate::auth::Permission;
9298 let columns = match permission {
9299 Permission::SelectColumns { table, columns }
9300 | Permission::InsertColumns { table, columns }
9301 | Permission::UpdateColumns { table, columns }
9302 if table == target_table =>
9303 {
9304 Some(columns)
9305 }
9306 _ => None,
9307 };
9308 if let Some(column) = columns
9309 .into_iter()
9310 .flatten()
9311 .find(|column| column.as_str() == old)
9312 {
9313 *column = new.to_string();
9314 }
9315}
9316
9317fn merge_permission(
9318 permissions: &mut Vec<crate::auth::Permission>,
9319 permission: crate::auth::Permission,
9320) {
9321 use crate::auth::Permission;
9322 let (kind, table, mut columns) = match permission {
9323 Permission::SelectColumns { table, columns } => (0, table, columns),
9324 Permission::InsertColumns { table, columns } => (1, table, columns),
9325 Permission::UpdateColumns { table, columns } => (2, table, columns),
9326 permission if !permissions.contains(&permission) => {
9327 permissions.push(permission);
9328 return;
9329 }
9330 _ => return,
9331 };
9332 for permission in permissions.iter_mut() {
9333 let existing = match permission {
9334 Permission::SelectColumns {
9335 table: existing_table,
9336 columns,
9337 } if kind == 0 && existing_table == &table => Some(columns),
9338 Permission::InsertColumns {
9339 table: existing_table,
9340 columns,
9341 } if kind == 1 && existing_table == &table => Some(columns),
9342 Permission::UpdateColumns {
9343 table: existing_table,
9344 columns,
9345 } if kind == 2 && existing_table == &table => Some(columns),
9346 _ => None,
9347 };
9348 if let Some(existing) = existing {
9349 existing.append(&mut columns);
9350 existing.sort();
9351 existing.dedup();
9352 return;
9353 }
9354 }
9355 columns.sort();
9356 columns.dedup();
9357 permissions.push(match kind {
9358 0 => Permission::SelectColumns { table, columns },
9359 1 => Permission::InsertColumns { table, columns },
9360 2 => Permission::UpdateColumns { table, columns },
9361 _ => unreachable!(),
9362 });
9363}
9364
9365fn revoke_permission_from(
9366 permissions: &mut Vec<crate::auth::Permission>,
9367 revoked: &crate::auth::Permission,
9368) {
9369 use crate::auth::Permission;
9370 let revoked_columns = match revoked {
9371 Permission::SelectColumns { table, columns } => Some((0, table, columns)),
9372 Permission::InsertColumns { table, columns } => Some((1, table, columns)),
9373 Permission::UpdateColumns { table, columns } => Some((2, table, columns)),
9374 _ => None,
9375 };
9376 let Some((kind, table, columns)) = revoked_columns else {
9377 permissions.retain(|permission| permission != revoked);
9378 return;
9379 };
9380 for permission in permissions.iter_mut() {
9381 let current = match permission {
9382 Permission::SelectColumns {
9383 table: current_table,
9384 columns,
9385 } if kind == 0 && current_table == table => Some(columns),
9386 Permission::InsertColumns {
9387 table: current_table,
9388 columns,
9389 } if kind == 1 && current_table == table => Some(columns),
9390 Permission::UpdateColumns {
9391 table: current_table,
9392 columns,
9393 } if kind == 2 && current_table == table => Some(columns),
9394 _ => None,
9395 };
9396 if let Some(current) = current {
9397 current.retain(|column| !columns.contains(column));
9398 }
9399 }
9400 permissions.retain(|permission| match permission {
9401 Permission::SelectColumns { columns, .. }
9402 | Permission::InsertColumns { columns, .. }
9403 | Permission::UpdateColumns { columns, .. } => !columns.is_empty(),
9404 _ => true,
9405 });
9406}
9407
9408fn validate_security_catalog(
9409 catalog: &Catalog,
9410 security: &crate::security::SecurityCatalog,
9411) -> Result<()> {
9412 let mut policy_names = HashSet::new();
9413 for table in &security.rls_tables {
9414 if catalog.live(table).is_none() {
9415 return Err(MongrelError::NotFound(format!(
9416 "RLS table {table:?} not found"
9417 )));
9418 }
9419 }
9420 for policy in &security.policies {
9421 if !policy_names.insert((policy.table.clone(), policy.name.clone())) {
9422 return Err(MongrelError::InvalidArgument(format!(
9423 "duplicate policy {:?} on {:?}",
9424 policy.name, policy.table
9425 )));
9426 }
9427 let schema = &catalog
9428 .live(&policy.table)
9429 .ok_or_else(|| {
9430 MongrelError::NotFound(format!("policy table {:?} not found", policy.table))
9431 })?
9432 .schema;
9433 if let Some(expression) = &policy.using {
9434 validate_security_expression(expression, schema)?;
9435 }
9436 if let Some(expression) = &policy.with_check {
9437 validate_security_expression(expression, schema)?;
9438 }
9439 }
9440 let mut mask_names = HashSet::new();
9441 for mask in &security.masks {
9442 if !mask_names.insert((mask.table.clone(), mask.name.clone())) {
9443 return Err(MongrelError::InvalidArgument(format!(
9444 "duplicate mask {:?} on {:?}",
9445 mask.name, mask.table
9446 )));
9447 }
9448 let column = catalog
9449 .live(&mask.table)
9450 .and_then(|entry| {
9451 entry
9452 .schema
9453 .columns
9454 .iter()
9455 .find(|column| column.id == mask.column)
9456 })
9457 .ok_or_else(|| {
9458 MongrelError::NotFound(format!(
9459 "mask column {} on {:?} not found",
9460 mask.column, mask.table
9461 ))
9462 })?;
9463 if matches!(
9464 mask.strategy,
9465 crate::security::MaskStrategy::Redact { .. } | crate::security::MaskStrategy::Sha256
9466 ) && !matches!(column.ty, TypeId::Bytes | TypeId::Enum { .. })
9467 {
9468 return Err(MongrelError::InvalidArgument(format!(
9469 "mask {:?} requires a string/bytes column",
9470 mask.name
9471 )));
9472 }
9473 }
9474 Ok(())
9475}
9476
9477fn validate_security_expression(
9478 expression: &crate::security::SecurityExpr,
9479 schema: &Schema,
9480) -> Result<()> {
9481 use crate::security::SecurityExpr;
9482 match expression {
9483 SecurityExpr::True => Ok(()),
9484 SecurityExpr::ColumnEqCurrentUser { column }
9485 | SecurityExpr::ColumnEqValue { column, .. } => {
9486 if schema
9487 .columns
9488 .iter()
9489 .any(|candidate| candidate.id == *column)
9490 {
9491 Ok(())
9492 } else {
9493 Err(MongrelError::InvalidArgument(format!(
9494 "security expression references unknown column id {column}"
9495 )))
9496 }
9497 }
9498 SecurityExpr::And { left, right } | SecurityExpr::Or { left, right } => {
9499 validate_security_expression(left, schema)?;
9500 validate_security_expression(right, schema)
9501 }
9502 SecurityExpr::Not { expression } => validate_security_expression(expression, schema),
9503 }
9504}
9505
9506fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
9511 for entry in &cat.tables {
9512 let txn_dir = root
9513 .join(TABLES_DIR)
9514 .join(entry.table_id.to_string())
9515 .join("_txn");
9516 if txn_dir.exists() {
9517 let _ = std::fs::remove_dir_all(&txn_dir);
9518 }
9519 }
9520}
9521
9522#[cfg(test)]
9523mod trigger_engine_tests {
9524 use super::*;
9525
9526 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
9527 WriteEvent {
9528 table: "test".into(),
9529 kind: TriggerEvent::Insert,
9530 new: Some(TriggerRowImage {
9531 columns: new_cells.iter().cloned().collect(),
9532 }),
9533 old: Some(TriggerRowImage {
9534 columns: old_cells.iter().cloned().collect(),
9535 }),
9536 changed_columns: Vec::new(),
9537 op_indices: Vec::new(),
9538 put_idx: None,
9539 trigger_stack: Vec::new(),
9540 }
9541 }
9542
9543 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
9544 WriteEvent {
9545 table: "test".into(),
9546 kind: TriggerEvent::Insert,
9547 new: Some(TriggerRowImage {
9548 columns: new_cells.iter().cloned().collect(),
9549 }),
9550 old: None,
9551 changed_columns: Vec::new(),
9552 op_indices: Vec::new(),
9553 put_idx: None,
9554 trigger_stack: Vec::new(),
9555 }
9556 }
9557
9558 #[test]
9559 fn value_order_int64_vs_float64() {
9560 assert_eq!(
9561 value_order(&Value::Int64(5), &Value::Float64(5.0)),
9562 Some(std::cmp::Ordering::Equal)
9563 );
9564 assert_eq!(
9565 value_order(&Value::Int64(5), &Value::Float64(3.0)),
9566 Some(std::cmp::Ordering::Greater)
9567 );
9568 assert_eq!(
9569 value_order(&Value::Int64(2), &Value::Float64(3.0)),
9570 Some(std::cmp::Ordering::Less)
9571 );
9572 }
9573
9574 #[test]
9575 fn value_order_null_returns_none() {
9576 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
9577 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
9578 assert_eq!(value_order(&Value::Null, &Value::Null), None);
9579 }
9580
9581 #[test]
9582 fn value_order_cross_group_returns_none() {
9583 assert_eq!(
9584 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
9585 None
9586 );
9587 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
9588 assert_eq!(
9589 value_order(
9590 &Value::Embedding(vec![1.0, 2.0]),
9591 &Value::Embedding(vec![1.0, 2.0])
9592 ),
9593 None
9594 );
9595 }
9596
9597 #[test]
9598 fn eval_trigger_expr_ranges_and_booleans() {
9599 let expr = TriggerExpr::And {
9600 left: Box::new(TriggerExpr::Gt {
9601 left: TriggerValue::NewColumn(1),
9602 right: TriggerValue::Literal(Value::Int64(0)),
9603 }),
9604 right: Box::new(TriggerExpr::Lte {
9605 left: TriggerValue::NewColumn(1),
9606 right: TriggerValue::Literal(Value::Int64(100)),
9607 }),
9608 };
9609 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
9610 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
9611 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
9612
9613 let or_expr = TriggerExpr::Or {
9614 left: Box::new(TriggerExpr::Lt {
9615 left: TriggerValue::NewColumn(1),
9616 right: TriggerValue::Literal(Value::Int64(0)),
9617 }),
9618 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
9619 TriggerValue::OldColumn(2),
9620 )))),
9621 };
9622 assert!(eval_trigger_expr(
9623 &or_expr,
9624 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
9625 )
9626 .unwrap());
9627 assert!(!eval_trigger_expr(
9628 &or_expr,
9629 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
9630 )
9631 .unwrap());
9632
9633 assert!(eval_trigger_expr(
9634 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
9635 &event_insert(&[])
9636 )
9637 .unwrap());
9638 assert!(!eval_trigger_expr(
9639 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
9640 &event_insert(&[])
9641 )
9642 .unwrap());
9643 assert!(!eval_trigger_expr(
9644 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
9645 &event_insert(&[])
9646 )
9647 .unwrap());
9648 }
9649}