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