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