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 rows_for(
1542 &self,
1543 table: &str,
1544 principal: Option<&crate::auth::Principal>,
1545 ) -> Result<Vec<crate::memtable::Row>> {
1546 let allowed = self.select_column_ids_for(table, principal)?;
1547 let handle = self.table(table)?;
1548 let rows = {
1549 let table = handle.lock();
1550 table.visible_rows(table.snapshot())?
1551 };
1552 let mut rows = self.secure_rows_for(table, rows, principal)?;
1553 for row in &mut rows {
1554 row.columns.retain(|column, _| allowed.contains(column));
1555 }
1556 Ok(rows)
1557 }
1558
1559 pub fn count_for(
1561 &self,
1562 table: &str,
1563 principal: Option<&crate::auth::Principal>,
1564 ) -> Result<u64> {
1565 self.select_column_ids_for(table, principal)?;
1566 if self.security_active_for(table) {
1567 Ok(self.rows_for(table, principal)?.len() as u64)
1568 } else {
1569 Ok(self.table(table)?.lock().count())
1570 }
1571 }
1572
1573 pub fn put_for(
1575 &self,
1576 table: &str,
1577 mut cells: Vec<(u16, crate::memtable::Value)>,
1578 principal: Option<&crate::auth::Principal>,
1579 ) -> Result<RowId> {
1580 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
1581 self.require_columns_for(
1582 table,
1583 crate::auth::ColumnOperation::Insert,
1584 &columns,
1585 principal,
1586 )?;
1587 let handle = self.table(table)?;
1588 let mut table_handle = handle.lock();
1589 table_handle.fill_auto_inc(&mut cells)?;
1590 table_handle.apply_defaults(&mut cells)?;
1591 let mut row = crate::memtable::Row::new(RowId(0), self.epoch.visible());
1592 row.columns.extend(cells.iter().cloned());
1593 self.check_row_policy_for(
1594 table,
1595 crate::security::PolicyCommand::Insert,
1596 &row,
1597 true,
1598 principal,
1599 )?;
1600 table_handle.put(cells)
1601 }
1602
1603 pub fn check_row_policy_for(
1604 &self,
1605 table: &str,
1606 command: crate::security::PolicyCommand,
1607 row: &crate::memtable::Row,
1608 check_new: bool,
1609 principal: Option<&crate::auth::Principal>,
1610 ) -> Result<()> {
1611 let security = self.catalog.read().security.clone();
1612 if !security.rls_enabled(table) {
1613 return Ok(());
1614 }
1615 let cached = self.principal.read().clone();
1616 let principal = principal
1617 .or(cached.as_ref())
1618 .ok_or(MongrelError::AuthRequired)?;
1619 if security.row_allowed(table, command, row, principal, check_new) {
1620 return Ok(());
1621 }
1622 let required = match command {
1623 crate::security::PolicyCommand::Insert => crate::auth::Permission::Insert {
1624 table: table.to_string(),
1625 },
1626 crate::security::PolicyCommand::Update => crate::auth::Permission::Update {
1627 table: table.to_string(),
1628 },
1629 crate::security::PolicyCommand::Select => crate::auth::Permission::Select {
1630 table: table.to_string(),
1631 },
1632 crate::security::PolicyCommand::Delete | crate::security::PolicyCommand::All => {
1633 crate::auth::Permission::Delete {
1634 table: table.to_string(),
1635 }
1636 }
1637 };
1638 Err(MongrelError::PermissionDenied {
1639 required,
1640 principal: principal.username.clone(),
1641 })
1642 }
1643
1644 pub fn set_materialized_view(
1647 &self,
1648 definition: crate::catalog::MaterializedViewEntry,
1649 ) -> Result<()> {
1650 use crate::wal::DdlOp;
1651 use std::sync::atomic::Ordering;
1652
1653 self.require(&crate::auth::Permission::Ddl)?;
1654 if self.poisoned.load(Ordering::Relaxed) {
1655 return Err(MongrelError::Other(
1656 "database poisoned by fsync error".into(),
1657 ));
1658 }
1659 if definition.name.is_empty() || definition.query.trim().is_empty() {
1660 return Err(MongrelError::InvalidArgument(
1661 "materialized view name and query must not be empty".into(),
1662 ));
1663 }
1664
1665 let _ddl = self.ddl_lock.lock();
1666 let table_id = self
1667 .catalog
1668 .read()
1669 .live(&definition.name)
1670 .ok_or_else(|| {
1671 MongrelError::NotFound(format!(
1672 "materialized view table {:?} not found",
1673 definition.name
1674 ))
1675 })?
1676 .table_id;
1677 let definition_json = DdlOp::encode_materialized_view(&definition)?;
1678 let _commit = self.commit_lock.lock();
1679 let epoch = self.epoch.bump_assigned();
1680 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
1681 let txn_id = self.alloc_txn_id();
1682 let commit_seq = {
1683 let mut wal = self.shared_wal.lock();
1684 wal.append(
1685 txn_id,
1686 table_id,
1687 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
1688 name: definition.name.clone(),
1689 definition_json,
1690 }),
1691 )?;
1692 wal.append_commit(txn_id, epoch, &[])?
1693 };
1694 self.group
1695 .await_durable(&self.shared_wal, commit_seq)
1696 .inspect_err(|_| {
1697 self.poisoned.store(true, Ordering::Relaxed);
1698 })?;
1699
1700 {
1701 let mut catalog = self.catalog.write();
1702 if let Some(existing) = catalog
1703 .materialized_views
1704 .iter_mut()
1705 .find(|existing| existing.name == definition.name)
1706 {
1707 *existing = definition;
1708 } else {
1709 catalog.materialized_views.push(definition);
1710 }
1711 }
1712 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
1713 self.epoch.publish_in_order(epoch);
1714 epoch_guard.disarm();
1715 Ok(())
1716 }
1717
1718 pub fn root(&self) -> &Path {
1720 &self.root
1721 }
1722
1723 pub fn is_read_only_replica(&self) -> bool {
1724 self.read_only
1725 }
1726
1727 pub fn set_replication_wal_retention_segments(&self, segments: usize) {
1728 self.replication_wal_retention_segments
1729 .store(segments, std::sync::atomic::Ordering::Relaxed);
1730 }
1731
1732 pub fn replication_snapshot(&self) -> Result<crate::replication::ReplicationSnapshot> {
1737 let _barrier = self.replication_barrier.write();
1738 let _ddl = self.ddl_lock.lock();
1739 let mut handles: Vec<_> = self
1740 .tables
1741 .read()
1742 .iter()
1743 .map(|(id, handle)| (*id, Arc::clone(handle)))
1744 .collect();
1745 handles.sort_by_key(|(id, _)| *id);
1746 let _table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
1747 let _commit = self.commit_lock.lock();
1748 let mut wal = self.shared_wal.lock();
1749 wal.group_sync()?;
1750 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
1751 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
1752 let epoch = records
1753 .iter()
1754 .filter_map(|record| match &record.op {
1755 crate::wal::Op::TxnCommit { epoch, .. } => Some(*epoch),
1756 _ => None,
1757 })
1758 .max()
1759 .unwrap_or(0)
1760 .max(self.visible_epoch().0);
1761 let files = crate::replication::capture_files(&self.root)?;
1762 drop(wal);
1763 Ok(crate::replication::ReplicationSnapshot::new(epoch, files))
1764 }
1765
1766 pub fn hot_backup(&self, destination: impl AsRef<Path>) -> Result<crate::backup::BackupReport> {
1774 self.require(&crate::auth::Permission::Ddl)?;
1775 let (destination, parent, stage) =
1776 prepare_backup_destination(&self.root, destination.as_ref())?;
1777 std::fs::create_dir(&stage)?;
1778
1779 let outcome = (|| {
1780 let barrier = self.replication_barrier.write();
1781 let ddl = self.ddl_lock.lock();
1782 let mut handles: Vec<_> = self
1783 .tables
1784 .read()
1785 .iter()
1786 .map(|(id, handle)| (*id, Arc::clone(handle)))
1787 .collect();
1788 handles.sort_by_key(|(id, _)| *id);
1789 let table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
1790 let commit = self.commit_lock.lock();
1791 let mut wal = self.shared_wal.lock();
1792 wal.group_sync()?;
1793 let epoch = self.visible_epoch().0;
1794
1795 let pin_nonce = std::time::SystemTime::now()
1796 .duration_since(std::time::UNIX_EPOCH)
1797 .unwrap_or_default()
1798 .as_nanos();
1799 let file_pin_root = self
1800 .root
1801 .join(META_DIR)
1802 .join("backup-pins")
1803 .join(format!("{}-{pin_nonce}", std::process::id()));
1804 std::fs::create_dir_all(&file_pin_root)?;
1805 let _file_pins = BackupFilePins {
1806 root: file_pin_root.clone(),
1807 };
1808 let mut run_files = Vec::new();
1809 for (index, (table_id, _)) in handles.iter().enumerate() {
1810 let table = &table_guards[index];
1811 for run in table.run_refs() {
1812 let source = table.runs_dir().join(format!("r-{}.sr", run.run_id));
1813 let relative = source
1814 .strip_prefix(&self.root)
1815 .map_err(|error| MongrelError::Other(format!("backup run path: {error}")))?
1816 .to_path_buf();
1817 let pinned = file_pin_root.join(format!("{table_id}-{}.sr", run.run_id));
1818 if std::fs::hard_link(&source, &pinned).is_err() {
1819 crate::backup::copy_file_synced(&source, &pinned)?;
1820 }
1821 run_files.push(((*table_id, run.run_id), pinned, relative));
1822 }
1823 }
1824 std::fs::File::open(&file_pin_root)?.sync_all()?;
1825 let run_keys: Vec<_> = run_files.iter().map(|(key, _, _)| *key).collect();
1826 {
1827 let mut pins = self.backup_pins.lock();
1828 for key in &run_keys {
1829 *pins.entry(*key).or_insert(0) += 1;
1830 }
1831 }
1832 let _run_pins = BackupRunPins {
1833 pins: &self.backup_pins,
1834 runs: run_keys,
1835 };
1836 let deferred: HashSet<_> = run_files
1837 .iter()
1838 .map(|(_, _, relative)| relative.clone())
1839 .collect();
1840 let mut copied = Vec::new();
1841 copy_backup_boundary(&self.root, &stage, &deferred, &mut copied)?;
1842
1843 drop(wal);
1844 drop(commit);
1845 drop(table_guards);
1846 drop(ddl);
1847 drop(barrier);
1848
1849 if let Some(hook) = self.backup_hook.lock().as_ref() {
1850 hook();
1851 }
1852 for (_, source, relative) in run_files {
1853 crate::backup::copy_file_synced(&source, &stage.join(&relative))?;
1854 copied.push(relative);
1855 }
1856
1857 let manifest = crate::backup::BackupManifest::create(&stage, epoch, &copied)?;
1858 manifest.write(&stage)?;
1859 crate::backup::sync_directories(&stage)?;
1860 if destination.exists() {
1861 return Err(MongrelError::Conflict(format!(
1862 "backup destination already exists: {}",
1863 destination.display()
1864 )));
1865 }
1866 std::fs::rename(&stage, &destination)?;
1867 std::fs::File::open(&parent)?.sync_all()?;
1868 Ok(crate::backup::BackupReport {
1869 destination,
1870 epoch,
1871 files: manifest.files.len(),
1872 bytes: manifest.total_bytes(),
1873 })
1874 })();
1875
1876 if outcome.is_err() && stage.exists() {
1877 let _ = std::fs::remove_dir_all(&stage);
1878 }
1879 outcome
1880 }
1881
1882 pub fn replication_batch_since(
1885 &self,
1886 since_epoch: u64,
1887 ) -> Result<crate::replication::ReplicationBatch> {
1888 use crate::wal::Op;
1889
1890 let mut wal = self.shared_wal.lock();
1891 wal.group_sync()?;
1892 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
1893 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
1894 drop(wal);
1895
1896 let commits: HashMap<u64, u64> = records
1897 .iter()
1898 .filter_map(|record| match &record.op {
1899 Op::TxnCommit { epoch, .. } => Some((record.txn_id, *epoch)),
1900 _ => None,
1901 })
1902 .collect();
1903 let earliest_epoch = commits.values().copied().min();
1904 let current_epoch = commits
1905 .values()
1906 .copied()
1907 .max()
1908 .unwrap_or(0)
1909 .max(self.visible_epoch().0);
1910 let selected: HashSet<u64> = commits
1911 .iter()
1912 .filter_map(|(txn_id, epoch)| (*epoch > since_epoch).then_some(*txn_id))
1913 .collect();
1914 let retention_gap = since_epoch < current_epoch
1915 && earliest_epoch.map_or(true, |epoch| epoch > since_epoch.saturating_add(1));
1916 let spilled = records.iter().any(|record| {
1917 selected.contains(&record.txn_id)
1918 && matches!(
1919 &record.op,
1920 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty()
1921 )
1922 });
1923 let records = records
1924 .into_iter()
1925 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
1926 .filter(|record| selected.contains(&record.txn_id))
1927 .collect();
1928 Ok(crate::replication::ReplicationBatch {
1929 current_epoch,
1930 earliest_epoch,
1931 requires_snapshot: retention_gap || spilled,
1932 records,
1933 })
1934 }
1935
1936 pub fn append_replication_batch(&self, records: &[crate::wal::Record]) -> Result<u64> {
1940 use crate::wal::Op;
1941
1942 if !self.read_only {
1943 return Err(MongrelError::InvalidArgument(
1944 "replication batches may only target a marked replica".into(),
1945 ));
1946 }
1947 let current = crate::replication::replica_epoch(&self.root)?;
1948 let mut commits = HashMap::new();
1949 let mut commit_timestamps = HashMap::new();
1950 for record in records {
1951 match &record.op {
1952 Op::TxnCommit { epoch, added_runs } => {
1953 if !added_runs.is_empty() {
1954 return Err(MongrelError::Conflict(
1955 "replication snapshot required for spilled-run transaction".into(),
1956 ));
1957 }
1958 if commits.insert(record.txn_id, *epoch).is_some() {
1959 return Err(MongrelError::InvalidArgument(format!(
1960 "duplicate commit for replication transaction {}",
1961 record.txn_id
1962 )));
1963 }
1964 }
1965 Op::CommitTimestamp { unix_nanos } => {
1966 commit_timestamps.insert(record.txn_id, *unix_nanos);
1967 }
1968 _ => {}
1969 }
1970 }
1971 for record in records {
1972 if record.txn_id != crate::wal::SYSTEM_TXN_ID
1973 && !matches!(&record.op, Op::TxnAbort)
1974 && !commits.contains_key(&record.txn_id)
1975 {
1976 return Err(MongrelError::InvalidArgument(format!(
1977 "incomplete replication transaction {}",
1978 record.txn_id
1979 )));
1980 }
1981 }
1982 let target_epoch = commits
1983 .values()
1984 .copied()
1985 .filter(|epoch| *epoch > current)
1986 .max()
1987 .unwrap_or(current);
1988 let mut selected: HashSet<u64> = commits
1989 .iter()
1990 .filter_map(|(txn_id, epoch)| (*epoch > current).then_some(*txn_id))
1991 .collect();
1992 if selected.is_empty() {
1993 return Ok(current);
1994 }
1995 let mut wal = self.shared_wal.lock();
1996 wal.group_sync()?;
1997 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
1998 let existing: HashSet<(u64, u64)> =
1999 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
2000 .into_iter()
2001 .filter_map(|record| match record.op {
2002 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
2003 _ => None,
2004 })
2005 .collect();
2006 selected.retain(|txn_id| {
2007 commits
2008 .get(txn_id)
2009 .is_some_and(|epoch| !existing.contains(&(*txn_id, *epoch)))
2010 });
2011 for record in records {
2012 if !selected.contains(&record.txn_id) {
2013 continue;
2014 }
2015 match &record.op {
2016 Op::TxnCommit { epoch, added_runs } => {
2017 let timestamp = commit_timestamps
2018 .get(&record.txn_id)
2019 .copied()
2020 .unwrap_or_else(current_unix_nanos);
2021 wal.append_commit_at(record.txn_id, Epoch(*epoch), added_runs, timestamp)?;
2022 }
2023 Op::TxnAbort | Op::Flush { .. } | Op::CommitTimestamp { .. } => {}
2024 op => {
2025 wal.append(record.txn_id, 0, op.clone())?;
2026 }
2027 }
2028 }
2029 if !selected.is_empty() {
2030 wal.group_sync()?;
2031 }
2032 Ok(target_epoch)
2033 }
2034
2035 pub fn table_id(&self, name: &str) -> Result<u64> {
2038 let cat = self.catalog.read();
2039 cat.live(name)
2040 .map(|e| e.table_id)
2041 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
2042 }
2043
2044 pub fn procedures(&self) -> Vec<StoredProcedure> {
2045 self.catalog
2046 .read()
2047 .procedures
2048 .iter()
2049 .map(|p| p.procedure.clone())
2050 .collect()
2051 }
2052
2053 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
2054 self.catalog
2055 .read()
2056 .procedures
2057 .iter()
2058 .find(|p| p.procedure.name == name)
2059 .map(|p| p.procedure.clone())
2060 }
2061
2062 pub fn create_procedure(&self, mut procedure: StoredProcedure) -> Result<StoredProcedure> {
2063 self.require(&crate::auth::Permission::Ddl)?;
2064 let _g = self.ddl_lock.lock();
2065 procedure.validate()?;
2066 self.validate_procedure_references(&procedure)?;
2067 {
2068 let cat = self.catalog.read();
2069 if cat
2070 .procedures
2071 .iter()
2072 .any(|p| p.procedure.name == procedure.name)
2073 {
2074 return Err(MongrelError::InvalidArgument(format!(
2075 "procedure {:?} already exists",
2076 procedure.name
2077 )));
2078 }
2079 }
2080 let commit_lock = Arc::clone(&self.commit_lock);
2081 let _c = commit_lock.lock();
2082 let epoch = self.epoch.bump_assigned();
2083 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2084 procedure.created_epoch = epoch.0;
2085 procedure.updated_epoch = epoch.0;
2086 {
2087 let mut cat = self.catalog.write();
2088 cat.procedures.push(ProcedureEntry::from(procedure.clone()));
2089 cat.db_epoch = epoch.0;
2090 }
2091 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2092 self.epoch.publish_in_order(epoch);
2093 _epoch_guard.disarm();
2094 Ok(procedure)
2095 }
2096
2097 pub fn create_or_replace_procedure(
2098 &self,
2099 procedure: StoredProcedure,
2100 ) -> Result<StoredProcedure> {
2101 let _g = self.ddl_lock.lock();
2102 procedure.validate()?;
2103 self.validate_procedure_references(&procedure)?;
2104 let commit_lock = Arc::clone(&self.commit_lock);
2105 let _c = commit_lock.lock();
2106 let epoch = self.epoch.bump_assigned();
2107 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2108 let replaced = {
2109 let mut cat = self.catalog.write();
2110 let next = match cat
2111 .procedures
2112 .iter()
2113 .position(|p| p.procedure.name == procedure.name)
2114 {
2115 Some(idx) => {
2116 let next = cat.procedures[idx]
2117 .procedure
2118 .replaced(procedure.clone(), epoch.0)?;
2119 cat.procedures[idx] = ProcedureEntry::from(next.clone());
2120 next
2121 }
2122 None => {
2123 let mut next = procedure;
2124 next.created_epoch = epoch.0;
2125 next.updated_epoch = epoch.0;
2126 cat.procedures.push(ProcedureEntry::from(next.clone()));
2127 next
2128 }
2129 };
2130 cat.db_epoch = epoch.0;
2131 next
2132 };
2133 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2134 self.epoch.publish_in_order(epoch);
2135 _epoch_guard.disarm();
2136 Ok(replaced)
2137 }
2138
2139 pub fn drop_procedure(&self, name: &str) -> Result<()> {
2140 self.require(&crate::auth::Permission::Ddl)?;
2141 let _g = self.ddl_lock.lock();
2142 let commit_lock = Arc::clone(&self.commit_lock);
2143 let _c = commit_lock.lock();
2144 let epoch = self.epoch.bump_assigned();
2145 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2146 {
2147 let mut cat = self.catalog.write();
2148 let before = cat.procedures.len();
2149 cat.procedures.retain(|p| p.procedure.name != name);
2150 if cat.procedures.len() == before {
2151 return Err(MongrelError::NotFound(format!(
2152 "procedure {name:?} not found"
2153 )));
2154 }
2155 cat.db_epoch = epoch.0;
2156 }
2157 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2158 self.epoch.publish_in_order(epoch);
2159 _epoch_guard.disarm();
2160 Ok(())
2161 }
2162
2163 pub fn users(&self) -> Vec<crate::auth::UserEntry> {
2168 self.catalog.read().users.clone()
2169 }
2170
2171 pub fn roles(&self) -> Vec<crate::auth::RoleEntry> {
2173 self.catalog.read().roles.clone()
2174 }
2175
2176 pub fn create_user(&self, username: &str, password: &str) -> Result<crate::auth::UserEntry> {
2178 self.require(&crate::auth::Permission::Admin)?;
2179 let hash = crate::auth::hash_password(password).map_err(MongrelError::Other)?;
2180 let epoch = self.epoch.bump_assigned();
2181 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2182 let id = {
2183 let mut cat = self.catalog.write();
2184 if cat.users.iter().any(|u| u.username == username) {
2185 return Err(MongrelError::InvalidArgument(format!(
2186 "user {username:?} already exists"
2187 )));
2188 }
2189 cat.next_user_id += 1;
2190 let id = cat.next_user_id;
2191 let entry = crate::auth::UserEntry {
2192 id,
2193 username: username.into(),
2194 password_hash: hash,
2195 roles: Vec::new(),
2196 is_admin: false,
2197 created_epoch: epoch.0,
2198 };
2199 cat.users.push(entry.clone());
2200 cat.db_epoch = epoch.0;
2201 entry
2202 };
2203 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2204 self.epoch.publish_in_order(epoch);
2205 _epoch_guard.disarm();
2206 Ok(id)
2207 }
2208
2209 pub fn drop_user(&self, username: &str) -> Result<()> {
2211 self.require(&crate::auth::Permission::Admin)?;
2212 let epoch = self.epoch.bump_assigned();
2213 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2214 {
2215 let mut cat = self.catalog.write();
2216 let before = cat.users.len();
2217 cat.users.retain(|u| u.username != username);
2218 if cat.users.len() == before {
2219 return Err(MongrelError::NotFound(format!(
2220 "user {username:?} not found"
2221 )));
2222 }
2223 cat.db_epoch = epoch.0;
2224 }
2225 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2226 self.epoch.publish_in_order(epoch);
2227 _epoch_guard.disarm();
2228 Ok(())
2229 }
2230
2231 pub fn alter_user_password(&self, username: &str, new_password: &str) -> Result<()> {
2233 self.require(&crate::auth::Permission::Admin)?;
2234 let hash = crate::auth::hash_password(new_password).map_err(MongrelError::Other)?;
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 user = cat
2240 .users
2241 .iter_mut()
2242 .find(|u| u.username == username)
2243 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
2244 user.password_hash = hash;
2245 cat.db_epoch = epoch.0;
2246 }
2247 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2248 self.epoch.publish_in_order(epoch);
2249 _epoch_guard.disarm();
2250 Ok(())
2251 }
2252
2253 pub fn verify_user(
2256 &self,
2257 username: &str,
2258 password: &str,
2259 ) -> Result<Option<crate::auth::UserEntry>> {
2260 let cat = self.catalog.read();
2261 let Some(user) = cat.users.iter().find(|u| u.username == username) else {
2262 return Ok(None);
2263 };
2264 if user.password_hash.is_empty() {
2265 return Ok(None);
2266 }
2267 let ok = crate::auth::verify_password(password, &user.password_hash)
2268 .map_err(MongrelError::Other)?;
2269 if ok {
2270 Ok(Some(user.clone()))
2271 } else {
2272 Ok(None)
2273 }
2274 }
2275
2276 pub fn set_user_admin(&self, username: &str, is_admin: bool) -> Result<()> {
2278 self.require(&crate::auth::Permission::Admin)?;
2279 let epoch = self.epoch.bump_assigned();
2280 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2281 {
2282 let mut cat = self.catalog.write();
2283 let user = cat
2284 .users
2285 .iter_mut()
2286 .find(|u| u.username == username)
2287 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
2288 user.is_admin = is_admin;
2289 cat.db_epoch = epoch.0;
2290 }
2291 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2292 self.epoch.publish_in_order(epoch);
2293 _epoch_guard.disarm();
2294 Ok(())
2295 }
2296
2297 pub fn create_role(&self, name: &str) -> Result<crate::auth::RoleEntry> {
2299 self.require(&crate::auth::Permission::Admin)?;
2300 let epoch = self.epoch.bump_assigned();
2301 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2302 let entry = {
2303 let mut cat = self.catalog.write();
2304 if cat.roles.iter().any(|r| r.name == name) {
2305 return Err(MongrelError::InvalidArgument(format!(
2306 "role {name:?} already exists"
2307 )));
2308 }
2309 let entry = crate::auth::RoleEntry {
2310 name: name.into(),
2311 permissions: Vec::new(),
2312 created_epoch: epoch.0,
2313 };
2314 cat.roles.push(entry.clone());
2315 cat.db_epoch = epoch.0;
2316 entry
2317 };
2318 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2319 self.epoch.publish_in_order(epoch);
2320 _epoch_guard.disarm();
2321 Ok(entry)
2322 }
2323
2324 pub fn drop_role(&self, name: &str) -> Result<()> {
2326 self.require(&crate::auth::Permission::Admin)?;
2327 let epoch = self.epoch.bump_assigned();
2328 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2329 {
2330 let mut cat = self.catalog.write();
2331 let before = cat.roles.len();
2332 cat.roles.retain(|r| r.name != name);
2333 if cat.roles.len() == before {
2334 return Err(MongrelError::NotFound(format!("role {name:?} not found")));
2335 }
2336 for user in &mut cat.users {
2338 user.roles.retain(|r| r != name);
2339 }
2340 cat.db_epoch = epoch.0;
2341 }
2342 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2343 self.epoch.publish_in_order(epoch);
2344 _epoch_guard.disarm();
2345 Ok(())
2346 }
2347
2348 pub fn grant_role(&self, username: &str, role_name: &str) -> Result<()> {
2350 self.require(&crate::auth::Permission::Admin)?;
2351 let epoch = self.epoch.bump_assigned();
2352 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2353 {
2354 let mut cat = self.catalog.write();
2355 if !cat.roles.iter().any(|r| r.name == role_name) {
2356 return Err(MongrelError::NotFound(format!(
2357 "role {role_name:?} not found"
2358 )));
2359 }
2360 let user = cat
2361 .users
2362 .iter_mut()
2363 .find(|u| u.username == username)
2364 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
2365 if !user.roles.contains(&role_name.to_string()) {
2366 user.roles.push(role_name.into());
2367 }
2368 cat.db_epoch = epoch.0;
2369 }
2370 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2371 self.epoch.publish_in_order(epoch);
2372 _epoch_guard.disarm();
2373 Ok(())
2374 }
2375
2376 pub fn revoke_role(&self, username: &str, role_name: &str) -> Result<()> {
2378 self.require(&crate::auth::Permission::Admin)?;
2379 let epoch = self.epoch.bump_assigned();
2380 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2381 {
2382 let mut cat = self.catalog.write();
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 user.roles.retain(|r| r != role_name);
2389 cat.db_epoch = epoch.0;
2390 }
2391 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2392 self.epoch.publish_in_order(epoch);
2393 _epoch_guard.disarm();
2394 Ok(())
2395 }
2396
2397 pub fn grant_permission(
2399 &self,
2400 role_name: &str,
2401 permission: crate::auth::Permission,
2402 ) -> Result<()> {
2403 self.require(&crate::auth::Permission::Admin)?;
2404 let epoch = self.epoch.bump_assigned();
2405 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2406 {
2407 let mut cat = self.catalog.write();
2408 let role = cat
2409 .roles
2410 .iter_mut()
2411 .find(|r| r.name == role_name)
2412 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
2413 merge_permission(&mut role.permissions, permission);
2414 cat.db_epoch = epoch.0;
2415 }
2416 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2417 self.epoch.publish_in_order(epoch);
2418 _epoch_guard.disarm();
2419 Ok(())
2420 }
2421
2422 pub fn revoke_permission(
2424 &self,
2425 role_name: &str,
2426 permission: crate::auth::Permission,
2427 ) -> Result<()> {
2428 self.require(&crate::auth::Permission::Admin)?;
2429 let epoch = self.epoch.bump_assigned();
2430 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2431 {
2432 let mut cat = self.catalog.write();
2433 let role = cat
2434 .roles
2435 .iter_mut()
2436 .find(|r| r.name == role_name)
2437 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
2438 revoke_permission_from(&mut role.permissions, &permission);
2439 cat.db_epoch = epoch.0;
2440 }
2441 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2442 self.epoch.publish_in_order(epoch);
2443 _epoch_guard.disarm();
2444 Ok(())
2445 }
2446
2447 pub fn resolve_principal(&self, username: &str) -> Option<crate::auth::Principal> {
2450 let cat = self.catalog.read();
2451 Self::resolve_principal_from_catalog(&cat, username)
2452 }
2453
2454 fn resolve_principal_from_catalog(
2459 cat: &Catalog,
2460 username: &str,
2461 ) -> Option<crate::auth::Principal> {
2462 let user = cat.users.iter().find(|u| u.username == username)?;
2463 let mut permissions = Vec::new();
2464 for role_name in &user.roles {
2465 if let Some(role) = cat.roles.iter().find(|r| &r.name == role_name) {
2466 permissions.extend(role.permissions.iter().cloned());
2467 }
2468 }
2469 Some(crate::auth::Principal {
2470 username: user.username.clone(),
2471 is_admin: user.is_admin,
2472 roles: user.roles.clone(),
2473 permissions,
2474 })
2475 }
2476
2477 pub fn check_permission(&self, username: &str, permission: &crate::auth::Permission) -> bool {
2479 match self.resolve_principal(username) {
2480 Some(p) => p.has_permission(permission),
2481 None => false,
2482 }
2483 }
2484
2485 pub fn require_auth_enabled(&self) -> bool {
2489 self.catalog.read().require_auth
2490 }
2491
2492 pub fn principal(&self) -> Option<crate::auth::Principal> {
2496 self.principal.read().clone()
2497 }
2498
2499 fn table_auth_checker(&self) -> Option<Arc<dyn crate::auth_state::TableAuthChecker>> {
2505 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
2506 self.auth_state.clone(),
2507 )))
2508 }
2509
2510 pub fn refresh_principal(&self) -> Result<()> {
2523 let username = match self.principal.read().clone() {
2524 Some(p) => p.username,
2525 None => return Ok(()),
2526 };
2527 let cat = catalog::read(&self.root, self.meta_dek.as_ref())?
2531 .ok_or_else(|| MongrelError::NotFound("catalog vanished during refresh".into()))?;
2532 *self.catalog.write() = cat.clone();
2535 match Self::resolve_principal_from_catalog(&cat, &username) {
2536 Some(p) => {
2537 *self.principal.write() = Some(p.clone());
2538 self.auth_state.set_principal(Some(p));
2542 Ok(())
2543 }
2544 None => Err(MongrelError::InvalidCredentials { username }),
2545 }
2546 }
2547
2548 pub fn enable_auth(&self, admin_username: &str, admin_password: &str) -> Result<()> {
2560 let password_hash =
2561 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
2562 let epoch = self.epoch.bump_assigned();
2563 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2564 {
2565 let mut cat = self.catalog.write();
2566 if cat.require_auth {
2567 return Err(MongrelError::InvalidArgument(
2568 "database already has require_auth enabled".into(),
2569 ));
2570 }
2571 if cat.users.iter().any(|u| u.username == admin_username) {
2574 return Err(MongrelError::InvalidArgument(format!(
2575 "user {admin_username:?} already exists"
2576 )));
2577 }
2578 cat.next_user_id = cat.next_user_id.max(1);
2579 let id = cat.next_user_id;
2580 cat.next_user_id += 1;
2581 cat.users.push(crate::auth::UserEntry {
2582 id,
2583 username: admin_username.to_string(),
2584 password_hash,
2585 roles: Vec::new(),
2586 is_admin: true,
2587 created_epoch: epoch.0,
2588 });
2589 cat.require_auth = true;
2590 cat.db_epoch = epoch.0;
2591 }
2592 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2593 *self.principal.write() = Some(crate::auth::Principal {
2596 username: admin_username.to_string(),
2597 is_admin: true,
2598 roles: Vec::new(),
2599 permissions: Vec::new(),
2600 });
2601 self.auth_state.set_require_auth(true);
2602 self.epoch.publish_in_order(epoch);
2603 _epoch_guard.disarm();
2604 Ok(())
2605 }
2606
2607 pub fn disable_auth(&self) -> Result<()> {
2624 let epoch = self.epoch.bump_assigned();
2625 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2626 {
2627 let mut cat = self.catalog.write();
2628 if !cat.require_auth {
2629 return Err(MongrelError::InvalidArgument(
2630 "database does not have require_auth enabled".into(),
2631 ));
2632 }
2633 cat.require_auth = false;
2634 cat.db_epoch = epoch.0;
2635 }
2636 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2637 *self.principal.write() = None;
2639 self.auth_state.set_require_auth(false);
2641 self.epoch.publish_in_order(epoch);
2642 _epoch_guard.disarm();
2643 Ok(())
2644 }
2645
2646 pub fn require(&self, perm: &crate::auth::Permission) -> Result<()> {
2653 if self.read_only && !matches!(perm, crate::auth::Permission::Select { .. }) {
2654 return Err(MongrelError::ReadOnlyReplica);
2655 }
2656 if !self.catalog.read().require_auth {
2657 return Ok(());
2658 }
2659 let guard = self.principal.read();
2660 let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
2661 if p.has_permission(perm) {
2662 Ok(())
2663 } else {
2664 Err(MongrelError::PermissionDenied {
2665 required: perm.clone(),
2666 principal: p.username.clone(),
2667 })
2668 }
2669 }
2670
2671 pub fn require_table(
2676 &self,
2677 table: &str,
2678 perm: crate::auth_state::RequiredPermission,
2679 ) -> Result<()> {
2680 self.require(&perm.into_permission(table))
2681 }
2682
2683 pub fn triggers(&self) -> Vec<StoredTrigger> {
2684 self.catalog
2685 .read()
2686 .triggers
2687 .iter()
2688 .map(|t| t.trigger.clone())
2689 .collect()
2690 }
2691
2692 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
2693 self.catalog
2694 .read()
2695 .triggers
2696 .iter()
2697 .find(|t| t.trigger.name == name)
2698 .map(|t| t.trigger.clone())
2699 }
2700
2701 pub fn create_trigger(&self, mut trigger: StoredTrigger) -> Result<StoredTrigger> {
2702 self.require(&crate::auth::Permission::Ddl)?;
2703 let _g = self.ddl_lock.lock();
2704 trigger.validate()?;
2705 self.validate_trigger_references(&trigger)?;
2706 {
2707 let cat = self.catalog.read();
2708 if cat.triggers.iter().any(|t| t.trigger.name == trigger.name) {
2709 return Err(MongrelError::InvalidArgument(format!(
2710 "trigger {:?} already exists",
2711 trigger.name
2712 )));
2713 }
2714 }
2715 let commit_lock = Arc::clone(&self.commit_lock);
2716 let _c = commit_lock.lock();
2717 let epoch = self.epoch.bump_assigned();
2718 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2719 trigger.created_epoch = epoch.0;
2720 trigger.updated_epoch = epoch.0;
2721 {
2722 let mut cat = self.catalog.write();
2723 cat.triggers.push(TriggerEntry::from(trigger.clone()));
2724 cat.db_epoch = epoch.0;
2725 }
2726 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2727 self.epoch.publish_in_order(epoch);
2728 _epoch_guard.disarm();
2729 Ok(trigger)
2730 }
2731
2732 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
2733 let _g = self.ddl_lock.lock();
2734 trigger.validate()?;
2735 self.validate_trigger_references(&trigger)?;
2736 let commit_lock = Arc::clone(&self.commit_lock);
2737 let _c = commit_lock.lock();
2738 let epoch = self.epoch.bump_assigned();
2739 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2740 let replaced = {
2741 let mut cat = self.catalog.write();
2742 let next = match cat
2743 .triggers
2744 .iter()
2745 .position(|t| t.trigger.name == trigger.name)
2746 {
2747 Some(idx) => {
2748 let next = cat.triggers[idx]
2749 .trigger
2750 .replaced(trigger.clone(), epoch.0)?;
2751 cat.triggers[idx] = TriggerEntry::from(next.clone());
2752 next
2753 }
2754 None => {
2755 let mut next = trigger;
2756 next.created_epoch = epoch.0;
2757 next.updated_epoch = epoch.0;
2758 cat.triggers.push(TriggerEntry::from(next.clone()));
2759 next
2760 }
2761 };
2762 cat.db_epoch = epoch.0;
2763 next
2764 };
2765 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2766 self.epoch.publish_in_order(epoch);
2767 _epoch_guard.disarm();
2768 Ok(replaced)
2769 }
2770
2771 pub fn drop_trigger(&self, name: &str) -> Result<()> {
2772 self.require(&crate::auth::Permission::Ddl)?;
2773 let _g = self.ddl_lock.lock();
2774 let commit_lock = Arc::clone(&self.commit_lock);
2775 let _c = commit_lock.lock();
2776 let epoch = self.epoch.bump_assigned();
2777 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2778 {
2779 let mut cat = self.catalog.write();
2780 let before = cat.triggers.len();
2781 cat.triggers.retain(|t| t.trigger.name != name);
2782 if cat.triggers.len() == before {
2783 return Err(MongrelError::NotFound(format!(
2784 "trigger {name:?} not found"
2785 )));
2786 }
2787 cat.db_epoch = epoch.0;
2788 }
2789 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2790 self.epoch.publish_in_order(epoch);
2791 _epoch_guard.disarm();
2792 Ok(())
2793 }
2794
2795 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
2796 self.catalog.read().external_tables.clone()
2797 }
2798
2799 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
2800 self.catalog
2801 .read()
2802 .external_tables
2803 .iter()
2804 .find(|t| t.name == name)
2805 .cloned()
2806 }
2807
2808 pub fn create_external_table(
2809 &self,
2810 mut entry: ExternalTableEntry,
2811 ) -> Result<ExternalTableEntry> {
2812 self.require(&crate::auth::Permission::Ddl)?;
2813 let _g = self.ddl_lock.lock();
2814 entry.validate()?;
2815 {
2816 let cat = self.catalog.read();
2817 if cat.live(&entry.name).is_some()
2818 || cat.external_tables.iter().any(|t| t.name == entry.name)
2819 {
2820 return Err(MongrelError::InvalidArgument(format!(
2821 "table {:?} already exists",
2822 entry.name
2823 )));
2824 }
2825 }
2826 let commit_lock = Arc::clone(&self.commit_lock);
2827 let _c = commit_lock.lock();
2828 let epoch = self.epoch.bump_assigned();
2829 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2830 entry.created_epoch = epoch.0;
2831 {
2832 let mut cat = self.catalog.write();
2833 cat.external_tables.push(entry.clone());
2834 cat.db_epoch = epoch.0;
2835 }
2836 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2837 self.epoch.publish_in_order(epoch);
2838 _epoch_guard.disarm();
2839 Ok(entry)
2840 }
2841
2842 pub fn drop_external_table(&self, name: &str) -> Result<()> {
2843 self.require(&crate::auth::Permission::Ddl)?;
2844 let _g = self.ddl_lock.lock();
2845 let commit_lock = Arc::clone(&self.commit_lock);
2846 let _c = commit_lock.lock();
2847 let epoch = self.epoch.bump_assigned();
2848 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2849 {
2850 let mut cat = self.catalog.write();
2851 let before = cat.external_tables.len();
2852 cat.external_tables.retain(|t| t.name != name);
2853 if cat.external_tables.len() == before {
2854 return Err(MongrelError::NotFound(format!(
2855 "external table {name:?} not found"
2856 )));
2857 }
2858 cat.db_epoch = epoch.0;
2859 }
2860 let state_dir = self.root.join(VTAB_DIR).join(name);
2861 if state_dir.exists() {
2862 std::fs::remove_dir_all(state_dir)?;
2863 }
2864 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
2865 self.epoch.publish_in_order(epoch);
2866 _epoch_guard.disarm();
2867 Ok(())
2868 }
2869
2870 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
2871 let txn_id = self.alloc_txn_id();
2872 self.commit_transaction_with_external_states(
2873 txn_id,
2874 self.epoch.visible(),
2875 Vec::new(),
2876 vec![(name.to_string(), state.to_vec())],
2877 Vec::new(),
2878 None,
2879 None,
2880 )
2881 }
2882
2883 pub fn trigger_config(&self) -> TriggerConfig {
2884 use std::sync::atomic::Ordering;
2885 TriggerConfig {
2886 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
2887 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
2888 max_loop_iterations: self.trigger_max_loop_iterations.load(Ordering::Relaxed),
2889 }
2890 }
2891
2892 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
2893 use std::sync::atomic::Ordering;
2894 if config.max_depth == 0 {
2895 return Err(MongrelError::InvalidArgument(
2896 "trigger max_depth must be greater than 0".into(),
2897 ));
2898 }
2899 self.trigger_recursive
2900 .store(config.recursive_triggers, Ordering::Relaxed);
2901 self.trigger_max_depth
2902 .store(config.max_depth, Ordering::Relaxed);
2903 self.trigger_max_loop_iterations
2904 .store(config.max_loop_iterations, Ordering::Relaxed);
2905 Ok(())
2906 }
2907
2908 pub fn set_recursive_triggers(&self, recursive: bool) {
2909 use std::sync::atomic::Ordering;
2910 self.trigger_recursive.store(recursive, Ordering::Relaxed);
2911 }
2912
2913 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent> {
2917 self.notify.subscribe()
2918 }
2919
2920 pub fn subscribe_change_commits(&self) -> tokio::sync::broadcast::Receiver<()> {
2921 self.change_wake.subscribe()
2922 }
2923
2924 pub fn change_events_since(&self, last_event_id: Option<&str>) -> Result<CdcBatch> {
2929 use crate::wal::Op;
2930
2931 let resume = match last_event_id {
2932 Some(id) => {
2933 let (epoch, index) = id.split_once(':').ok_or_else(|| {
2934 MongrelError::InvalidArgument(format!(
2935 "invalid CDC event id {id:?}; expected <epoch>:<index>"
2936 ))
2937 })?;
2938 Some((
2939 epoch.parse::<u64>().map_err(|error| {
2940 MongrelError::InvalidArgument(format!("invalid CDC epoch: {error}"))
2941 })?,
2942 index.parse::<u32>().map_err(|error| {
2943 MongrelError::InvalidArgument(format!("invalid CDC index: {error}"))
2944 })?,
2945 ))
2946 }
2947 None => None,
2948 };
2949
2950 let mut wal = self.shared_wal.lock();
2951 wal.group_sync()?;
2952 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
2953 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
2954 drop(wal);
2955
2956 let commits: HashMap<u64, (u64, Vec<crate::wal::AddedRun>)> = records
2957 .iter()
2958 .filter_map(|record| match &record.op {
2959 Op::TxnCommit { epoch, added_runs } => {
2960 Some((record.txn_id, (*epoch, added_runs.clone())))
2961 }
2962 _ => None,
2963 })
2964 .collect();
2965 let earliest_epoch = commits.values().map(|(epoch, _)| *epoch).min();
2966 let current_epoch = self.epoch.visible().0;
2967 let gap = resume.is_some_and(|(epoch, _)| {
2968 epoch < current_epoch
2969 && earliest_epoch.map_or(true, |earliest| earliest > epoch.saturating_add(1))
2970 });
2971 if gap {
2972 return Ok(CdcBatch {
2973 events: Vec::new(),
2974 current_epoch,
2975 earliest_epoch,
2976 gap: true,
2977 });
2978 }
2979
2980 let table_names: HashMap<u64, String> = self
2981 .catalog
2982 .read()
2983 .tables
2984 .iter()
2985 .map(|entry| (entry.table_id, entry.name.clone()))
2986 .collect();
2987 let before_images: HashMap<(u64, u64, u64), crate::memtable::Row> = records
2988 .iter()
2989 .filter_map(|record| {
2990 if !commits.contains_key(&record.txn_id) {
2991 return None;
2992 }
2993 let Op::BeforeImage {
2994 table_id,
2995 row_id,
2996 row,
2997 } = &record.op
2998 else {
2999 return None;
3000 };
3001 bincode::deserialize(row)
3002 .ok()
3003 .map(|before| ((record.txn_id, *table_id, row_id.0), before))
3004 })
3005 .collect();
3006 let mut operation_indices: HashMap<u64, u32> = HashMap::new();
3007 let mut events = Vec::new();
3008 for record in &records {
3009 let Some((commit_epoch, _)) = commits.get(&record.txn_id) else {
3010 continue;
3011 };
3012 let event = match &record.op {
3013 Op::Put { table_id, rows } => {
3014 let rows: Vec<crate::memtable::Row> = bincode::deserialize(rows)?;
3015 let data = serde_json::to_value(rows)
3016 .map_err(|error| MongrelError::Other(format!("CDC JSON: {error}")))?;
3017 Some((*table_id, "put", data))
3018 }
3019 Op::Delete { table_id, row_ids } => {
3020 let before = row_ids
3021 .iter()
3022 .filter_map(|row_id| {
3023 before_images
3024 .get(&(record.txn_id, *table_id, row_id.0))
3025 .cloned()
3026 })
3027 .collect::<Vec<_>>();
3028 Some((
3029 *table_id,
3030 "delete",
3031 serde_json::json!({
3032 "row_ids": row_ids.iter().map(|row_id| row_id.0).collect::<Vec<_>>(),
3033 "before": before,
3034 }),
3035 ))
3036 }
3037 Op::TruncateTable { table_id } => {
3038 Some((*table_id, "truncate", serde_json::Value::Null))
3039 }
3040 _ => None,
3041 };
3042 if let Some((table_id, op, data)) = event {
3043 let index = operation_indices.entry(record.txn_id).or_insert(0);
3044 let event_position = (*commit_epoch, *index);
3045 *index = index.saturating_add(1);
3046 if resume.is_some_and(|position| event_position <= position) {
3047 continue;
3048 }
3049 events.push(ChangeEvent {
3050 id: Some(format!("{}:{}", event_position.0, event_position.1)),
3051 channel: "changes".into(),
3052 table_id: Some(table_id),
3053 table: table_names.get(&table_id).cloned().unwrap_or_default(),
3054 op: op.into(),
3055 epoch: *commit_epoch,
3056 txn_id: Some(record.txn_id),
3057 message: None,
3058 data: Some(data),
3059 });
3060 }
3061 if let Op::TxnCommit { added_runs, .. } = &record.op {
3062 for run in added_runs {
3063 let index = operation_indices.entry(record.txn_id).or_insert(0);
3064 let event_position = (*commit_epoch, *index);
3065 *index = index.saturating_add(1);
3066 if resume.is_some_and(|position| event_position <= position) {
3067 continue;
3068 }
3069 let handle = self.tables.read().get(&run.table_id).cloned();
3070 let rows = handle.and_then(|handle| {
3071 let table = handle.lock();
3072 let mut reader = table.open_reader(run.run_id).ok()?;
3073 let mut rows = reader.all_rows().ok()?;
3074 for row in &mut rows {
3075 row.committed_epoch = Epoch(*commit_epoch);
3076 }
3077 Some(rows)
3078 });
3079 let Some(rows) = rows else {
3080 return Ok(CdcBatch {
3085 events: Vec::new(),
3086 current_epoch,
3087 earliest_epoch,
3088 gap: true,
3089 });
3090 };
3091 events.push(ChangeEvent {
3092 id: Some(format!("{}:{}", event_position.0, event_position.1)),
3093 channel: "changes".into(),
3094 table_id: Some(run.table_id),
3095 table: table_names.get(&run.table_id).cloned().unwrap_or_default(),
3096 op: "put_run".into(),
3097 epoch: *commit_epoch,
3098 txn_id: Some(record.txn_id),
3099 message: None,
3100 data: Some(serde_json::json!({
3101 "run_id": run.run_id.to_string(),
3102 "row_count": run.row_count,
3103 "min_row_id": run.min_row_id,
3104 "max_row_id": run.max_row_id,
3105 "rows": rows,
3106 })),
3107 });
3108 }
3109 }
3110 }
3111 Ok(CdcBatch {
3112 events,
3113 current_epoch,
3114 earliest_epoch,
3115 gap: false,
3116 })
3117 }
3118
3119 pub fn notify(&self, channel: &str, message: Option<String>) {
3122 let _ = self.notify.send(ChangeEvent {
3123 id: None,
3124 channel: channel.to_string(),
3125 table_id: None,
3126 table: String::new(),
3127 op: "notify".into(),
3128 epoch: self.epoch.visible().0,
3129 txn_id: None,
3130 message,
3131 data: None,
3132 });
3133 }
3134
3135 pub fn call_procedure(
3136 &self,
3137 name: &str,
3138 args: HashMap<String, crate::Value>,
3139 ) -> Result<ProcedureCallResult> {
3140 self.call_procedure_as(name, args, None)
3141 }
3142
3143 pub fn call_procedure_as(
3144 &self,
3145 name: &str,
3146 args: HashMap<String, crate::Value>,
3147 principal: Option<&crate::auth::Principal>,
3148 ) -> Result<ProcedureCallResult> {
3149 self.require_for(principal, &crate::auth::Permission::All)?;
3153 let procedure = self
3154 .procedure(name)
3155 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
3156 let args = bind_procedure_args(&procedure, args)?;
3157 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
3158 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
3159 if has_writes {
3160 let mut tx = self.begin_as(principal.cloned());
3161 let run = (|| {
3162 for step in &procedure.body.steps {
3163 let output = self.execute_procedure_step(
3164 step,
3165 &args,
3166 &outputs,
3167 Some(&mut tx),
3168 principal,
3169 )?;
3170 outputs.insert(step.id().to_string(), output);
3171 }
3172 eval_return_output(&procedure.body.return_value, &args, &outputs)
3173 })();
3174 match run {
3175 Ok(output) => {
3176 let epoch = tx.commit()?.0;
3177 Ok(ProcedureCallResult {
3178 epoch: Some(epoch),
3179 output,
3180 })
3181 }
3182 Err(e) => {
3183 tx.rollback();
3184 Err(e)
3185 }
3186 }
3187 } else {
3188 for step in &procedure.body.steps {
3189 let output = self.execute_procedure_step(step, &args, &outputs, None, principal)?;
3190 outputs.insert(step.id().to_string(), output);
3191 }
3192 Ok(ProcedureCallResult {
3193 epoch: None,
3194 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
3195 })
3196 }
3197 }
3198
3199 fn execute_procedure_step(
3200 &self,
3201 step: &ProcedureStep,
3202 args: &HashMap<String, crate::Value>,
3203 outputs: &HashMap<String, ProcedureCallOutput>,
3204 tx: Option<&mut crate::txn::Transaction<'_>>,
3205 principal: Option<&crate::auth::Principal>,
3206 ) -> Result<ProcedureCallOutput> {
3207 match step {
3208 ProcedureStep::NativeQuery {
3209 table,
3210 conditions,
3211 projection,
3212 limit,
3213 ..
3214 } => {
3215 let mut q = crate::Query::new();
3216 for condition in conditions {
3217 q = q.and(eval_condition(condition, args, outputs)?);
3218 }
3219 let handle = self.table(table)?;
3220 let rows = handle.lock().query(&q)?;
3221 let mut rows = self.secure_rows_for(table, rows, principal)?;
3222 if let Some(limit) = limit {
3223 rows.truncate(*limit);
3224 }
3225 let projection = projection.as_ref();
3226 Ok(ProcedureCallOutput::Rows(
3227 rows.into_iter()
3228 .map(|row| ProcedureCallRow {
3229 row_id: Some(row.row_id),
3230 columns: match projection {
3231 Some(ids) => row
3232 .columns
3233 .into_iter()
3234 .filter(|(id, _)| ids.contains(id))
3235 .collect(),
3236 None => row.columns,
3237 },
3238 })
3239 .collect(),
3240 ))
3241 }
3242 ProcedureStep::Put {
3243 table,
3244 cells,
3245 returning,
3246 ..
3247 } => {
3248 let tx = tx.ok_or_else(|| {
3249 MongrelError::InvalidArgument(
3250 "write procedure step requires a transaction".into(),
3251 )
3252 })?;
3253 let cells = eval_cells(cells, args, outputs)?;
3254 if *returning {
3255 let out = tx.put_returning(table, cells)?;
3256 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
3257 row_id: None,
3258 columns: out.row.columns.into_iter().collect(),
3259 }))
3260 } else {
3261 tx.put(table, cells)?;
3262 Ok(ProcedureCallOutput::Null)
3263 }
3264 }
3265 ProcedureStep::Upsert {
3266 table,
3267 cells,
3268 update_cells,
3269 returning,
3270 ..
3271 } => {
3272 let tx = tx.ok_or_else(|| {
3273 MongrelError::InvalidArgument(
3274 "write procedure step requires a transaction".into(),
3275 )
3276 })?;
3277 let cells = eval_cells(cells, args, outputs)?;
3278 let action = match update_cells {
3279 Some(update_cells) => {
3280 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
3281 }
3282 None => crate::UpsertAction::DoNothing,
3283 };
3284 let out = tx.upsert(table, cells, action)?;
3285 if *returning {
3286 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
3287 row_id: None,
3288 columns: out.row.columns.into_iter().collect(),
3289 }))
3290 } else {
3291 Ok(ProcedureCallOutput::Null)
3292 }
3293 }
3294 ProcedureStep::DeleteByPk { table, pk, .. } => {
3295 let tx = tx.ok_or_else(|| {
3296 MongrelError::InvalidArgument(
3297 "write procedure step requires a transaction".into(),
3298 )
3299 })?;
3300 let pk = eval_value(pk, args, outputs)?;
3301 let handle = self.table(table)?;
3302 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
3303 MongrelError::NotFound("procedure delete_by_pk target not found".into())
3304 })?;
3305 tx.delete(table, row_id)?;
3306 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
3307 }
3308 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
3309 "DeleteRows procedure step is not supported by the core executor yet".into(),
3310 )),
3311 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
3312 "SqlQuery procedure step must be executed by mongreldb-query".into(),
3313 )),
3314 }
3315 }
3316
3317 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
3318 let cat = self.catalog.read();
3319 for step in &procedure.body.steps {
3320 let Some(table_name) = step.table() else {
3321 continue;
3322 };
3323 let schema = &cat
3324 .live(table_name)
3325 .ok_or_else(|| {
3326 MongrelError::InvalidArgument(format!(
3327 "procedure {:?} references unknown table {table_name:?}",
3328 procedure.name
3329 ))
3330 })?
3331 .schema;
3332 match step {
3333 ProcedureStep::NativeQuery {
3334 conditions,
3335 projection,
3336 ..
3337 } => {
3338 for condition in conditions {
3339 validate_condition_columns(condition, schema)?;
3340 }
3341 if let Some(projection) = projection {
3342 for id in projection {
3343 validate_column_id(*id, schema)?;
3344 }
3345 }
3346 }
3347 ProcedureStep::Put { cells, .. } => {
3348 for cell in cells {
3349 validate_column_id(cell.column_id, schema)?;
3350 }
3351 }
3352 ProcedureStep::Upsert {
3353 cells,
3354 update_cells,
3355 ..
3356 } => {
3357 for cell in cells {
3358 validate_column_id(cell.column_id, schema)?;
3359 }
3360 if let Some(update_cells) = update_cells {
3361 for cell in update_cells {
3362 validate_column_id(cell.column_id, schema)?;
3363 }
3364 }
3365 }
3366 ProcedureStep::DeleteByPk { .. } => {
3367 if schema.primary_key().is_none() {
3368 return Err(MongrelError::InvalidArgument(format!(
3369 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
3370 procedure.name
3371 )));
3372 }
3373 }
3374 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
3375 }
3376 }
3377 Ok(())
3378 }
3379
3380 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
3381 let cat = self.catalog.read();
3382 let target_schema = match &trigger.target {
3383 TriggerTarget::Table(target_name) => cat
3384 .live(target_name)
3385 .ok_or_else(|| {
3386 MongrelError::InvalidArgument(format!(
3387 "trigger {:?} references unknown target table {target_name:?}",
3388 trigger.name
3389 ))
3390 })?
3391 .schema
3392 .clone(),
3393 TriggerTarget::View(_) => Schema {
3394 columns: trigger.target_columns.clone(),
3395 ..Schema::default()
3396 },
3397 };
3398 for col in &trigger.update_of {
3399 if target_schema.column(col).is_none() {
3400 return Err(MongrelError::InvalidArgument(format!(
3401 "trigger {:?} UPDATE OF references unknown column {col:?}",
3402 trigger.name
3403 )));
3404 }
3405 }
3406 if let Some(expr) = &trigger.when {
3407 validate_trigger_expr(expr, &target_schema, trigger.event)?;
3408 }
3409 let mut select_schemas: HashMap<String, &Schema> = HashMap::new();
3410 for step in &trigger.program.steps {
3411 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
3412 {
3413 return Err(MongrelError::InvalidArgument(
3414 "SetNew trigger steps are only valid in BEFORE triggers".into(),
3415 ));
3416 }
3417 validate_trigger_step(
3418 step,
3419 &cat,
3420 &target_schema,
3421 trigger.event,
3422 &mut select_schemas,
3423 )?;
3424 }
3425 Ok(())
3426 }
3427
3428 pub fn begin(&self) -> crate::txn::Transaction<'_> {
3430 self.begin_with_isolation(crate::txn::IsolationLevel::default())
3431 }
3432
3433 pub fn begin_as(
3434 &self,
3435 principal: Option<crate::auth::Principal>,
3436 ) -> crate::txn::Transaction<'_> {
3437 let txn_id = self.alloc_txn_id();
3438 let read = Snapshot::at(self.epoch.visible());
3439 crate::txn::Transaction::new(self, txn_id, read).with_principal(principal)
3440 }
3441
3442 pub fn begin_with_isolation(
3444 &self,
3445 level: crate::txn::IsolationLevel,
3446 ) -> crate::txn::Transaction<'_> {
3447 let txn_id = self.alloc_txn_id();
3448 let epoch = match level {
3449 crate::txn::IsolationLevel::ReadCommitted => self.epoch.visible(),
3450 _ => self.epoch.visible(),
3451 };
3452 let read = Snapshot::at(epoch);
3453 crate::txn::Transaction::new(self, txn_id, read)
3454 }
3455
3456 pub fn begin_with_external_trigger_bridge<'a>(
3459 &'a self,
3460 bridge: &'a dyn ExternalTriggerBridge,
3461 ) -> crate::txn::Transaction<'a> {
3462 let txn_id = self.alloc_txn_id();
3463 let read = Snapshot::at(self.epoch.visible());
3464 crate::txn::Transaction::new(self, txn_id, read).with_external_trigger_bridge(bridge)
3465 }
3466
3467 pub fn begin_with_external_trigger_bridge_as<'a>(
3468 &'a self,
3469 bridge: &'a dyn ExternalTriggerBridge,
3470 principal: Option<crate::auth::Principal>,
3471 ) -> crate::txn::Transaction<'a> {
3472 let txn_id = self.alloc_txn_id();
3473 let read = Snapshot::at(self.epoch.visible());
3474 crate::txn::Transaction::new(self, txn_id, read)
3475 .with_external_trigger_bridge(bridge)
3476 .with_principal(principal)
3477 }
3478
3479 pub fn transaction<T>(
3481 &self,
3482 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
3483 ) -> Result<T> {
3484 let mut tx = self.begin();
3485 match f(&mut tx) {
3486 Ok(out) => {
3487 tx.commit()?;
3488 Ok(out)
3489 }
3490 Err(e) => {
3491 tx.rollback();
3492 Err(e)
3493 }
3494 }
3495 }
3496
3497 pub fn transaction_with_external_trigger_bridge<'a, T>(
3500 &'a self,
3501 bridge: &'a dyn ExternalTriggerBridge,
3502 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
3503 ) -> Result<T> {
3504 let mut tx = self.begin_with_external_trigger_bridge(bridge);
3505 match f(&mut tx) {
3506 Ok(out) => {
3507 tx.commit()?;
3508 Ok(out)
3509 }
3510 Err(e) => {
3511 tx.rollback();
3512 Err(e)
3513 }
3514 }
3515 }
3516
3517 pub fn transaction_with_external_trigger_bridge_as<'a, T>(
3518 &'a self,
3519 bridge: &'a dyn ExternalTriggerBridge,
3520 principal: Option<crate::auth::Principal>,
3521 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
3522 ) -> Result<T> {
3523 let mut tx = self.begin_with_external_trigger_bridge_as(bridge, principal);
3524 match f(&mut tx) {
3525 Ok(output) => {
3526 tx.commit()?;
3527 Ok(output)
3528 }
3529 Err(error) => {
3530 tx.rollback();
3531 Err(error)
3532 }
3533 }
3534 }
3535
3536 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
3539 self.active_txns.register(epoch)
3540 }
3541
3542 fn fill_auto_increment_for_staging(
3543 &self,
3544 staging: &mut [(u64, crate::txn::Staged)],
3545 ) -> Result<()> {
3546 let tables = self.tables.read();
3547 for (table_id, staged) in staging {
3548 if let crate::txn::Staged::Put(cells) = staged {
3549 if let Some(handle) = tables.get(table_id) {
3550 let mut t = handle.lock();
3551 t.fill_auto_inc(cells)?;
3552 }
3553 }
3554 }
3555 Ok(())
3556 }
3557
3558 fn expand_table_triggers(
3559 &self,
3560 staging: &mut Vec<(u64, crate::txn::Staged)>,
3561 read_epoch: Epoch,
3562 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
3563 external_states: &mut Vec<(String, Vec<u8>)>,
3564 ) -> Result<()> {
3565 let mut external_writes = Vec::new();
3566 let config = self.trigger_config();
3567 if config.recursive_triggers {
3568 let chunk = std::mem::take(staging);
3569 let stacks = vec![Vec::new(); chunk.len()];
3570 *staging = self.expand_trigger_chunk(
3571 chunk,
3572 stacks,
3573 read_epoch,
3574 0,
3575 config.max_depth,
3576 &mut external_writes,
3577 &config,
3578 )?;
3579 self.apply_external_trigger_writes(
3580 external_writes,
3581 external_trigger_bridge,
3582 external_states,
3583 staging,
3584 )?;
3585 return Ok(());
3586 }
3587
3588 let mut expansion = self.expand_table_triggers_once(staging, read_epoch, None, &config)?;
3589 if !expansion.before.is_empty() {
3590 let mut final_staging = expansion.before;
3591 final_staging.extend(filter_ignored_staging(
3592 std::mem::take(staging),
3593 &expansion.ignored_indices,
3594 ));
3595 *staging = final_staging;
3596 } else if !expansion.ignored_indices.is_empty() {
3597 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
3598 }
3599 staging.append(&mut expansion.after);
3600 external_writes.append(&mut expansion.before_external);
3601 external_writes.append(&mut expansion.after_external);
3602 self.apply_external_trigger_writes(
3603 external_writes,
3604 external_trigger_bridge,
3605 external_states,
3606 staging,
3607 )?;
3608 Ok(())
3609 }
3610
3611 #[allow(clippy::too_many_arguments)]
3612 fn expand_trigger_chunk(
3613 &self,
3614 mut chunk: Vec<(u64, crate::txn::Staged)>,
3615 stacks: Vec<Vec<String>>,
3616 read_epoch: Epoch,
3617 depth: u32,
3618 max_depth: u32,
3619 external_writes: &mut Vec<ExternalTriggerWrite>,
3620 config: &TriggerConfig,
3621 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
3622 if chunk.is_empty() {
3623 return Ok(Vec::new());
3624 }
3625 self.fill_auto_increment_for_staging(&mut chunk)?;
3626 let expansion =
3627 self.expand_table_triggers_once(&mut chunk, read_epoch, Some(&stacks), config)?;
3628 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
3629 let stack = expansion
3630 .before_stacks
3631 .first()
3632 .or_else(|| expansion.after_stacks.first())
3633 .cloned()
3634 .unwrap_or_default();
3635 return Err(MongrelError::Conflict(format!(
3636 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
3637 Self::format_trigger_stack(&stack)
3638 )));
3639 }
3640
3641 let mut out = Vec::new();
3642 external_writes.extend(expansion.before_external);
3643 out.extend(self.expand_trigger_chunk(
3644 expansion.before,
3645 expansion.before_stacks,
3646 read_epoch,
3647 depth + 1,
3648 max_depth,
3649 external_writes,
3650 config,
3651 )?);
3652 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
3653 external_writes.extend(expansion.after_external);
3654 out.extend(self.expand_trigger_chunk(
3655 expansion.after,
3656 expansion.after_stacks,
3657 read_epoch,
3658 depth + 1,
3659 max_depth,
3660 external_writes,
3661 config,
3662 )?);
3663 Ok(out)
3664 }
3665
3666 fn apply_external_trigger_writes(
3667 &self,
3668 writes: Vec<ExternalTriggerWrite>,
3669 bridge: Option<&dyn ExternalTriggerBridge>,
3670 external_states: &mut Vec<(String, Vec<u8>)>,
3671 staging: &mut Vec<(u64, crate::txn::Staged)>,
3672 ) -> Result<()> {
3673 if writes.is_empty() {
3674 return Ok(());
3675 }
3676 let bridge = bridge.ok_or_else(|| {
3677 MongrelError::InvalidArgument(
3678 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
3679 )
3680 })?;
3681 for write in writes {
3682 let table = write.table().to_string();
3683 let entry = self.external_table(&table).ok_or_else(|| {
3684 MongrelError::NotFound(format!("external table {table:?} not found"))
3685 })?;
3686 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
3687 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
3688 external_states.push((table, result.state));
3689 for base_write in result.base_writes {
3690 match base_write {
3691 ExternalTriggerBaseWrite::Put { table, cells } => {
3692 let table_id = self.table_id(&table)?;
3693 staging.push((table_id, crate::txn::Staged::Put(cells)));
3694 }
3695 ExternalTriggerBaseWrite::Delete { table, row_id } => {
3696 let table_id = self.table_id(&table)?;
3697 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
3698 }
3699 }
3700 }
3701 }
3702 dedup_external_states_in_place(external_states);
3703 Ok(())
3704 }
3705
3706 fn expand_table_triggers_once(
3707 &self,
3708 staging: &mut Vec<(u64, crate::txn::Staged)>,
3709 read_epoch: Epoch,
3710 trigger_stacks: Option<&[Vec<String>]>,
3711 config: &TriggerConfig,
3712 ) -> Result<TriggerExpansion> {
3713 let triggers: Vec<StoredTrigger> = self
3714 .catalog
3715 .read()
3716 .triggers
3717 .iter()
3718 .filter(|entry| {
3719 entry.trigger.enabled
3720 && matches!(
3721 entry.trigger.timing,
3722 TriggerTiming::Before | TriggerTiming::After
3723 )
3724 && matches!(entry.trigger.target, TriggerTarget::Table(_))
3725 })
3726 .map(|entry| entry.trigger.clone())
3727 .collect();
3728 if triggers.is_empty() || staging.is_empty() {
3729 return Ok(TriggerExpansion::default());
3730 }
3731
3732 let before_triggers = triggers
3733 .iter()
3734 .filter(|trigger| trigger.timing == TriggerTiming::Before)
3735 .cloned()
3736 .collect::<Vec<_>>();
3737 let after_triggers = triggers
3738 .iter()
3739 .filter(|trigger| trigger.timing == TriggerTiming::After)
3740 .cloned()
3741 .collect::<Vec<_>>();
3742
3743 let mut before_added = Vec::new();
3744 let mut before_stacks = Vec::new();
3745 let mut before_external = Vec::new();
3746 let mut ignored_indices = std::collections::BTreeSet::new();
3747 if !before_triggers.is_empty() {
3748 let before_events =
3749 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks)?;
3750 let mut out = TriggerProgramOutput {
3751 added: &mut before_added,
3752 added_stacks: &mut before_stacks,
3753 added_external: &mut before_external,
3754 ignored_indices: &mut ignored_indices,
3755 };
3756 self.execute_triggers_for_events(
3757 &before_triggers,
3758 &before_events,
3759 Some(staging),
3760 &mut out,
3761 config,
3762 read_epoch,
3763 )?;
3764 }
3765
3766 let after_events = if after_triggers.is_empty() {
3767 Vec::new()
3768 } else {
3769 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks)?
3770 .into_iter()
3771 .filter(|event| {
3772 !event
3773 .op_indices
3774 .iter()
3775 .any(|idx| ignored_indices.contains(idx))
3776 })
3777 .collect()
3778 };
3779
3780 let mut after_added = Vec::new();
3781 let mut after_stacks = Vec::new();
3782 let mut after_external = Vec::new();
3783 let mut out = TriggerProgramOutput {
3784 added: &mut after_added,
3785 added_stacks: &mut after_stacks,
3786 added_external: &mut after_external,
3787 ignored_indices: &mut ignored_indices,
3788 };
3789 self.execute_triggers_for_events(
3790 &after_triggers,
3791 &after_events,
3792 None,
3793 &mut out,
3794 config,
3795 read_epoch,
3796 )?;
3797 Ok(TriggerExpansion {
3798 before: before_added,
3799 before_stacks,
3800 before_external,
3801 after: after_added,
3802 after_stacks,
3803 after_external,
3804 ignored_indices,
3805 })
3806 }
3807
3808 fn execute_triggers_for_events(
3809 &self,
3810 triggers: &[StoredTrigger],
3811 events: &[WriteEvent],
3812 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
3813 out: &mut TriggerProgramOutput<'_>,
3814 config: &TriggerConfig,
3815 read_epoch: Epoch,
3816 ) -> Result<()> {
3817 for event in events {
3818 for trigger in triggers {
3819 if event
3820 .op_indices
3821 .iter()
3822 .any(|idx| out.ignored_indices.contains(idx))
3823 {
3824 break;
3825 }
3826 let matches = {
3827 let cat = self.catalog.read();
3828 trigger_matches_event(trigger, event, &cat)?
3829 };
3830 if !matches {
3831 continue;
3832 }
3833 if let Some(when) = &trigger.when {
3834 if !eval_trigger_expr(when, event)? {
3835 continue;
3836 }
3837 }
3838 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
3839 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
3840 return Err(MongrelError::Conflict(format!(
3841 "trigger recursion cycle detected; trigger stack: {}",
3842 Self::format_trigger_stack(&trigger_stack)
3843 )));
3844 }
3845 let outcome = match staging.as_mut() {
3846 Some(staging) => self.execute_trigger_program(
3847 trigger,
3848 event,
3849 Some(&mut **staging),
3850 out,
3851 &trigger_stack,
3852 config,
3853 read_epoch,
3854 )?,
3855 None => self.execute_trigger_program(
3856 trigger,
3857 event,
3858 None,
3859 out,
3860 &trigger_stack,
3861 config,
3862 read_epoch,
3863 )?,
3864 };
3865 if outcome == TriggerProgramOutcome::Ignore {
3866 out.ignored_indices.extend(event.op_indices.iter().copied());
3867 break;
3868 }
3869 }
3870 }
3871 Ok(())
3872 }
3873
3874 fn trigger_events_for_staging(
3875 &self,
3876 staging: &[(u64, crate::txn::Staged)],
3877 read_epoch: Epoch,
3878 trigger_stacks: Option<&[Vec<String>]>,
3879 ) -> Result<Vec<WriteEvent>> {
3880 use crate::txn::Staged;
3881 use std::collections::{HashMap, VecDeque};
3882
3883 let snapshot = Snapshot::at(read_epoch);
3884 let cat = self.catalog.read();
3885 let mut table_names = HashMap::new();
3886 let mut table_schemas = HashMap::new();
3887 for entry in cat
3888 .tables
3889 .iter()
3890 .filter(|entry| matches!(entry.state, TableState::Live))
3891 {
3892 table_names.insert(entry.table_id, entry.name.clone());
3893 table_schemas.insert(entry.table_id, entry.schema.clone());
3894 }
3895 drop(cat);
3896
3897 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
3898 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
3899 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
3900
3901 for (idx, (table_id, staged)) in staging.iter().enumerate() {
3902 let Some(schema) = table_schemas.get(table_id) else {
3903 continue;
3904 };
3905 let Some(pk) = schema.primary_key() else {
3906 continue;
3907 };
3908 match staged {
3909 Staged::Delete(row_id) => {
3910 let handle = self.table_by_id(*table_id)?;
3911 let Some(row) = handle.lock().get(*row_id, snapshot) else {
3912 continue;
3913 };
3914 let Some(pk_value) = row.columns.get(&pk.id) else {
3915 continue;
3916 };
3917 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
3918 delete_by_key
3919 .entry((*table_id, pk_value.encode_key()))
3920 .or_default()
3921 .push_back(idx);
3922 }
3923 Staged::Put(cells) => {
3924 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
3925 put_by_key
3926 .entry((*table_id, value.encode_key()))
3927 .or_default()
3928 .push_back(idx);
3929 }
3930 }
3931 Staged::Update(row_id, _) => {
3932 let handle = self.table_by_id(*table_id)?;
3933 let row = handle.lock().get(*row_id, snapshot);
3934 if let Some(row) = row {
3935 old_rows.insert(idx, TriggerRowImage::from_row(row));
3936 }
3937 }
3938 Staged::Truncate => {}
3939 }
3940 }
3941
3942 let mut paired_delete = std::collections::HashSet::new();
3943 let mut paired_put = std::collections::HashSet::new();
3944 let mut events = Vec::new();
3945
3946 for (key, deletes) in delete_by_key.iter_mut() {
3947 let Some(puts) = put_by_key.get_mut(key) else {
3948 continue;
3949 };
3950 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
3951 paired_delete.insert(delete_idx);
3952 paired_put.insert(put_idx);
3953 let (table_id, _) = &staging[put_idx];
3954 let Some(table_name) = table_names.get(table_id).cloned() else {
3955 continue;
3956 };
3957 let old = old_rows.get(&delete_idx).cloned();
3958 let new = match &staging[put_idx].1 {
3959 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
3960 _ => None,
3961 };
3962 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
3963 events.push(WriteEvent {
3964 table: table_name,
3965 kind: TriggerEvent::Update,
3966 old,
3967 new,
3968 changed_columns,
3969 op_indices: vec![delete_idx, put_idx],
3970 put_idx: Some(put_idx),
3971 trigger_stack: Self::trigger_stack_for_indices(
3972 trigger_stacks,
3973 &[delete_idx, put_idx],
3974 ),
3975 });
3976 }
3977 }
3978
3979 for (idx, (table_id, staged)) in staging.iter().enumerate() {
3980 let Some(table_name) = table_names.get(table_id).cloned() else {
3981 continue;
3982 };
3983 match staged {
3984 Staged::Put(cells) if !paired_put.contains(&idx) => {
3985 let new = Some(TriggerRowImage::from_cells(cells));
3986 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
3987 events.push(WriteEvent {
3988 table: table_name,
3989 kind: TriggerEvent::Insert,
3990 old: None,
3991 new,
3992 changed_columns,
3993 op_indices: vec![idx],
3994 put_idx: Some(idx),
3995 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
3996 });
3997 }
3998 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
3999 let old = match old_rows.get(&idx).cloned() {
4000 Some(old) => Some(old),
4001 None => {
4002 let handle = self.table_by_id(*table_id)?;
4003 let row = handle.lock().get(*row_id, snapshot);
4004 row.map(TriggerRowImage::from_row)
4005 }
4006 };
4007 let Some(old) = old else {
4008 continue;
4009 };
4010 let changed_columns = old.columns.keys().copied().collect();
4011 events.push(WriteEvent {
4012 table: table_name,
4013 kind: TriggerEvent::Delete,
4014 old: Some(old),
4015 new: None,
4016 changed_columns,
4017 op_indices: vec![idx],
4018 put_idx: None,
4019 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
4020 });
4021 }
4022 Staged::Update(_, cells) => {
4023 let old = old_rows.get(&idx).cloned();
4024 let new = Some(TriggerRowImage::from_cells(cells));
4025 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
4026 events.push(WriteEvent {
4027 table: table_name,
4028 kind: TriggerEvent::Update,
4029 old,
4030 new,
4031 changed_columns,
4032 op_indices: vec![idx],
4033 put_idx: Some(idx),
4034 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
4035 });
4036 }
4037 Staged::Truncate => {}
4038 _ => {}
4039 }
4040 }
4041
4042 Ok(events)
4043 }
4044
4045 #[allow(clippy::too_many_arguments)]
4046 fn execute_trigger_program(
4047 &self,
4048 trigger: &StoredTrigger,
4049 event: &WriteEvent,
4050 staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
4051 out: &mut TriggerProgramOutput<'_>,
4052 trigger_stack: &[String],
4053 config: &TriggerConfig,
4054 read_epoch: Epoch,
4055 ) -> Result<TriggerProgramOutcome> {
4056 let mut event = event.clone();
4057 let mut select_results: HashMap<String, Vec<TriggerRowImage>> = HashMap::new();
4058 self.execute_trigger_steps(
4059 trigger,
4060 &trigger.program.steps,
4061 &mut event,
4062 staging,
4063 out,
4064 trigger_stack,
4065 config,
4066 &mut select_results,
4067 0,
4068 None,
4069 read_epoch,
4070 )
4071 }
4072
4073 #[allow(clippy::too_many_arguments)]
4074 fn execute_trigger_steps(
4075 &self,
4076 trigger: &StoredTrigger,
4077 steps: &[TriggerStep],
4078 event: &mut WriteEvent,
4079 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
4080 out: &mut TriggerProgramOutput<'_>,
4081 trigger_stack: &[String],
4082 config: &TriggerConfig,
4083 select_results: &mut HashMap<String, Vec<TriggerRowImage>>,
4084 depth: u32,
4085 selected: Option<&TriggerRowImage>,
4086 read_epoch: Epoch,
4087 ) -> Result<TriggerProgramOutcome> {
4088 let _ = depth;
4089 for step in steps {
4090 match step {
4091 TriggerStep::SetNew { cells } => {
4092 if trigger.timing != TriggerTiming::Before {
4093 return Err(MongrelError::InvalidArgument(
4094 "SetNew trigger step is only valid in BEFORE triggers".into(),
4095 ));
4096 }
4097 let put_idx = event.put_idx.ok_or_else(|| {
4098 MongrelError::InvalidArgument(
4099 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
4100 )
4101 })?;
4102 let staging = staging.as_deref_mut().ok_or_else(|| {
4103 MongrelError::InvalidArgument(
4104 "SetNew trigger step requires mutable trigger staging".into(),
4105 )
4106 })?;
4107 let row_cells = match staging.get_mut(put_idx).map(|(_, op)| op) {
4108 Some(crate::txn::Staged::Put(cells))
4109 | Some(crate::txn::Staged::Update(_, cells)) => cells,
4110 _ => {
4111 return Err(MongrelError::InvalidArgument(
4112 "SetNew trigger step target row is not mutable".into(),
4113 ))
4114 }
4115 };
4116 for (column_id, value) in eval_trigger_cells(cells, event, selected)? {
4117 row_cells.retain(|(id, _)| *id != column_id);
4118 row_cells.push((column_id, value.clone()));
4119 if let Some(new) = &mut event.new {
4120 new.columns.insert(column_id, value);
4121 }
4122 }
4123 row_cells.sort_by_key(|(id, _)| *id);
4124 }
4125 TriggerStep::Insert { table, cells } => {
4126 let cells = eval_trigger_cells(cells, event, selected)?;
4127 if let Ok(table_id) = self.table_id(table) {
4128 out.added.push((table_id, crate::txn::Staged::Put(cells)));
4129 out.added_stacks.push(trigger_stack.to_vec());
4130 } else if self.external_table(table).is_some() {
4131 out.added_external.push(ExternalTriggerWrite::Insert {
4132 table: table.clone(),
4133 cells,
4134 });
4135 } else {
4136 return Err(MongrelError::NotFound(format!(
4137 "trigger {:?} insert target {table:?} not found",
4138 trigger.name
4139 )));
4140 }
4141 }
4142 TriggerStep::UpdateByPk { table, pk, cells } => {
4143 let pk = eval_trigger_value(pk, event, selected)?;
4144 let cells = eval_trigger_cells(cells, event, selected)?;
4145 if self.external_table(table).is_some() {
4146 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
4147 table: table.clone(),
4148 pk,
4149 cells,
4150 });
4151 } else {
4152 let row_id = self
4153 .table(table)?
4154 .lock()
4155 .lookup_pk(&pk.encode_key())
4156 .ok_or_else(|| {
4157 MongrelError::NotFound(format!(
4158 "trigger {:?} update target not found",
4159 trigger.name
4160 ))
4161 })?;
4162 let handle = self.table(table)?;
4163 let snapshot = Snapshot::at(self.epoch.visible());
4164 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
4165 MongrelError::NotFound(format!(
4166 "trigger {:?} update target not visible",
4167 trigger.name
4168 ))
4169 })?;
4170 let mut merged = old.columns;
4171 for (column_id, value) in cells {
4172 merged.insert(column_id, value);
4173 }
4174 out.added.push((
4175 self.table_id(table)?,
4176 crate::txn::Staged::Update(row_id, merged.into_iter().collect()),
4177 ));
4178 out.added_stacks.push(trigger_stack.to_vec());
4179 }
4180 }
4181 TriggerStep::DeleteByPk { table, pk } => {
4182 let pk = eval_trigger_value(pk, event, selected)?;
4183 if self.external_table(table).is_some() {
4184 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
4185 table: table.clone(),
4186 pk,
4187 });
4188 } else {
4189 let row_id = self
4190 .table(table)?
4191 .lock()
4192 .lookup_pk(&pk.encode_key())
4193 .ok_or_else(|| {
4194 MongrelError::NotFound(format!(
4195 "trigger {:?} delete target not found",
4196 trigger.name
4197 ))
4198 })?;
4199 out.added
4200 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
4201 out.added_stacks.push(trigger_stack.to_vec());
4202 }
4203 }
4204 TriggerStep::Select {
4205 id,
4206 table,
4207 conditions,
4208 } => {
4209 let schema = self.table(table)?.lock().schema().clone();
4210 let snapshot = Snapshot::at(read_epoch);
4211 let rows = self.table(table)?.lock().visible_rows(snapshot)?;
4212 let mut matched = Vec::new();
4213 for row in rows {
4214 let image = TriggerRowImage::from_row(row);
4215 let passes = conditions
4216 .iter()
4217 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
4218 .collect::<Result<Vec<_>>>()?
4219 .into_iter()
4220 .all(|b| b);
4221 if passes {
4222 matched.push(image);
4223 }
4224 }
4225 if let Some(pk) = schema.primary_key() {
4226 matched.sort_by(|a, b| {
4227 let av = a.columns.get(&pk.id).unwrap_or(&Value::Null);
4228 let bv = b.columns.get(&pk.id).unwrap_or(&Value::Null);
4229 value_order(av, bv).unwrap_or(std::cmp::Ordering::Equal)
4230 });
4231 }
4232 select_results.insert(id.clone(), matched);
4233 }
4234 TriggerStep::Foreach { id, steps } => {
4235 let rows = select_results.get(id).ok_or_else(|| {
4236 MongrelError::InvalidArgument(format!(
4237 "trigger {:?} foreach references unknown select id {id:?}",
4238 trigger.name
4239 ))
4240 })?;
4241 if rows.len() > config.max_loop_iterations as usize {
4242 return Err(MongrelError::InvalidArgument(format!(
4243 "trigger {:?} foreach exceeded max_loop_iterations ({})",
4244 trigger.name, config.max_loop_iterations
4245 )));
4246 }
4247 for row in rows.clone() {
4248 let result = self.execute_trigger_steps(
4249 trigger,
4250 steps,
4251 event,
4252 staging.as_deref_mut(),
4253 out,
4254 trigger_stack,
4255 config,
4256 select_results,
4257 depth + 1,
4258 Some(&row),
4259 read_epoch,
4260 )?;
4261 if result == TriggerProgramOutcome::Ignore {
4262 return Ok(TriggerProgramOutcome::Ignore);
4263 }
4264 }
4265 }
4266 TriggerStep::DeleteWhere { table, conditions } => {
4267 let schema = self.table(table)?.lock().schema().clone();
4268 let snapshot = Snapshot::at(read_epoch);
4269 let rows = self.table(table)?.lock().visible_rows(snapshot)?;
4270 let table_id = self.table_id(table)?;
4271 let mut to_delete = Vec::new();
4272 for row in rows {
4273 let image = TriggerRowImage::from_row(row.clone());
4274 let passes = conditions
4275 .iter()
4276 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
4277 .collect::<Result<Vec<_>>>()?
4278 .into_iter()
4279 .all(|b| b);
4280 if passes {
4281 to_delete.push((table_id, row.row_id));
4282 }
4283 }
4284 for (table_id, row_id) in to_delete {
4285 out.added
4286 .push((table_id, crate::txn::Staged::Delete(row_id)));
4287 out.added_stacks.push(trigger_stack.to_vec());
4288 }
4289 }
4290 TriggerStep::UpdateWhere {
4291 table,
4292 conditions,
4293 cells,
4294 } => {
4295 let schema = self.table(table)?.lock().schema().clone();
4296 let snapshot = Snapshot::at(read_epoch);
4297 let rows = self.table(table)?.lock().visible_rows(snapshot)?;
4298 let table_id = self.table_id(table)?;
4299 let mut to_update = Vec::new();
4300 for row in rows {
4301 let image = TriggerRowImage::from_row(row.clone());
4302 let passes = conditions
4303 .iter()
4304 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
4305 .collect::<Result<Vec<_>>>()?
4306 .into_iter()
4307 .all(|b| b);
4308 if passes {
4309 let new_cells = cells
4310 .iter()
4311 .map(|cell| {
4312 Ok((
4313 cell.column_id,
4314 eval_trigger_value(&cell.value, event, Some(&image))?,
4315 ))
4316 })
4317 .collect::<Result<Vec<_>>>()?;
4318 let mut merged = row.columns.clone();
4319 for (column_id, value) in new_cells {
4320 merged.insert(column_id, value);
4321 }
4322 to_update.push((table_id, row.row_id, merged));
4323 }
4324 }
4325 for (table_id, row_id, merged) in to_update {
4326 out.added.push((
4327 table_id,
4328 crate::txn::Staged::Update(row_id, merged.into_iter().collect()),
4329 ));
4330 out.added_stacks.push(trigger_stack.to_vec());
4331 }
4332 }
4333 TriggerStep::Raise { action, message } => match action {
4334 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
4335 TriggerRaiseAction::Abort
4336 | TriggerRaiseAction::Fail
4337 | TriggerRaiseAction::Rollback => {
4338 let message = eval_trigger_value(message, event, selected)?;
4339 return Err(MongrelError::Conflict(format!(
4340 "trigger {:?} raised: {}; trigger stack: {}",
4341 trigger.name,
4342 trigger_message(message),
4343 Self::format_trigger_stack(trigger_stack)
4344 )));
4345 }
4346 },
4347 }
4348 }
4349 Ok(TriggerProgramOutcome::Continue)
4350 }
4351
4352 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
4353 let Some(stacks) = stacks else {
4354 return Vec::new();
4355 };
4356 let mut out = Vec::new();
4357 for idx in indices {
4358 let Some(stack) = stacks.get(*idx) else {
4359 continue;
4360 };
4361 for name in stack {
4362 if !out.iter().any(|existing| existing == name) {
4363 out.push(name.clone());
4364 }
4365 }
4366 }
4367 out
4368 }
4369
4370 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
4371 let mut out = stack.to_vec();
4372 out.push(trigger_name.to_string());
4373 out
4374 }
4375
4376 fn format_trigger_stack(stack: &[String]) -> String {
4377 if stack.is_empty() {
4378 "<root>".into()
4379 } else {
4380 stack.join(" -> ")
4381 }
4382 }
4383
4384 fn validate_constraints(
4399 &self,
4400 staging: &mut Vec<(u64, crate::txn::Staged)>,
4401 read_epoch: Epoch,
4402 ) -> Result<()> {
4403 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
4404 use crate::memtable::Row;
4405 use crate::txn::Staged;
4406 use std::collections::HashSet;
4407
4408 let snapshot = Snapshot::at(read_epoch);
4409 let cat = self.catalog.read();
4410
4411 let live: Vec<(u64, &str, &crate::schema::Schema)> = cat
4413 .tables
4414 .iter()
4415 .filter(|e| matches!(e.state, TableState::Live))
4416 .map(|e| (e.table_id, e.name.as_str(), &e.schema))
4417 .collect();
4418
4419 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
4421 if !any_constraints {
4422 return Ok(());
4423 }
4424
4425 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
4427 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
4428 if let Some(r) = rows_cache.get(&table_id) {
4429 return Ok(r.clone());
4430 }
4431 let handle = self.table_by_id(table_id)?;
4432 let rows = handle.lock().visible_rows(snapshot)?;
4433 rows_cache.insert(table_id, rows.clone());
4434 Ok(rows)
4435 };
4436
4437 let mut processed_updates = HashSet::new();
4442 type PendingUpdate = (usize, u64, crate::rowid::RowId, Vec<(u16, Value)>);
4443 loop {
4444 let updates: Vec<PendingUpdate> = staging
4445 .iter()
4446 .enumerate()
4447 .filter_map(|(index, (table_id, op))| match op {
4448 Staged::Update(row_id, cells) if !processed_updates.contains(&index) => {
4449 Some((index, *table_id, *row_id, cells.clone()))
4450 }
4451 _ => None,
4452 })
4453 .collect();
4454 if updates.is_empty() {
4455 break;
4456 }
4457 let mut new_ops = Vec::new();
4458 for (index, table_id, row_id, new_cells) in updates {
4459 processed_updates.insert(index);
4460 let Some(tname) = live
4461 .iter()
4462 .find(|(id, _, _)| *id == table_id)
4463 .map(|(_, name, _)| *name)
4464 else {
4465 continue;
4466 };
4467 let Some(old_row) = self.table_by_id(table_id)?.lock().get(row_id, snapshot) else {
4468 continue;
4469 };
4470 let new_map: HashMap<u16, Value> = new_cells.iter().cloned().collect();
4471 for (child_id, _child_name, child_schema) in &live {
4472 for fk in &child_schema.constraints.foreign_keys {
4473 if fk.ref_table != tname {
4474 continue;
4475 }
4476 let Some(old_key) = encode_composite_key(&fk.ref_columns, &old_row.columns)
4477 else {
4478 continue;
4479 };
4480 if encode_composite_key(&fk.ref_columns, &new_map).as_deref()
4481 == Some(old_key.as_slice())
4482 {
4483 continue;
4484 }
4485 if fk.on_update == FkAction::Restrict {
4486 continue;
4487 }
4488 let child_rows = load_rows(*child_id)?;
4489 for child in child_rows {
4490 if encode_composite_key(&fk.columns, &child.columns).as_deref()
4491 != Some(old_key.as_slice())
4492 {
4493 continue;
4494 }
4495 if staging.iter().any(|(id, op)| {
4496 *id == *child_id
4497 && matches!(op, Staged::Delete(id) if *id == child.row_id)
4498 }) {
4499 continue;
4500 }
4501 let mut cells: Vec<(u16, Value)> = child
4502 .columns
4503 .iter()
4504 .map(|(column_id, value)| (*column_id, value.clone()))
4505 .collect();
4506 for (child_column, parent_column) in
4507 fk.columns.iter().zip(&fk.ref_columns)
4508 {
4509 cells.retain(|(column_id, _)| column_id != child_column);
4510 let value = match fk.on_update {
4511 FkAction::Cascade => {
4512 new_map.get(parent_column).cloned().unwrap_or(Value::Null)
4513 }
4514 FkAction::SetNull => Value::Null,
4515 FkAction::Restrict => unreachable!(),
4516 };
4517 cells.push((*child_column, value));
4518 }
4519 cells.sort_by_key(|(column_id, _)| *column_id);
4520 if let Some(existing_index) = staging.iter().position(|(id, op)| {
4521 *id == *child_id
4522 && matches!(op, Staged::Update(id, _) if *id == child.row_id)
4523 }) {
4524 if let Staged::Update(_, existing) = &mut staging[existing_index].1
4525 {
4526 if *existing != cells {
4527 *existing = cells;
4528 processed_updates.remove(&existing_index);
4529 }
4530 }
4531 } else {
4532 new_ops.push((*child_id, Staged::Update(child.row_id, cells)));
4533 }
4534 }
4535 }
4536 }
4537 }
4538 staging.extend(new_ops);
4539 }
4540
4541 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
4546 loop {
4547 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
4548 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
4549 .iter()
4550 .filter_map(|(t, op)| match op {
4551 Staged::Delete(rid) => Some((*t, *rid)),
4552 _ => None,
4553 })
4554 .collect();
4555 for (table_id, rid) in deletes {
4556 if !cascaded.insert((table_id, rid.0)) {
4557 continue;
4558 }
4559 let Some(tname) = live
4560 .iter()
4561 .find(|(t, _, _)| *t == table_id)
4562 .map(|(_, n, _)| *n)
4563 else {
4564 continue;
4565 };
4566 let parent_handle = self.table_by_id(table_id)?;
4567 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
4568 continue;
4569 };
4570 for (child_id, _child_name, child_schema) in &live {
4571 for fk in &child_schema.constraints.foreign_keys {
4572 if fk.ref_table != tname {
4573 continue;
4574 }
4575 let Some(parent_key) =
4576 encode_composite_key(&fk.ref_columns, &parent_row.columns)
4577 else {
4578 continue;
4579 };
4580 let key_preserved = staging.iter().any(|(t, op)| {
4589 if *t != table_id {
4590 return false;
4591 }
4592 let Staged::Put(cells) = op else {
4593 return false;
4594 };
4595 let map: HashMap<u16, crate::memtable::Value> =
4596 cells.iter().cloned().collect();
4597 encode_composite_key(&fk.ref_columns, &map).as_deref()
4598 == Some(parent_key.as_slice())
4599 });
4600 if key_preserved {
4601 continue;
4602 }
4603 match fk.on_delete {
4604 FkAction::Restrict => continue,
4605 FkAction::Cascade => {
4606 let child_rows = load_rows(*child_id)?;
4607 for cr in &child_rows {
4608 if !cascaded.contains(&(*child_id, cr.row_id.0))
4609 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
4610 == Some(parent_key.as_slice())
4611 {
4612 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
4613 }
4614 }
4615 }
4616 FkAction::SetNull => {
4617 let child_rows = load_rows(*child_id)?;
4618 for cr in &child_rows {
4619 if !cascaded.contains(&(*child_id, cr.row_id.0))
4620 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
4621 == Some(parent_key.as_slice())
4622 {
4623 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
4626 .columns
4627 .iter()
4628 .map(|(k, v)| (*k, v.clone()))
4629 .collect();
4630 for cid in &fk.columns {
4631 cells.retain(|(k, _)| k != cid);
4632 cells.push((*cid, crate::memtable::Value::Null));
4633 }
4634 new_ops.push((*child_id, Staged::Update(cr.row_id, cells)));
4635 }
4636 }
4637 }
4638 }
4639 }
4640 }
4641 }
4642 if new_ops.is_empty() {
4643 break;
4644 }
4645 staging.extend(new_ops);
4646 }
4647
4648 let staged_deletes: HashSet<(u64, u64)> = staging
4652 .iter()
4653 .filter_map(|(t, op)| match op {
4654 Staged::Delete(rid) | Staged::Update(rid, _) => Some((*t, rid.0)),
4655 _ => None,
4656 })
4657 .collect();
4658
4659 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
4661
4662 for (table_id, op) in staging.iter() {
4664 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id).copied()
4665 else {
4666 continue;
4667 };
4668 let cells_map: HashMap<u16, crate::memtable::Value>;
4669 match op {
4670 Staged::Put(cells) | Staged::Update(_, cells) => {
4671 cells_map = cells.iter().cloned().collect();
4672
4673 if !schema.constraints.checks.is_empty() {
4675 validate_checks(&schema.constraints.checks, &cells_map)?;
4676 }
4677
4678 for uc in &schema.constraints.uniques {
4680 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
4681 continue; };
4683 let marker = (*table_id, uc.id, key.clone());
4684 if !seen_unique.insert(marker) {
4685 return Err(MongrelError::Conflict(format!(
4686 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
4687 uc.name
4688 )));
4689 }
4690 let rows = load_rows(*table_id)?;
4691 for r in &rows {
4692 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
4695 continue;
4696 }
4697 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
4698 if theirs == key {
4699 return Err(MongrelError::Conflict(format!(
4700 "UNIQUE constraint '{}' on table '{tname}' violated",
4701 uc.name
4702 )));
4703 }
4704 }
4705 }
4706 }
4707
4708 for fk in &schema.constraints.foreign_keys {
4710 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
4711 continue; };
4713 let Some(parent_id) = cat
4714 .tables
4715 .iter()
4716 .find(|t| t.name == fk.ref_table)
4717 .map(|t| t.table_id)
4718 else {
4719 return Err(MongrelError::InvalidArgument(format!(
4720 "FOREIGN KEY '{}' references unknown table '{}'",
4721 fk.name, fk.ref_table
4722 )));
4723 };
4724 let parent_rows = load_rows(parent_id)?;
4725 let mut found = false;
4726 for r in &parent_rows {
4727 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
4728 continue;
4729 }
4730 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
4731 if pkey == child_key {
4732 found = true;
4733 break;
4734 }
4735 }
4736 }
4737 if !found {
4744 for (st_table, st_op) in staging.iter() {
4745 if *st_table != parent_id {
4746 continue;
4747 }
4748 if let Staged::Put(pcells) | Staged::Update(_, pcells) = st_op {
4749 let pmap: HashMap<u16, crate::memtable::Value> =
4750 pcells.iter().cloned().collect();
4751 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &pmap)
4752 {
4753 if pkey == child_key {
4754 found = true;
4755 break;
4756 }
4757 }
4758 }
4759 }
4760 }
4761 if !found {
4762 return Err(MongrelError::Conflict(format!(
4763 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
4764 fk.name, fk.ref_table
4765 )));
4766 }
4767 }
4768
4769 if let Staged::Update(row_id, _) = op {
4774 let parent_handle = self.table_by_id(*table_id)?;
4775 let Some(old_parent) = parent_handle.lock().get(*row_id, snapshot) else {
4776 continue;
4777 };
4778 for (child_id, child_name, child_schema) in &live {
4779 for fk in &child_schema.constraints.foreign_keys {
4780 if fk.ref_table != tname || fk.on_update != FkAction::Restrict {
4781 continue;
4782 }
4783 let Some(old_key) =
4784 encode_composite_key(&fk.ref_columns, &old_parent.columns)
4785 else {
4786 continue;
4787 };
4788 if encode_composite_key(&fk.ref_columns, &cells_map).as_deref()
4789 == Some(old_key.as_slice())
4790 {
4791 continue;
4792 }
4793 for child in load_rows(*child_id)? {
4794 if encode_composite_key(&fk.columns, &child.columns).as_deref()
4795 != Some(old_key.as_slice())
4796 {
4797 continue;
4798 }
4799 let replacement = staging.iter().find_map(|(id, op)| {
4800 if *id != *child_id {
4801 return None;
4802 }
4803 match op {
4804 Staged::Delete(id) if *id == child.row_id => Some(None),
4805 Staged::Update(id, cells) if *id == child.row_id => {
4806 let map: HashMap<u16, Value> =
4807 cells.iter().cloned().collect();
4808 Some(encode_composite_key(&fk.columns, &map))
4809 }
4810 _ => None,
4811 }
4812 });
4813 if replacement.is_some_and(|key| {
4814 key.as_deref() != Some(old_key.as_slice())
4815 }) {
4816 continue;
4817 }
4818 return Err(MongrelError::Conflict(format!(
4819 "FOREIGN KEY '{}' on table '{child_name}' restricts update (parent key referenced)",
4820 fk.name
4821 )));
4822 }
4823 }
4824 }
4825 }
4826 }
4827 Staged::Delete(rid) => {
4828 let parent_handle = self.table_by_id(*table_id)?;
4832 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
4833 continue;
4834 };
4835 for (child_id, child_name, child_schema) in &live {
4836 for fk in &child_schema.constraints.foreign_keys {
4837 if fk.ref_table != tname || fk.on_delete != FkAction::Restrict {
4838 continue;
4839 }
4840 let Some(parent_key) =
4841 encode_composite_key(&fk.ref_columns, &parent_row.columns)
4842 else {
4843 continue;
4844 };
4845 let child_rows = load_rows(*child_id)?;
4846 for r in &child_rows {
4847 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
4850 continue;
4851 }
4852 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
4853 if ck == parent_key {
4854 return Err(MongrelError::Conflict(format!(
4855 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
4856 fk.name
4857 )));
4858 }
4859 }
4860 }
4861 }
4862 }
4863 }
4864 Staged::Truncate => {
4865 for (child_id, child_name, child_schema) in &live {
4869 for fk in &child_schema.constraints.foreign_keys {
4870 if fk.ref_table != tname {
4871 continue;
4872 }
4873 let child_rows = load_rows(*child_id)?;
4874 if child_rows
4875 .iter()
4876 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
4877 {
4878 return Err(MongrelError::Conflict(format!(
4879 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
4880 fk.name
4881 )));
4882 }
4883 }
4884 }
4885 }
4886 }
4887 }
4888 Ok(())
4889 }
4890
4891 fn validate_security_writes(
4892 &self,
4893 staging: &[(u64, crate::txn::Staged)],
4894 read_epoch: Epoch,
4895 explicit_principal: Option<&crate::auth::Principal>,
4896 ) -> Result<()> {
4897 use crate::security::PolicyCommand;
4898 use crate::txn::Staged;
4899
4900 let catalog = self.catalog.read();
4901 if catalog.security.rls_tables.is_empty() {
4902 return Ok(());
4903 }
4904 let security = catalog.security.clone();
4905 let table_names = catalog
4906 .tables
4907 .iter()
4908 .filter(|entry| matches!(entry.state, TableState::Live))
4909 .map(|entry| (entry.table_id, entry.name.clone()))
4910 .collect::<HashMap<_, _>>();
4911 drop(catalog);
4912 if !staging.iter().any(|(table_id, _)| {
4913 table_names
4914 .get(table_id)
4915 .is_some_and(|table| security.rls_enabled(table))
4916 }) {
4917 return Ok(());
4918 }
4919 let cached = self.principal.read().clone();
4920 let principal = explicit_principal
4921 .or(cached.as_ref())
4922 .ok_or(MongrelError::AuthRequired)?;
4923
4924 for (table_id, operation) in staging {
4925 let Some(table) = table_names.get(table_id) else {
4926 continue;
4927 };
4928 if !security.rls_enabled(table) || principal.is_admin {
4929 continue;
4930 }
4931 let denied = |command| MongrelError::PermissionDenied {
4932 required: match command {
4933 PolicyCommand::Insert => crate::auth::Permission::Insert {
4934 table: table.clone(),
4935 },
4936 PolicyCommand::Update => crate::auth::Permission::Update {
4937 table: table.clone(),
4938 },
4939 PolicyCommand::Delete | PolicyCommand::All | PolicyCommand::Select => {
4940 crate::auth::Permission::Delete {
4941 table: table.clone(),
4942 }
4943 }
4944 },
4945 principal: principal.username.clone(),
4946 };
4947 match operation {
4948 Staged::Put(cells) => {
4949 let mut row = crate::memtable::Row::new(RowId(0), Epoch(read_epoch.0));
4950 row.columns.extend(cells.iter().cloned());
4951 if !security.row_allowed(table, PolicyCommand::Insert, &row, principal, true) {
4952 return Err(denied(PolicyCommand::Insert));
4953 }
4954 }
4955 Staged::Update(row_id, cells) => {
4956 let old = self
4957 .table_by_id(*table_id)?
4958 .lock()
4959 .get(*row_id, Snapshot::at(read_epoch))
4960 .ok_or_else(|| {
4961 MongrelError::NotFound(format!("row {} not found", row_id.0))
4962 })?;
4963 if !security.row_allowed(table, PolicyCommand::Update, &old, principal, false) {
4964 return Err(denied(PolicyCommand::Update));
4965 }
4966 let mut new = crate::memtable::Row::new(*row_id, Epoch(read_epoch.0));
4967 new.columns.extend(cells.iter().cloned());
4968 if !security.row_allowed(table, PolicyCommand::Update, &new, principal, true) {
4969 return Err(denied(PolicyCommand::Update));
4970 }
4971 }
4972 Staged::Delete(row_id) => {
4973 let old = self
4974 .table_by_id(*table_id)?
4975 .lock()
4976 .get(*row_id, Snapshot::at(read_epoch))
4977 .ok_or_else(|| {
4978 MongrelError::NotFound(format!("row {} not found", row_id.0))
4979 })?;
4980 if !security.row_allowed(table, PolicyCommand::Delete, &old, principal, false) {
4981 return Err(denied(PolicyCommand::Delete));
4982 }
4983 }
4984 Staged::Truncate => return Err(denied(PolicyCommand::Delete)),
4985 }
4986 }
4987 Ok(())
4988 }
4989
4990 #[allow(clippy::too_many_arguments)]
4997 pub(crate) fn commit_transaction_with_external_states(
4998 &self,
4999 txn_id: u64,
5000 read_epoch: Epoch,
5001 mut staging: Vec<(u64, crate::txn::Staged)>,
5002 external_states: Vec<(String, Vec<u8>)>,
5003 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
5004 security_principal: Option<crate::auth::Principal>,
5005 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
5006 ) -> Result<Epoch> {
5007 use crate::memtable::Row;
5008 use crate::txn::{Staged, StagedOp, WriteKey};
5009 use crate::wal::Op;
5010 use std::collections::hash_map::DefaultHasher;
5011 use std::hash::{Hash, Hasher};
5012 use std::sync::atomic::Ordering;
5013
5014 if self.read_only {
5015 return Err(MongrelError::ReadOnlyReplica);
5016 }
5017 let _replication_guard = self.replication_barrier.read();
5018 if self.poisoned.load(Ordering::Relaxed) {
5019 return Err(MongrelError::Other(
5020 "database poisoned by fsync error".into(),
5021 ));
5022 }
5023 let mut external_states = dedup_external_states(external_states);
5024 if !external_states.is_empty() {
5025 let cat = self.catalog.read();
5026 for (name, _) in &external_states {
5027 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
5028 return Err(MongrelError::NotFound(format!(
5029 "external table {name:?} not found"
5030 )));
5031 }
5032 }
5033 }
5034 let prepared_materialized_views = {
5035 let mut deduplicated = HashMap::new();
5036 for definition in materialized_view_updates {
5037 if definition.name.is_empty() || definition.query.trim().is_empty() {
5038 return Err(MongrelError::InvalidArgument(
5039 "materialized view name and query must not be empty".into(),
5040 ));
5041 }
5042 deduplicated.insert(definition.name.clone(), definition);
5043 }
5044 let catalog = self.catalog.read();
5045 let mut prepared = Vec::with_capacity(deduplicated.len());
5046 for definition in deduplicated.into_values() {
5047 let table_id = catalog
5048 .live(&definition.name)
5049 .ok_or_else(|| {
5050 MongrelError::NotFound(format!(
5051 "materialized view table {:?} not found",
5052 definition.name
5053 ))
5054 })?
5055 .table_id;
5056 prepared.push((table_id, definition));
5057 }
5058 prepared.sort_by(|left, right| left.1.name.cmp(&right.1.name));
5059 prepared
5060 };
5061
5062 self.fill_auto_increment_for_staging(&mut staging)?;
5065 self.expand_table_triggers(
5066 &mut staging,
5067 read_epoch,
5068 external_trigger_bridge,
5069 &mut external_states,
5070 )?;
5071 self.fill_auto_increment_for_staging(&mut staging)?;
5072 external_states = dedup_external_states(external_states);
5073
5074 self.validate_constraints(&mut staging, read_epoch)?;
5079 self.validate_security_writes(&staging, read_epoch, security_principal.as_ref())?;
5080 let mut normalized = Vec::with_capacity(staging.len() * 2);
5081 for (table_id, op) in staging {
5082 match op {
5083 crate::txn::Staged::Update(row_id, cells) => {
5084 normalized.push((table_id, crate::txn::Staged::Delete(row_id)));
5085 normalized.push((table_id, crate::txn::Staged::Put(cells)));
5086 }
5087 op => normalized.push((table_id, op)),
5088 }
5089 }
5090 staging = normalized;
5091 let has_changes = !staging.is_empty()
5092 || !external_states.is_empty()
5093 || !prepared_materialized_views.is_empty();
5094 let truncated_tables: HashSet<u64> = staging
5095 .iter()
5096 .filter_map(|(table_id, op)| matches!(op, Staged::Truncate).then_some(*table_id))
5097 .collect();
5098
5099 let write_keys = {
5100 let cat = self.catalog.read();
5101 let mut keys: Vec<WriteKey> = Vec::new();
5102 for (table_id, staged) in &staging {
5103 match staged {
5104 Staged::Put(cells) => {
5105 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
5106 for col in &entry.schema.columns {
5107 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
5108 if let Some((_, val)) =
5109 cells.iter().find(|(id, _)| *id == col.id)
5110 {
5111 let mut h = DefaultHasher::new();
5112 val.encode_key().hash(&mut h);
5113 keys.push(WriteKey::Unique {
5114 table_id: *table_id,
5115 index_id: 0,
5116 key_hash: h.finish(),
5117 });
5118 }
5119 }
5120 }
5121 for uc in &entry.schema.constraints.uniques {
5128 if let Some(key_bytes) = crate::constraint::encode_composite_key(
5129 &uc.columns,
5130 &cells.iter().cloned().collect(),
5131 ) {
5132 let mut h = DefaultHasher::new();
5133 key_bytes.hash(&mut h);
5134 keys.push(WriteKey::Unique {
5135 table_id: *table_id,
5136 index_id: uc.id | 0x8000,
5137 key_hash: h.finish(),
5138 });
5139 }
5140 }
5141 }
5142 }
5143 Staged::Delete(rid) => keys.push(WriteKey::Row {
5144 table_id: *table_id,
5145 row_id: rid.0,
5146 }),
5147 Staged::Truncate => keys.push(WriteKey::Table {
5148 table_id: *table_id,
5149 }),
5150 Staged::Update(_, _) => unreachable!("updates normalized before prepare"),
5151 }
5152 }
5153 for (name, _) in &external_states {
5154 let mut h = DefaultHasher::new();
5155 name.hash(&mut h);
5156 keys.push(WriteKey::Unique {
5157 table_id: EXTERNAL_TABLE_ID,
5158 index_id: 0,
5159 key_hash: h.finish(),
5160 });
5161 }
5162 keys
5163 };
5164
5165 let min_active = self.active_txns.min_read_epoch();
5167 if min_active < u64::MAX {
5168 self.conflicts.prune_below(Epoch(min_active));
5169 }
5170
5171 if self.conflicts.conflicts(&write_keys, read_epoch) {
5175 return Err(MongrelError::Conflict(
5176 "write-write conflict (pre-validate, first-committer-wins)".into(),
5177 ));
5178 }
5179 let pre_validate_version = self.conflicts.version();
5180
5181 let mut spilled: Vec<SpilledRun> = Vec::new();
5185 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
5186 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
5190 {
5191 let mut table_bytes: HashMap<u64, usize> = HashMap::new();
5192 for (table_id, staged) in &staging {
5193 if let Staged::Put(cells) = staged {
5194 *table_bytes.entry(*table_id).or_default() += cells.len() * 16;
5195 }
5196 }
5197 let tables = self.tables.read();
5198 for (&table_id, &bytes) in &table_bytes {
5199 if bytes as u64
5200 <= self
5201 .spill_threshold
5202 .load(std::sync::atomic::Ordering::Relaxed)
5203 {
5204 continue;
5205 }
5206 let Some(handle) = tables.get(&table_id) else {
5207 continue;
5208 };
5209 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
5210 let mut t = handle.lock();
5211 let tdir = t.table_dir().to_path_buf();
5212 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
5213 std::fs::create_dir_all(&txn_dir)?;
5214 let run_id = t.alloc_run_id() as u128;
5215 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
5216
5217 let mut rows: Vec<Row> = Vec::new();
5218 for (tid, staged) in &staging {
5219 if *tid != table_id {
5220 continue;
5221 }
5222 if let Staged::Put(cells) = staged {
5223 t.validate_cells_not_null(cells)?;
5224 let row_id = t.alloc_row_id();
5225 let mut row = Row::new(row_id, Epoch(0));
5226 for (c, v) in cells {
5227 row.columns.insert(*c, v.clone());
5228 }
5229 rows.push(row);
5230 }
5231 }
5232 let schema = t.schema_ref().clone();
5233 let kek = t.kek_ref().cloned();
5234 let specs = t.indexable_column_specs();
5235 drop(t);
5236
5237 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
5238 .uniform_epoch(true);
5239 if let Some(ref kek) = kek {
5240 writer = writer.with_encryption(kek.as_ref(), specs);
5241 }
5242 let header = writer.write(&pending_path, &rows)?;
5243 let row_count = header.row_count;
5244 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
5245 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
5246
5247 spilled.push(SpilledRun {
5248 table_id,
5249 run_id,
5250 pending_path,
5251 rows,
5252 row_count,
5253 min_rid,
5254 max_rid,
5255 });
5256 spilled_tables.insert(table_id);
5257 }
5258 }
5259
5260 if spill_guard.is_some() {
5262 if let Some(hook) = self.spill_hook.lock().as_ref() {
5263 hook();
5264 }
5265 }
5266
5267 let mut prebuilt: Vec<Option<Row>> = Vec::with_capacity(staging.len());
5278 let mut delete_images: Vec<Option<Row>> = Vec::with_capacity(staging.len());
5279 {
5280 let tables = self.tables.read();
5281 for (table_id, staged) in &staging {
5282 match staged {
5283 Staged::Put(cells) if !spilled_tables.contains(table_id) => {
5284 let handle = tables.get(table_id).ok_or_else(|| {
5285 MongrelError::NotFound(format!("table {table_id} not mounted"))
5286 })?;
5287 let mut t = handle.lock();
5288 t.validate_cells_not_null(cells)?;
5289 let row_id = t.alloc_row_id();
5290 drop(t);
5291 let mut row = Row::new(row_id, Epoch(0));
5292 for (c, v) in cells {
5293 row.columns.insert(*c, v.clone());
5294 }
5295 prebuilt.push(Some(row));
5296 delete_images.push(None);
5297 }
5298 Staged::Delete(row_id) => {
5299 let before = tables.get(table_id).and_then(|handle| {
5300 handle.lock().get(*row_id, Snapshot::at(read_epoch))
5301 });
5302 prebuilt.push(None);
5303 delete_images.push(before);
5304 }
5305 Staged::Put(_) | Staged::Truncate => {
5306 prebuilt.push(None);
5307 delete_images.push(None);
5308 }
5309 Staged::Update(_, _) => unreachable!("updates normalized before prepare"),
5310 }
5311 }
5312 }
5313
5314 let mut prepared_external = Vec::with_capacity(external_states.len());
5315 for (name, state) in &external_states {
5316 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
5317 prepared_external.push((name.clone(), state.clone(), pending));
5318 }
5319
5320 let added_runs: Vec<crate::wal::AddedRun> = spilled
5322 .iter()
5323 .map(|s| crate::wal::AddedRun {
5324 table_id: s.table_id,
5325 run_id: s.run_id,
5326 row_count: s.row_count,
5327 level: 0,
5328 min_row_id: s.min_rid,
5329 max_row_id: s.max_rid,
5330 content_hash: [0u8; 32],
5331 })
5332 .collect();
5333 let (new_epoch, mut _epoch_guard, applies, committed_materialized_views, commit_seq) = {
5334 let mut wal = self.shared_wal.lock();
5335
5336 if self.conflicts.version() != pre_validate_version
5341 && self.conflicts.conflicts(&write_keys, read_epoch)
5342 {
5343 drop(wal);
5347 for s in &spilled {
5348 if let Some(parent) = s.pending_path.parent() {
5349 let _ = std::fs::remove_dir_all(parent);
5350 }
5351 }
5352 for (_, _, pending) in &prepared_external {
5353 let _ = std::fs::remove_file(pending);
5354 }
5355 return Err(MongrelError::Conflict(
5356 "write-write conflict (sequencer delta re-check)".into(),
5357 ));
5358 }
5359
5360 let new_epoch = self.epoch.bump_assigned();
5361 let _epoch_guard = EpochGuard::new(self.epoch.as_ref(), new_epoch);
5362 let mut applies: Vec<(u64, Vec<StagedOp>)> = Vec::new();
5363 let mut committed_materialized_views = Vec::new();
5364
5365 for (idx, (table_id, staged)) in staging.iter().enumerate() {
5366 if spilled_tables.contains(table_id) && matches!(staged, Staged::Put(_)) {
5369 continue;
5370 }
5371 let mut ops = Vec::new();
5372 match staged {
5373 Staged::Put(_) => {
5374 let mut row = prebuilt[idx].take().expect("prebuilt put row");
5376 row.committed_epoch = new_epoch;
5377 let payload = bincode::serialize(&vec![row.clone()])
5378 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
5379 wal.append(
5380 txn_id,
5381 *table_id,
5382 Op::Put {
5383 table_id: *table_id,
5384 rows: payload,
5385 },
5386 )?;
5387 ops.push(StagedOp::Put(row));
5388 }
5389 Staged::Delete(rid) => {
5390 if let Some(before) = &delete_images[idx] {
5391 wal.append(
5392 txn_id,
5393 *table_id,
5394 Op::BeforeImage {
5395 table_id: *table_id,
5396 row_id: *rid,
5397 row: bincode::serialize(before).map_err(|error| {
5398 MongrelError::Other(format!(
5399 "before-image serialize: {error}"
5400 ))
5401 })?,
5402 },
5403 )?;
5404 }
5405 wal.append(
5406 txn_id,
5407 *table_id,
5408 Op::Delete {
5409 table_id: *table_id,
5410 row_ids: vec![*rid],
5411 },
5412 )?;
5413 ops.push(StagedOp::Delete(*rid));
5414 }
5415 Staged::Truncate => {
5416 wal.append(
5417 txn_id,
5418 *table_id,
5419 Op::TruncateTable {
5420 table_id: *table_id,
5421 },
5422 )?;
5423 ops.push(StagedOp::Truncate);
5424 }
5425 Staged::Update(_, _) => unreachable!("updates normalized before sequencer"),
5426 }
5427 applies.push((*table_id, ops));
5428 }
5429
5430 for (name, state, _) in &prepared_external {
5431 wal.append(
5432 txn_id,
5433 EXTERNAL_TABLE_ID,
5434 Op::ExternalTableState {
5435 name: name.clone(),
5436 state: state.clone(),
5437 },
5438 )?;
5439 }
5440
5441 for (table_id, definition) in &prepared_materialized_views {
5442 let mut definition = definition.clone();
5443 definition.last_refresh_epoch = new_epoch.0;
5444 wal.append(
5445 txn_id,
5446 *table_id,
5447 Op::Ddl(crate::wal::DdlOp::SetMaterializedView {
5448 name: definition.name.clone(),
5449 definition_json: crate::wal::DdlOp::encode_materialized_view(&definition)?,
5450 }),
5451 )?;
5452 committed_materialized_views.push(definition);
5453 }
5454
5455 let commit_seq = wal.append_commit(txn_id, new_epoch, &added_runs)?;
5456
5457 self.conflicts.record(&write_keys, new_epoch);
5462 (
5463 new_epoch,
5464 _epoch_guard,
5465 applies,
5466 committed_materialized_views,
5467 commit_seq,
5468 )
5469 };
5470
5471 self.group
5473 .await_durable(&self.shared_wal, commit_seq)
5474 .inspect_err(|_| {
5475 self.poisoned.store(true, Ordering::Relaxed);
5476 })?;
5477
5478 {
5480 let tables = self.tables.read();
5481 for (table_id, ops) in applies {
5485 if let Some(handle) = tables.get(&table_id) {
5486 let mut t = handle.lock();
5487 for op in ops {
5488 match op {
5489 StagedOp::Put(row) => t.apply_put_rows(vec![row])?,
5490 StagedOp::Delete(rid) => t.apply_delete(rid, new_epoch),
5491 StagedOp::Truncate => t.apply_truncate(new_epoch)?,
5492 }
5493 }
5494 t.invalidate_pending_cache();
5495 t.persist_manifest(new_epoch)?;
5496 }
5497 }
5498 for s in &spilled {
5499 if let Some(handle) = tables.get(&s.table_id) {
5500 let mut t = handle.lock();
5501 let dest = t.run_path(s.run_id as u64);
5502 std::fs::rename(&s.pending_path, &dest)?;
5503 if let Some(parent) = s.pending_path.parent() {
5504 let _ = std::fs::remove_dir_all(parent);
5505 }
5506 t.link_run(crate::manifest::RunRef {
5507 run_id: s.run_id,
5508 level: 0,
5509 epoch_created: new_epoch.0,
5510 row_count: s.row_count,
5511 });
5512 t.apply_run_metadata(&s.rows)?;
5513 if truncated_tables.contains(&s.table_id) {
5514 t.set_flushed_epoch(new_epoch);
5519 }
5520 t.invalidate_pending_cache();
5521 t.persist_manifest(new_epoch)?;
5522 }
5523 }
5524 }
5525 for (name, _, pending) in &prepared_external {
5526 publish_external_state_file(&self.root, name, pending)?;
5527 }
5528 if !committed_materialized_views.is_empty() {
5529 {
5530 let mut catalog = self.catalog.write();
5531 for definition in committed_materialized_views {
5532 if let Some(existing) = catalog
5533 .materialized_views
5534 .iter_mut()
5535 .find(|existing| existing.name == definition.name)
5536 {
5537 *existing = definition;
5538 } else {
5539 catalog.materialized_views.push(definition);
5540 }
5541 }
5542 catalog.db_epoch = catalog.db_epoch.max(new_epoch.0);
5543 }
5544 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
5545 }
5546
5547 self.epoch.publish_in_order(new_epoch);
5548 if has_changes {
5549 let _ = self.change_wake.send(());
5550 }
5551 _epoch_guard.disarm();
5552 Ok(new_epoch)
5553 }
5554
5555 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
5558 let e = self.epoch.visible();
5559 let g = self.snapshots.register(e);
5560 (Snapshot::at(e), g)
5561 }
5562
5563 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
5566 let e = self.epoch.visible();
5567 let g = self.snapshots.register_owned(e);
5568 (Snapshot::at(e), g)
5569 }
5570
5571 pub fn set_history_retention_epochs(&self, epochs: u64) -> Result<()> {
5576 let _guard = self.ddl_lock.lock();
5577 let current = self.epoch.visible();
5578 let (old_epochs, old_start) = self.snapshots.history_config();
5579 let earliest_already_guaranteed = if old_epochs == 0 {
5580 current
5581 } else {
5582 Epoch(old_start.0.max(current.0.saturating_sub(old_epochs)))
5583 };
5584 let start = if epochs == 0 {
5585 current
5586 } else {
5587 earliest_already_guaranteed
5588 };
5589 write_history_retention(&self.root, epochs, start)?;
5590 self.snapshots.configure_history(epochs, start);
5591 Ok(())
5592 }
5593
5594 pub fn history_retention_epochs(&self) -> u64 {
5595 self.snapshots.history_config().0
5596 }
5597
5598 pub fn earliest_retained_epoch(&self) -> Epoch {
5599 let current = self.epoch.visible();
5600 self.snapshots.history_floor(current).unwrap_or(current)
5601 }
5602
5603 pub fn snapshot_at_owned(&self, epoch: Epoch) -> Result<(Snapshot, OwnedSnapshotGuard)> {
5606 let current = self.epoch.visible();
5607 if epoch > current {
5608 return Err(MongrelError::InvalidArgument(format!(
5609 "epoch {} is in the future; current epoch is {}",
5610 epoch.0, current.0
5611 )));
5612 }
5613 let earliest = self.earliest_retained_epoch();
5614 if epoch < earliest {
5615 return Err(MongrelError::InvalidArgument(format!(
5616 "epoch {} is no longer retained; earliest available epoch is {}",
5617 epoch.0, earliest.0
5618 )));
5619 }
5620 let guard = self.snapshots.register_owned(epoch);
5621 Ok((Snapshot::at(epoch), guard))
5622 }
5623
5624 pub fn table_names(&self) -> Vec<String> {
5626 self.catalog
5627 .read()
5628 .tables
5629 .iter()
5630 .filter(|t| matches!(t.state, TableState::Live))
5631 .map(|t| t.name.clone())
5632 .collect()
5633 }
5634
5635 pub fn close(&self) -> Result<()> {
5641 for name in self.table_names() {
5642 if let Ok(handle) = self.table(&name) {
5643 if let Err(e) = handle.lock().close() {
5644 eprintln!("[close] flush failed for {name}: {e}");
5645 }
5646 }
5647 }
5648 Ok(())
5649 }
5650
5651 pub fn compact(&self) -> Result<(usize, usize)> {
5658 self.require(&crate::auth::Permission::Ddl)?;
5659 let mut compacted = 0;
5660 let mut skipped = 0;
5661 for name in self.table_names() {
5662 let Ok(handle) = self.table(&name) else {
5663 continue;
5664 };
5665 {
5666 let mut t = handle.lock();
5667 let before = t.run_count();
5668 if before < 2 && !t.should_compact() {
5669 skipped += 1;
5670 continue;
5671 }
5672 match t.compact() {
5673 Ok(()) => {
5674 let after = t.run_count();
5675 compacted += 1;
5676 eprintln!("[compact] {name}: {before} -> {after} runs");
5677 }
5678 Err(e) => {
5679 eprintln!("[compact] {name}: compaction failed: {e}");
5680 skipped += 1;
5681 }
5682 }
5683 }
5684 }
5685 Ok((compacted, skipped))
5686 }
5687
5688 pub fn compact_table(&self, name: &str) -> Result<bool> {
5691 self.require(&crate::auth::Permission::Ddl)?;
5692 let handle = self.table(name)?;
5693 let mut t = handle.lock();
5694 let before = t.run_count();
5695 if before < 2 {
5696 return Ok(false);
5697 }
5698 t.compact()?;
5699 Ok(t.run_count() < before)
5700 }
5701
5702 pub fn table(&self, name: &str) -> Result<TableHandle> {
5704 let cat = self.catalog.read();
5705 let entry = cat
5706 .live(name)
5707 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
5708 let id = entry.table_id;
5709 drop(cat);
5710 self.tables
5711 .read()
5712 .get(&id)
5713 .cloned()
5714 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
5715 }
5716
5717 pub fn has_ttl_tables(&self) -> bool {
5720 self.tables
5721 .read()
5722 .values()
5723 .any(|table| table.lock().ttl().is_some())
5724 }
5725
5726 fn table_by_id(&self, id: u64) -> Result<TableHandle> {
5729 self.tables
5730 .read()
5731 .get(&id)
5732 .cloned()
5733 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
5734 }
5735
5736 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
5742 use crate::wal::DdlOp;
5743 use std::sync::atomic::Ordering;
5744
5745 self.require(&crate::auth::Permission::Ddl)?;
5746 if self.poisoned.load(Ordering::Relaxed) {
5747 return Err(MongrelError::Other(
5748 "database poisoned by fsync error".into(),
5749 ));
5750 }
5751
5752 let _g = self.ddl_lock.lock();
5753 {
5754 let cat = self.catalog.read();
5755 if cat.live(name).is_some() {
5756 return Err(MongrelError::InvalidArgument(format!(
5757 "table {name:?} already exists"
5758 )));
5759 }
5760 }
5761
5762 let commit_lock = Arc::clone(&self.commit_lock);
5765 let _c = commit_lock.lock();
5766 let table_id = {
5767 let mut cat = self.catalog.write();
5768 let id = cat.next_table_id;
5769 cat.next_table_id += 1;
5770 id
5771 };
5772 let epoch = self.epoch.bump_assigned();
5773 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5774 let txn_id = self.alloc_txn_id();
5775
5776 let mut schema = schema;
5780 schema.schema_id = table_id;
5781 schema.validate_auto_increment()?;
5788 for constraint in &schema.constraints.checks {
5789 constraint.expr.validate()?;
5790 }
5791
5792 let schema_json = DdlOp::encode_schema(&schema)?;
5795 let commit_seq = {
5796 let mut wal = self.shared_wal.lock();
5797 wal.append(
5798 txn_id,
5799 table_id,
5800 crate::wal::Op::Ddl(DdlOp::CreateTable {
5801 table_id,
5802 name: name.to_string(),
5803 schema_json,
5804 }),
5805 )?;
5806 wal.append_commit(txn_id, epoch, &[])?
5807 };
5808 self.group
5809 .await_durable(&self.shared_wal, commit_seq)
5810 .inspect_err(|_| {
5811 self.poisoned.store(true, Ordering::Relaxed);
5812 })?;
5813
5814 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
5816 std::fs::create_dir_all(&tdir)?;
5817 let ctx = SharedCtx {
5818 epoch: Arc::clone(&self.epoch),
5819 page_cache: Arc::clone(&self.page_cache),
5820 decoded_cache: Arc::clone(&self.decoded_cache),
5821 snapshots: Arc::clone(&self.snapshots),
5822 kek: self.kek.clone(),
5823 commit_lock: Arc::clone(&self.commit_lock),
5824 shared: Some(crate::engine::SharedWalCtx {
5825 wal: Arc::clone(&self.shared_wal),
5826 group: Arc::clone(&self.group),
5827 poisoned: Arc::clone(&self.poisoned),
5828 txn_ids: Arc::clone(&self.next_txn_id),
5829 change_wake: self.change_wake.clone(),
5830 }),
5831 table_name: Some(name.to_string()),
5832 auth: self.table_auth_checker(),
5833 read_only: self.read_only,
5834 };
5835 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
5836
5837 {
5840 let mut cat = self.catalog.write();
5841 cat.tables.push(CatalogEntry {
5842 table_id,
5843 name: name.to_string(),
5844 schema,
5845 state: TableState::Live,
5846 created_epoch: epoch.0,
5847 });
5848 }
5849 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
5850 self.tables
5851 .write()
5852 .insert(table_id, Arc::new(Mutex::new(table)));
5853
5854 self.epoch.publish_in_order(epoch);
5855 _epoch_guard.disarm();
5856 Ok(table_id)
5857 }
5858
5859 pub fn drop_table(&self, name: &str) -> Result<()> {
5861 use crate::wal::DdlOp;
5862 use std::sync::atomic::Ordering;
5863
5864 self.require(&crate::auth::Permission::Ddl)?;
5865 if self.poisoned.load(Ordering::Relaxed) {
5866 return Err(MongrelError::Other(
5867 "database poisoned by fsync error".into(),
5868 ));
5869 }
5870
5871 let _g = self.ddl_lock.lock();
5872 let table_id = {
5873 let cat = self.catalog.read();
5874 cat.live(name)
5875 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
5876 .table_id
5877 };
5878
5879 let commit_lock = Arc::clone(&self.commit_lock);
5880 let _c = commit_lock.lock();
5881 let epoch = self.epoch.bump_assigned();
5882 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5883 let txn_id = self.alloc_txn_id();
5884 let commit_seq = {
5885 let mut wal = self.shared_wal.lock();
5886 wal.append(
5887 txn_id,
5888 table_id,
5889 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
5890 )?;
5891 wal.append_commit(txn_id, epoch, &[])?
5892 };
5893 self.group
5894 .await_durable(&self.shared_wal, commit_seq)
5895 .inspect_err(|_| {
5896 self.poisoned.store(true, Ordering::Relaxed);
5897 })?;
5898
5899 {
5900 let mut cat = self.catalog.write();
5901 let entry = cat
5902 .tables
5903 .iter_mut()
5904 .find(|t| t.table_id == table_id)
5905 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
5906 entry.state = TableState::Dropped { at_epoch: epoch.0 };
5907 cat.triggers.retain(|trigger| {
5908 !matches!(
5909 &trigger.trigger.target,
5910 TriggerTarget::Table(target) if target == name
5911 )
5912 });
5913 cat.materialized_views
5914 .retain(|definition| definition.name != name);
5915 cat.security.rls_tables.retain(|table| table != name);
5916 cat.security.policies.retain(|policy| policy.table != name);
5917 cat.security.masks.retain(|mask| mask.table != name);
5918 for role in &mut cat.roles {
5919 role.permissions
5920 .retain(|permission| permission_table(permission) != Some(name));
5921 }
5922 }
5923 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
5924 self.tables.write().remove(&table_id);
5925
5926 self.epoch.publish_in_order(epoch);
5927 _epoch_guard.disarm();
5928 Ok(())
5929 }
5930
5931 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
5940 use crate::wal::DdlOp;
5941 use std::sync::atomic::Ordering;
5942
5943 self.require(&crate::auth::Permission::Ddl)?;
5944 if self.poisoned.load(Ordering::Relaxed) {
5945 return Err(MongrelError::Other(
5946 "database poisoned by fsync error".into(),
5947 ));
5948 }
5949
5950 if name == new_name {
5953 return Ok(());
5954 }
5955 if new_name.is_empty() {
5956 return Err(MongrelError::InvalidArgument(
5957 "rename_table: new name must not be empty".into(),
5958 ));
5959 }
5960
5961 let _g = self.ddl_lock.lock();
5962 let table_id = {
5963 let cat = self.catalog.read();
5964 let src = cat
5965 .live(name)
5966 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
5967 if cat.live(new_name).is_some() {
5971 return Err(MongrelError::InvalidArgument(format!(
5972 "rename_table: a table named {new_name:?} already exists"
5973 )));
5974 }
5975 src.table_id
5976 };
5977
5978 let commit_lock = Arc::clone(&self.commit_lock);
5979 let _c = commit_lock.lock();
5980 let epoch = self.epoch.bump_assigned();
5981 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5982 let txn_id = self.alloc_txn_id();
5983 let commit_seq = {
5984 let mut wal = self.shared_wal.lock();
5985 wal.append(
5986 txn_id,
5987 table_id,
5988 crate::wal::Op::Ddl(DdlOp::RenameTable {
5989 table_id,
5990 new_name: new_name.to_string(),
5991 }),
5992 )?;
5993 wal.append_commit(txn_id, epoch, &[])?
5994 };
5995 self.group
5996 .await_durable(&self.shared_wal, commit_seq)
5997 .inspect_err(|_| {
5998 self.poisoned.store(true, Ordering::Relaxed);
5999 })?;
6000
6001 {
6002 let mut cat = self.catalog.write();
6003 let entry = cat
6004 .tables
6005 .iter_mut()
6006 .find(|t| t.table_id == table_id)
6007 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
6008 entry.name = new_name.to_string();
6009 for trigger in &mut cat.triggers {
6010 if matches!(
6011 &trigger.trigger.target,
6012 TriggerTarget::Table(target) if target == name
6013 ) {
6014 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
6015 }
6016 }
6017 if let Some(definition) = cat
6018 .materialized_views
6019 .iter_mut()
6020 .find(|definition| definition.name == name)
6021 {
6022 definition.name = new_name.to_string();
6023 }
6024 for table in &mut cat.security.rls_tables {
6025 if table == name {
6026 *table = new_name.to_string();
6027 }
6028 }
6029 for policy in &mut cat.security.policies {
6030 if policy.table == name {
6031 policy.table = new_name.to_string();
6032 }
6033 }
6034 for mask in &mut cat.security.masks {
6035 if mask.table == name {
6036 mask.table = new_name.to_string();
6037 }
6038 }
6039 for role in &mut cat.roles {
6040 for permission in &mut role.permissions {
6041 rename_permission_table(permission, name, new_name);
6042 }
6043 }
6044 }
6045 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
6046 if let Some(table) = self.tables.read().get(&table_id) {
6049 table.lock().set_catalog_name(new_name.to_string());
6050 }
6051 self.epoch.publish_in_order(epoch);
6052 _epoch_guard.disarm();
6053 Ok(())
6054 }
6055
6056 pub fn alter_column(
6057 &self,
6058 table_name: &str,
6059 column_name: &str,
6060 change: AlterColumn,
6061 ) -> Result<ColumnDef> {
6062 use crate::wal::DdlOp;
6063 use std::sync::atomic::Ordering;
6064
6065 self.require(&crate::auth::Permission::Ddl)?;
6066 if self.poisoned.load(Ordering::Relaxed) {
6067 return Err(MongrelError::Other(
6068 "database poisoned by fsync error".into(),
6069 ));
6070 }
6071
6072 let _g = self.ddl_lock.lock();
6073 let table_id = {
6074 let cat = self.catalog.read();
6075 cat.live(table_name)
6076 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
6077 .table_id
6078 };
6079 let handle =
6080 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
6081 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
6082 })?;
6083
6084 let backfill = {
6090 let table = handle.lock();
6091 let old = table
6092 .schema()
6093 .column(column_name)
6094 .cloned()
6095 .ok_or_else(|| MongrelError::Schema(format!("unknown column {column_name}")))?;
6096 let next_flags = change.flags.unwrap_or(old.flags);
6097 if old.flags.contains(crate::schema::ColumnFlags::NULLABLE)
6098 && !next_flags.contains(crate::schema::ColumnFlags::NULLABLE)
6099 && old.default_value.is_some()
6100 {
6101 let snapshot = Snapshot::at(self.epoch.visible());
6102 let mut updates = Vec::new();
6103 for row in table.visible_rows(snapshot)? {
6104 if row
6105 .columns
6106 .get(&old.id)
6107 .is_some_and(|value| !matches!(value, Value::Null))
6108 {
6109 continue;
6110 }
6111 let mut cells: Vec<(u16, Value)> = row.columns.into_iter().collect();
6112 table.apply_defaults(&mut cells)?;
6113 updates.push((table_id, crate::txn::Staged::Update(row.row_id, cells)));
6114 }
6115 updates
6116 } else {
6117 Vec::new()
6118 }
6119 };
6120 if !backfill.is_empty() {
6121 self.commit_transaction_with_external_states(
6122 self.alloc_txn_id(),
6123 self.epoch.visible(),
6124 backfill,
6125 Vec::new(),
6126 Vec::new(),
6127 None,
6128 None,
6129 )?;
6130 }
6131 let mut table = handle.lock();
6132 let column = table.prepare_alter_column(column_name, &change)?;
6133 let renamed_column = (column.name != column_name).then(|| column.name.clone());
6134 if table
6135 .schema()
6136 .columns
6137 .iter()
6138 .find(|c| c.id == column.id)
6139 .is_some_and(|c| c == &column)
6140 {
6141 return Ok(column);
6142 }
6143
6144 let commit_lock = Arc::clone(&self.commit_lock);
6145 let _c = commit_lock.lock();
6146 let epoch = self.epoch.bump_assigned();
6147 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6148 let txn_id = self.alloc_txn_id();
6149 let column_json = DdlOp::encode_column(&column)?;
6150 let commit_seq = {
6151 let mut wal = self.shared_wal.lock();
6152 wal.append(
6153 txn_id,
6154 table_id,
6155 crate::wal::Op::Ddl(DdlOp::AlterTable {
6156 table_id,
6157 column_json,
6158 }),
6159 )?;
6160 wal.append_commit(txn_id, epoch, &[])?
6161 };
6162 self.group
6163 .await_durable(&self.shared_wal, commit_seq)
6164 .inspect_err(|_| {
6165 self.poisoned.store(true, Ordering::Relaxed);
6166 })?;
6167
6168 table.apply_altered_column(column.clone())?;
6169 let schema = table.schema().clone();
6170 drop(table);
6171
6172 {
6173 let mut cat = self.catalog.write();
6174 let entry = cat
6175 .tables
6176 .iter_mut()
6177 .find(|t| t.table_id == table_id)
6178 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
6179 entry.schema = schema;
6180 if let Some(new_column_name) = renamed_column {
6181 for trigger in &mut cat.triggers {
6182 if matches!(
6183 &trigger.trigger.target,
6184 TriggerTarget::Table(target) if target == table_name
6185 ) {
6186 trigger.trigger = trigger.trigger.renamed_update_column(
6187 column_name,
6188 new_column_name.clone(),
6189 epoch.0,
6190 )?;
6191 }
6192 }
6193 for role in &mut cat.roles {
6194 for permission in &mut role.permissions {
6195 rename_permission_column(
6196 permission,
6197 table_name,
6198 column_name,
6199 &new_column_name,
6200 );
6201 }
6202 }
6203 }
6204 }
6205 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
6206
6207 self.epoch.publish_in_order(epoch);
6208 _epoch_guard.disarm();
6209 Ok(column)
6210 }
6211
6212 pub fn set_table_ttl(
6215 &self,
6216 table_name: &str,
6217 column_name: &str,
6218 duration_nanos: u64,
6219 ) -> Result<crate::manifest::TtlPolicy> {
6220 let policy = self.replace_table_ttl(table_name, Some((column_name, duration_nanos)))?;
6221 Ok(policy.expect("set TTL produces a policy"))
6222 }
6223
6224 pub fn clear_table_ttl(&self, table_name: &str) -> Result<()> {
6225 self.replace_table_ttl(table_name, None)?;
6226 Ok(())
6227 }
6228
6229 fn replace_table_ttl(
6230 &self,
6231 table_name: &str,
6232 requested: Option<(&str, u64)>,
6233 ) -> Result<Option<crate::manifest::TtlPolicy>> {
6234 use crate::wal::DdlOp;
6235 use std::sync::atomic::Ordering;
6236
6237 self.require(&crate::auth::Permission::Ddl)?;
6238 if self.poisoned.load(Ordering::Relaxed) {
6239 return Err(MongrelError::Other(
6240 "database poisoned by fsync error".into(),
6241 ));
6242 }
6243
6244 let _g = self.ddl_lock.lock();
6245 let table_id = {
6246 let cat = self.catalog.read();
6247 cat.live(table_name)
6248 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
6249 .table_id
6250 };
6251 let handle =
6252 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
6253 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
6254 })?;
6255 let mut table = handle.lock();
6256 let policy = match requested {
6257 Some((column, duration)) => Some(table.prepare_ttl_policy(column, duration)?),
6258 None => None,
6259 };
6260 if table.ttl() == policy {
6261 return Ok(policy);
6262 }
6263
6264 let commit_lock = Arc::clone(&self.commit_lock);
6265 let _c = commit_lock.lock();
6266 let epoch = self.epoch.bump_assigned();
6267 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6268 let txn_id = self.alloc_txn_id();
6269 let policy_json = DdlOp::encode_ttl(policy)?;
6270 let commit_seq = {
6271 let mut wal = self.shared_wal.lock();
6272 wal.append(
6273 txn_id,
6274 table_id,
6275 crate::wal::Op::Ddl(DdlOp::SetTtl {
6276 table_id,
6277 policy_json,
6278 }),
6279 )?;
6280 wal.append_commit(txn_id, epoch, &[])?
6281 };
6282 self.group
6283 .await_durable(&self.shared_wal, commit_seq)
6284 .inspect_err(|_| {
6285 self.poisoned.store(true, Ordering::Relaxed);
6286 })?;
6287
6288 table.apply_ttl_policy_at(policy, epoch)?;
6289 self.epoch.publish_in_order(epoch);
6290 _epoch_guard.disarm();
6291 Ok(policy)
6292 }
6293
6294 pub fn gc(&self) -> Result<usize> {
6300 self.require(&crate::auth::Permission::Ddl)?;
6301 let min_active = self.snapshots.min_active(self.epoch.visible()).0;
6302 let mut reclaimed = 0;
6303
6304 let cat = self.catalog.read();
6306 for entry in &cat.tables {
6307 if let TableState::Dropped { at_epoch } = entry.state {
6308 if at_epoch <= min_active {
6309 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
6310 if tdir.exists() {
6311 std::fs::remove_dir_all(&tdir)?;
6312 reclaimed += 1;
6313 }
6314 }
6315 }
6316 }
6317 drop(cat);
6318
6319 let cat = self.catalog.read();
6324 for entry in &cat.tables {
6325 if !matches!(entry.state, TableState::Live) {
6326 continue;
6327 }
6328 let txn_dir = self
6329 .root
6330 .join(TABLES_DIR)
6331 .join(entry.table_id.to_string())
6332 .join("_txn");
6333 if !txn_dir.exists() {
6334 continue;
6335 }
6336 for sub in std::fs::read_dir(&txn_dir)? {
6337 let sub = sub?;
6338 let name = sub.file_name();
6339 let Some(name) = name.to_str() else { continue };
6340 let is_active = name
6342 .parse::<u64>()
6343 .map(|id| self.active_spills.is_active(id))
6344 .unwrap_or(false);
6345 if is_active {
6346 continue;
6347 }
6348 std::fs::remove_dir_all(sub.path())?;
6349 reclaimed += 1;
6350 }
6351 }
6352 drop(cat);
6353
6354 let external_names = {
6355 let cat = self.catalog.read();
6356 cat.external_tables
6357 .iter()
6358 .map(|entry| entry.name.clone())
6359 .collect::<std::collections::HashSet<_>>()
6360 };
6361 let vtab_dir = self.root.join(VTAB_DIR);
6362 if vtab_dir.exists() {
6363 for entry in std::fs::read_dir(&vtab_dir)? {
6364 let entry = entry?;
6365 let name = entry.file_name();
6366 let Some(name) = name.to_str() else { continue };
6367 if external_names.contains(name) {
6368 continue;
6369 }
6370 let path = entry.path();
6371 if path.is_dir() {
6372 std::fs::remove_dir_all(path)?;
6373 } else {
6374 std::fs::remove_file(path)?;
6375 }
6376 reclaimed += 1;
6377 }
6378 }
6379
6380 let tables = self.tables.read();
6384 for (table_id, handle) in tables.iter() {
6385 let backup_pinned: HashSet<u128> = self
6386 .backup_pins
6387 .lock()
6388 .keys()
6389 .filter_map(|(pinned_table, run_id)| {
6390 (*pinned_table == *table_id).then_some(*run_id)
6391 })
6392 .collect();
6393 reclaimed += handle
6394 .lock()
6395 .reap_retiring(Epoch(min_active), &backup_pinned)?;
6396 }
6397
6398 let all_durable = self.active_spills.is_idle()
6406 && tables.values().all(|h| {
6407 let g = h.lock();
6408 g.memtable_len() == 0 && g.mutable_run_len() == 0
6409 });
6410 drop(tables);
6411 if all_durable {
6412 let retain = self
6413 .replication_wal_retention_segments
6414 .load(std::sync::atomic::Ordering::Relaxed);
6415 reclaimed += self
6416 .shared_wal
6417 .lock()
6418 .gc_segments_retain_recent(u64::MAX, retain)?;
6419 }
6420
6421 Ok(reclaimed)
6422 }
6423
6424 pub fn checkpoint(&self) -> Result<()> {
6442 self.close()?;
6444
6445 let _ = self.compact()?;
6447
6448 self.gc()?;
6451
6452 {
6459 let wal = self.shared_wal.lock();
6460 let active = wal.active_segment_no();
6461 drop(wal);
6462 let wal_dir = self.root.join("_wal");
6464 if wal_dir.exists() {
6465 for entry in std::fs::read_dir(&wal_dir)? {
6466 let entry = entry?;
6467 let path = entry.path();
6468 if path.extension().is_some_and(|ext| ext == "wal") {
6469 let _ = std::fs::remove_file(&path);
6470 }
6471 }
6472 }
6473 let _ = active; }
6475
6476 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
6478
6479 let tables = self.tables.read();
6482 let visible = self.epoch.visible();
6483 for handle in tables.values() {
6484 handle.lock().persist_manifest(visible)?;
6485 }
6486
6487 Ok(())
6488 }
6489 fn alloc_txn_id(&self) -> u64 {
6490 let mut g = self.next_txn_id.lock();
6491 let id = *g;
6492 *g = g.wrapping_add(1);
6493 id
6494 }
6495
6496 pub fn set_spill_threshold(&self, bytes: u64) {
6500 self.spill_threshold
6501 .store(bytes, std::sync::atomic::Ordering::Relaxed);
6502 }
6503
6504 #[doc(hidden)]
6508 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
6509 *self.spill_hook.lock() = Some(Box::new(f));
6510 }
6511
6512 #[doc(hidden)]
6515 pub fn __set_backup_hook(&self, f: impl Fn() + Send + Sync + 'static) {
6516 *self.backup_hook.lock() = Some(Box::new(f));
6517 }
6518
6519 #[doc(hidden)]
6523 pub fn __wal_group_sync_count(&self) -> u64 {
6524 self.shared_wal.lock().group_sync_count()
6525 }
6526
6527 #[doc(hidden)]
6530 pub fn __poison(&self) {
6531 self.poisoned
6532 .store(true, std::sync::atomic::Ordering::Relaxed);
6533 }
6534
6535 pub fn check(&self) -> Vec<CheckIssue> {
6548 let mut issues = Vec::new();
6549 let cat = self.catalog.read();
6550 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
6551 for entry in &cat.tables {
6552 if !matches!(entry.state, TableState::Live) {
6553 continue;
6554 }
6555 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
6556 let mut err = |sev: &str, desc: String| {
6557 issues.push(CheckIssue {
6558 table_id: entry.table_id,
6559 table_name: entry.name.clone(),
6560 severity: sev.into(),
6561 description: desc,
6562 });
6563 };
6564 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
6565 Ok(m) => m,
6566 Err(e) => {
6567 err("error", format!("manifest read failed: {e}"));
6568 continue;
6569 }
6570 };
6571 if m.flushed_epoch > m.current_epoch {
6572 err(
6573 "error",
6574 format!(
6575 "flushed_epoch {} exceeds current_epoch {} (impossible)",
6576 m.flushed_epoch, m.current_epoch
6577 ),
6578 );
6579 }
6580
6581 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
6582 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
6583 for rr in &m.runs {
6584 referenced.insert(rr.run_id);
6585 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
6586 if !run_path.exists() {
6587 err("error", format!("missing run file: r-{}.sr", rr.run_id));
6588 continue;
6589 }
6590 match crate::sorted_run::RunReader::open(
6591 &run_path,
6592 entry.schema.clone(),
6593 self.kek.clone(),
6594 ) {
6595 Ok(reader) => {
6596 if reader.row_count() as u64 != rr.row_count {
6597 err(
6598 "error",
6599 format!(
6600 "run r-{} row count mismatch: manifest {} vs run {}",
6601 rr.run_id,
6602 rr.row_count,
6603 reader.row_count()
6604 ),
6605 );
6606 }
6607 }
6608 Err(e) => {
6609 err(
6610 "error",
6611 format!("run r-{} integrity check failed: {e}", rr.run_id),
6612 );
6613 }
6614 }
6615 }
6616
6617 for r in &m.retiring {
6621 referenced.insert(r.run_id);
6622 }
6623
6624 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
6626 for ent in rd.flatten() {
6627 let p = ent.path();
6628 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
6629 continue;
6630 }
6631 let run_id = p
6632 .file_stem()
6633 .and_then(|s| s.to_str())
6634 .and_then(|s| s.strip_prefix("r-"))
6635 .and_then(|s| s.parse::<u128>().ok());
6636 if let Some(id) = run_id {
6637 if !referenced.contains(&id) {
6638 err(
6639 "warning",
6640 format!("orphan run file r-{id}.sr not referenced by the manifest"),
6641 );
6642 }
6643 }
6644 }
6645 }
6646 }
6647
6648 let external_names = cat
6649 .external_tables
6650 .iter()
6651 .map(|entry| entry.name.clone())
6652 .collect::<std::collections::HashSet<_>>();
6653 let vtab_dir = self.root.join(VTAB_DIR);
6654 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
6655 for entry in entries.flatten() {
6656 let name = entry.file_name();
6657 let Some(name) = name.to_str() else { continue };
6658 if !external_names.contains(name) {
6659 issues.push(CheckIssue {
6660 table_id: EXTERNAL_TABLE_ID,
6661 table_name: name.to_string(),
6662 severity: "warning".into(),
6663 description: format!(
6664 "orphan external table state entry {:?} not referenced by the catalog",
6665 entry.path()
6666 ),
6667 });
6668 }
6669 }
6670 }
6671
6672 for (seg, msg) in self.shared_wal.lock().verify_segments() {
6679 issues.push(CheckIssue {
6680 table_id: WAL_TABLE_ID,
6681 table_name: "<wal>".into(),
6682 severity: "error".into(),
6683 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
6684 });
6685 }
6686 issues
6687 }
6688
6689 pub fn doctor(&self) -> Result<Vec<u64>> {
6693 let _ddl = self.ddl_lock.lock();
6696 let issues = self.check();
6697 let bad_tables: std::collections::HashSet<u64> = issues
6702 .iter()
6703 .filter(|i| {
6704 i.severity == "error"
6705 && i.table_id != WAL_TABLE_ID
6706 && i.table_id != EXTERNAL_TABLE_ID
6707 })
6708 .map(|i| i.table_id)
6709 .collect();
6710 if bad_tables.is_empty() {
6711 return Ok(Vec::new());
6712 }
6713
6714 let qdir = self.root.join("_quarantine");
6715 std::fs::create_dir_all(&qdir)?;
6716 let mut quarantined = Vec::new();
6717 for &table_id in &bad_tables {
6718 let tdir = self.root.join(TABLES_DIR).join(table_id.to_string());
6719 if tdir.exists() {
6720 let dest = qdir.join(table_id.to_string());
6721 std::fs::rename(&tdir, &dest)?;
6722 }
6723 {
6724 let mut cat = self.catalog.write();
6725 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
6726 entry.state = TableState::Dropped {
6727 at_epoch: self.epoch.visible().0,
6728 };
6729 }
6730 }
6731 self.tables.write().remove(&table_id);
6733 quarantined.push(table_id);
6734 }
6735 catalog::write_atomic(&self.root, &self.catalog.read(), self.meta_dek.as_ref())?;
6736 Ok(quarantined)
6737 }
6738
6739 #[allow(dead_code)]
6741 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
6742 self.kek.as_ref()
6743 }
6744
6745 #[allow(dead_code)]
6747 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
6748 &self.epoch
6749 }
6750
6751 #[allow(dead_code)]
6753 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
6754 &self.snapshots
6755 }
6756}
6757
6758fn external_state_dir(root: &Path, name: &str) -> PathBuf {
6759 root.join(VTAB_DIR).join(name)
6760}
6761
6762fn filter_ignored_staging(
6763 staging: Vec<(u64, crate::txn::Staged)>,
6764 ignored_indices: &std::collections::BTreeSet<usize>,
6765) -> Vec<(u64, crate::txn::Staged)> {
6766 if ignored_indices.is_empty() {
6767 return staging;
6768 }
6769 staging
6770 .into_iter()
6771 .enumerate()
6772 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
6773 .collect()
6774}
6775
6776fn external_state_file(root: &Path, name: &str) -> PathBuf {
6777 external_state_dir(root, name).join("state.json")
6778}
6779
6780fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
6781 let path = external_state_file(root, name);
6782 match std::fs::read(path) {
6783 Ok(bytes) => Ok(bytes),
6784 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
6785 Err(e) => Err(e.into()),
6786 }
6787}
6788
6789fn current_external_state_bytes(
6790 root: &Path,
6791 external_states: &[(String, Vec<u8>)],
6792 name: &str,
6793) -> Result<Vec<u8>> {
6794 for (table, state) in external_states.iter().rev() {
6795 if table == name {
6796 return Ok(state.clone());
6797 }
6798 }
6799 read_external_state_file(root, name)
6800}
6801
6802fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
6803 let mut out = external_states;
6804 dedup_external_states_in_place(&mut out);
6805 out
6806}
6807
6808fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
6809 let mut seen = std::collections::HashSet::new();
6810 let mut out = Vec::with_capacity(external_states.len());
6811 for (name, state) in std::mem::take(external_states).into_iter().rev() {
6812 if seen.insert(name.clone()) {
6813 out.push((name, state));
6814 }
6815 }
6816 out.reverse();
6817 *external_states = out;
6818}
6819
6820fn prepare_external_state_file(
6821 root: &Path,
6822 name: &str,
6823 state: &[u8],
6824 txn_id: u64,
6825) -> Result<PathBuf> {
6826 let dir = external_state_dir(root, name);
6827 std::fs::create_dir_all(&dir)?;
6828 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
6829 {
6830 let mut file = std::fs::File::create(&pending)?;
6831 file.write_all(state)?;
6832 file.sync_all()?;
6833 }
6834 Ok(pending)
6835}
6836
6837fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
6838 let path = external_state_file(root, name);
6839 std::fs::rename(pending, &path)?;
6840 if let Ok(dir) = std::fs::File::open(external_state_dir(root, name)) {
6841 let _ = dir.sync_all();
6842 }
6843 Ok(())
6844}
6845
6846fn write_external_state_file(root: &Path, name: &str, state: &[u8]) -> Result<()> {
6847 let pending = prepare_external_state_file(root, name, state, 0)?;
6848 publish_external_state_file(root, name, &pending)
6849}
6850
6851fn recover_shared_wal(
6860 root: &Path,
6861 tables: &HashMap<u64, TableHandle>,
6862 epoch: &EpochAuthority,
6863 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
6864) -> Result<()> {
6865 use crate::memtable::Row;
6866 use crate::rowid::RowId;
6867 use crate::wal::{Op, SharedWal};
6868
6869 let records = SharedWal::replay_with_dek(root, wal_dek)?;
6870
6871 let mut committed: HashMap<u64, u64> = HashMap::new();
6873 let mut spilled_to_link: Vec<(
6874 u64, u64, Vec<crate::wal::AddedRun>,
6877 )> = Vec::new();
6878 for r in &records {
6879 if let Op::TxnCommit {
6880 epoch: ce,
6881 ref added_runs,
6882 } = r.op
6883 {
6884 committed.insert(r.txn_id, ce);
6885 if !added_runs.is_empty() {
6886 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
6887 }
6888 }
6889 }
6890 let truncated_transactions: HashSet<(u64, u64)> = records
6891 .iter()
6892 .filter_map(|record| {
6893 committed.get(&record.txn_id)?;
6894 match record.op {
6895 Op::TruncateTable { table_id } => Some((record.txn_id, table_id)),
6896 _ => None,
6897 }
6898 })
6899 .collect();
6900
6901 type TableStage = (Vec<Row>, Vec<(RowId, Epoch)>, Option<Epoch>, Epoch);
6903 let mut stage: HashMap<u64, TableStage> = HashMap::new();
6904 let mut max_epoch = epoch.visible().0;
6905 for r in records {
6906 let Some(&ce) = committed.get(&r.txn_id) else {
6907 continue; };
6909 let commit_epoch = Epoch(ce);
6910 max_epoch = max_epoch.max(ce);
6911 match r.op {
6912 Op::Put { table_id, rows } => {
6913 let skip = tables
6915 .get(&table_id)
6916 .map(|h| h.lock().flushed_epoch() >= ce)
6917 .unwrap_or(true);
6918 if skip {
6919 continue;
6920 }
6921 let rows: Vec<Row> = match bincode::deserialize(&rows) {
6922 Ok(v) => v,
6923 Err(_) => continue,
6924 };
6925 let rows: Vec<Row> = rows
6928 .into_iter()
6929 .map(|mut row| {
6930 row.committed_epoch = commit_epoch;
6931 row
6932 })
6933 .collect();
6934 let entry = stage
6935 .entry(table_id)
6936 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
6937 entry.0.extend(rows);
6938 entry.3 = commit_epoch;
6939 }
6940 Op::Delete { table_id, row_ids } => {
6941 let skip = tables
6942 .get(&table_id)
6943 .map(|h| h.lock().flushed_epoch() >= ce)
6944 .unwrap_or(true);
6945 if skip {
6946 continue;
6947 }
6948 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
6949 let entry = stage
6950 .entry(table_id)
6951 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
6952 entry.1.extend(dels);
6953 entry.3 = commit_epoch;
6954 }
6955 Op::TruncateTable { table_id } => {
6956 let skip = tables
6957 .get(&table_id)
6958 .map(|h| h.lock().flushed_epoch() >= ce)
6959 .unwrap_or(true);
6960 if skip {
6961 continue;
6962 }
6963 stage.insert(
6964 table_id,
6965 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
6966 );
6967 }
6968 Op::ExternalTableState { name, state } => {
6969 write_external_state_file(root, &name, &state)?;
6970 }
6971 Op::Flush { .. }
6972 | Op::TxnCommit { .. }
6973 | Op::TxnAbort
6974 | Op::Ddl(_)
6975 | Op::BeforeImage { .. }
6976 | Op::CommitTimestamp { .. } => {}
6977 }
6978 }
6979 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
6980 let Some(handle) = tables.get(&table_id) else {
6981 continue;
6982 };
6983 let mut t = handle.lock();
6984 if let Some(epoch) = truncate_epoch {
6985 t.apply_truncate(epoch)?;
6986 }
6987 t.recover_apply(rows, deletes)?;
6988 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
6992 t.live_count = rows.len() as u64;
6993 t.persist_manifest(table_epoch)?;
6994 }
6995
6996 for (txn_id, ce, added_runs) in &spilled_to_link {
7000 for ar in added_runs {
7001 let Some(handle) = tables.get(&ar.table_id) else {
7002 continue;
7003 };
7004 let mut t = handle.lock();
7005 let dest = t.run_path(ar.run_id as u64);
7006 if !dest.exists() {
7007 let pending = root
7008 .join(TABLES_DIR)
7009 .join(ar.table_id.to_string())
7010 .join("_txn")
7011 .join(txn_id.to_string())
7012 .join(format!("r-{}.sr", ar.run_id));
7013 if pending.exists() {
7014 if let Some(parent) = pending.parent() {
7015 std::fs::rename(&pending, &dest)?;
7016 let _ = std::fs::remove_dir_all(parent);
7017 }
7018 }
7019 }
7020 if t.run_path(ar.run_id as u64).exists() {
7026 let linked = t.recover_spilled_run(crate::manifest::RunRef {
7027 run_id: ar.run_id,
7028 level: ar.level,
7029 epoch_created: *ce,
7030 row_count: ar.row_count,
7031 });
7032 let replaced = truncated_transactions.contains(&(*txn_id, ar.table_id));
7033 if replaced {
7034 t.set_flushed_epoch(Epoch(*ce));
7035 }
7036 if linked || replaced {
7037 t.persist_manifest(Epoch(*ce))?;
7038 }
7039 }
7040 }
7041 }
7042
7043 epoch.advance_recovered(Epoch(max_epoch));
7044 Ok(())
7045}
7046
7047fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
7048 match condition {
7049 ProcedureCondition::Pk { .. } => {
7050 if schema.primary_key().is_none() {
7051 return Err(MongrelError::InvalidArgument(
7052 "procedure condition Pk references a table without a primary key".into(),
7053 ));
7054 }
7055 }
7056 ProcedureCondition::BitmapEq { column_id, .. }
7057 | ProcedureCondition::BitmapIn { column_id, .. }
7058 | ProcedureCondition::Range { column_id, .. }
7059 | ProcedureCondition::RangeF64 { column_id, .. }
7060 | ProcedureCondition::IsNull { column_id }
7061 | ProcedureCondition::IsNotNull { column_id }
7062 | ProcedureCondition::FmContains { column_id, .. } => {
7063 validate_column_id(*column_id, schema)?;
7064 }
7065 }
7066 Ok(())
7067}
7068
7069fn bind_procedure_args(
7070 procedure: &StoredProcedure,
7071 mut args: HashMap<String, crate::Value>,
7072) -> Result<HashMap<String, crate::Value>> {
7073 let mut out = HashMap::new();
7074 for param in &procedure.params {
7075 let value = match args.remove(¶m.name) {
7076 Some(value) => value,
7077 None => param.default.clone().ok_or_else(|| {
7078 MongrelError::InvalidArgument(format!(
7079 "missing required procedure parameter {:?}",
7080 param.name
7081 ))
7082 })?,
7083 };
7084 if !param.nullable && matches!(value, crate::Value::Null) {
7085 return Err(MongrelError::InvalidArgument(format!(
7086 "procedure parameter {:?} must not be NULL",
7087 param.name
7088 )));
7089 }
7090 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty.clone()) {
7091 return Err(MongrelError::InvalidArgument(format!(
7092 "procedure parameter {:?} has wrong type",
7093 param.name
7094 )));
7095 }
7096 out.insert(param.name.clone(), value);
7097 }
7098 if let Some(extra) = args.keys().next() {
7099 return Err(MongrelError::InvalidArgument(format!(
7100 "unknown procedure parameter {extra:?}"
7101 )));
7102 }
7103 Ok(out)
7104}
7105
7106fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
7107 matches!(
7108 (value, ty),
7109 (crate::Value::Bool(_), crate::TypeId::Bool)
7110 | (crate::Value::Int64(_), crate::TypeId::Int8)
7111 | (crate::Value::Int64(_), crate::TypeId::Int16)
7112 | (crate::Value::Int64(_), crate::TypeId::Int32)
7113 | (crate::Value::Int64(_), crate::TypeId::Int64)
7114 | (crate::Value::Int64(_), crate::TypeId::UInt8)
7115 | (crate::Value::Int64(_), crate::TypeId::UInt16)
7116 | (crate::Value::Int64(_), crate::TypeId::UInt32)
7117 | (crate::Value::Int64(_), crate::TypeId::UInt64)
7118 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
7119 | (crate::Value::Int64(_), crate::TypeId::Date32)
7120 | (crate::Value::Float64(_), crate::TypeId::Float32)
7121 | (crate::Value::Float64(_), crate::TypeId::Float64)
7122 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
7123 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
7124 )
7125}
7126
7127fn eval_cells(
7128 cells: &[crate::procedure::ProcedureCell],
7129 args: &HashMap<String, crate::Value>,
7130 outputs: &HashMap<String, ProcedureCallOutput>,
7131) -> Result<Vec<(u16, crate::Value)>> {
7132 cells
7133 .iter()
7134 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
7135 .collect()
7136}
7137
7138fn eval_condition(
7139 condition: &ProcedureCondition,
7140 args: &HashMap<String, crate::Value>,
7141 outputs: &HashMap<String, ProcedureCallOutput>,
7142) -> Result<crate::Condition> {
7143 Ok(match condition {
7144 ProcedureCondition::Pk { value } => {
7145 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
7146 }
7147 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
7148 column_id: *column_id,
7149 value: eval_value(value, args, outputs)?.encode_key(),
7150 },
7151 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
7152 column_id: *column_id,
7153 values: values
7154 .iter()
7155 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
7156 .collect::<Result<Vec<_>>>()?,
7157 },
7158 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
7159 column_id: *column_id,
7160 lo: expect_i64(eval_value(lo, args, outputs)?)?,
7161 hi: expect_i64(eval_value(hi, args, outputs)?)?,
7162 },
7163 ProcedureCondition::RangeF64 {
7164 column_id,
7165 lo,
7166 lo_inclusive,
7167 hi,
7168 hi_inclusive,
7169 } => crate::Condition::RangeF64 {
7170 column_id: *column_id,
7171 lo: expect_f64(eval_value(lo, args, outputs)?)?,
7172 lo_inclusive: *lo_inclusive,
7173 hi: expect_f64(eval_value(hi, args, outputs)?)?,
7174 hi_inclusive: *hi_inclusive,
7175 },
7176 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
7177 column_id: *column_id,
7178 },
7179 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
7180 column_id: *column_id,
7181 },
7182 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
7183 column_id: *column_id,
7184 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
7185 },
7186 })
7187}
7188
7189fn eval_value(
7190 value: &ProcedureValue,
7191 args: &HashMap<String, crate::Value>,
7192 outputs: &HashMap<String, ProcedureCallOutput>,
7193) -> Result<crate::Value> {
7194 match value {
7195 ProcedureValue::Literal(value) => Ok(value.clone()),
7196 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
7197 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
7198 }),
7199 ProcedureValue::StepScalar(id) => match outputs.get(id) {
7200 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
7201 _ => Err(MongrelError::InvalidArgument(format!(
7202 "procedure step {id:?} did not return a scalar"
7203 ))),
7204 },
7205 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
7206 Err(MongrelError::InvalidArgument(
7207 "row-valued procedure reference cannot be used as a scalar".into(),
7208 ))
7209 }
7210 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
7211 "structured procedure value cannot be used as a scalar cell".into(),
7212 )),
7213 }
7214}
7215
7216fn eval_return_output(
7217 value: &ProcedureValue,
7218 args: &HashMap<String, crate::Value>,
7219 outputs: &HashMap<String, ProcedureCallOutput>,
7220) -> Result<ProcedureCallOutput> {
7221 match value {
7222 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
7223 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
7224 args.get(name).cloned().ok_or_else(|| {
7225 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
7226 })?,
7227 )),
7228 ProcedureValue::StepRows(id)
7229 | ProcedureValue::StepRow(id)
7230 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
7231 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
7232 }),
7233 ProcedureValue::Object(fields) => {
7234 let mut out = Vec::with_capacity(fields.len());
7235 for (name, value) in fields {
7236 out.push((name.clone(), eval_return_output(value, args, outputs)?));
7237 }
7238 Ok(ProcedureCallOutput::Object(out))
7239 }
7240 ProcedureValue::Array(values) => {
7241 let mut out = Vec::with_capacity(values.len());
7242 for value in values {
7243 out.push(eval_return_output(value, args, outputs)?);
7244 }
7245 Ok(ProcedureCallOutput::Array(out))
7246 }
7247 }
7248}
7249
7250fn expect_i64(value: crate::Value) -> Result<i64> {
7251 match value {
7252 crate::Value::Int64(value) => Ok(value),
7253 _ => Err(MongrelError::InvalidArgument(
7254 "procedure value must be Int64".into(),
7255 )),
7256 }
7257}
7258
7259fn expect_f64(value: crate::Value) -> Result<f64> {
7260 match value {
7261 crate::Value::Float64(value) => Ok(value),
7262 _ => Err(MongrelError::InvalidArgument(
7263 "procedure value must be Float64".into(),
7264 )),
7265 }
7266}
7267
7268fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
7269 match value {
7270 crate::Value::Bytes(value) => Ok(value),
7271 _ => Err(MongrelError::InvalidArgument(
7272 "procedure value must be Bytes".into(),
7273 )),
7274 }
7275}
7276
7277fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
7278 if schema.columns.iter().any(|c| c.id == column_id) {
7279 Ok(())
7280 } else {
7281 Err(MongrelError::InvalidArgument(format!(
7282 "unknown column id {column_id}"
7283 )))
7284 }
7285}
7286
7287fn trigger_matches_event(
7288 trigger: &StoredTrigger,
7289 event: &WriteEvent,
7290 cat: &Catalog,
7291) -> Result<bool> {
7292 if trigger.event != event.kind {
7293 return Ok(false);
7294 }
7295 let TriggerTarget::Table(target) = &trigger.target else {
7296 return Ok(false);
7297 };
7298 if target != &event.table {
7299 return Ok(false);
7300 }
7301 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
7302 let schema = &cat
7303 .live(target)
7304 .ok_or_else(|| {
7305 MongrelError::InvalidArgument(format!(
7306 "trigger {:?} references unknown table {target:?}",
7307 trigger.name
7308 ))
7309 })?
7310 .schema;
7311 let mut watched = Vec::with_capacity(trigger.update_of.len());
7312 for name in &trigger.update_of {
7313 let col = schema.column(name).ok_or_else(|| {
7314 MongrelError::InvalidArgument(format!(
7315 "trigger {:?} references unknown UPDATE OF column {name:?}",
7316 trigger.name
7317 ))
7318 })?;
7319 watched.push(col.id);
7320 }
7321 if !event
7322 .changed_columns
7323 .iter()
7324 .any(|column_id| watched.contains(column_id))
7325 {
7326 return Ok(false);
7327 }
7328 }
7329 Ok(true)
7330}
7331
7332fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
7333 let mut ids = std::collections::BTreeSet::new();
7334 if let Some(old) = old {
7335 ids.extend(old.columns.keys().copied());
7336 }
7337 if let Some(new) = new {
7338 ids.extend(new.columns.keys().copied());
7339 }
7340 ids.into_iter()
7341 .filter(|id| {
7342 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
7343 })
7344 .collect()
7345}
7346
7347fn eval_trigger_cells(
7348 cells: &[crate::trigger::TriggerCell],
7349 event: &WriteEvent,
7350 selected: Option<&TriggerRowImage>,
7351) -> Result<Vec<(u16, Value)>> {
7352 cells
7353 .iter()
7354 .map(|cell| {
7355 Ok((
7356 cell.column_id,
7357 eval_trigger_value(&cell.value, event, selected)?,
7358 ))
7359 })
7360 .collect()
7361}
7362
7363fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
7364 match expr {
7365 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
7366 Value::Bool(value) => Ok(value),
7367 Value::Null => Ok(false),
7368 other => Err(MongrelError::InvalidArgument(format!(
7369 "trigger WHEN value must be boolean, got {other:?}"
7370 ))),
7371 },
7372 TriggerExpr::Eq { left, right } => Ok(values_equal(
7373 &eval_trigger_value(left, event, None)?,
7374 &eval_trigger_value(right, event, None)?,
7375 )),
7376 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
7377 &eval_trigger_value(left, event, None)?,
7378 &eval_trigger_value(right, event, None)?,
7379 )),
7380 TriggerExpr::Lt { left, right } => match value_order(
7381 &eval_trigger_value(left, event, None)?,
7382 &eval_trigger_value(right, event, None)?,
7383 ) {
7384 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
7385 None => Ok(false),
7386 },
7387 TriggerExpr::Lte { left, right } => match value_order(
7388 &eval_trigger_value(left, event, None)?,
7389 &eval_trigger_value(right, event, None)?,
7390 ) {
7391 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
7392 None => Ok(false),
7393 },
7394 TriggerExpr::Gt { left, right } => match value_order(
7395 &eval_trigger_value(left, event, None)?,
7396 &eval_trigger_value(right, event, None)?,
7397 ) {
7398 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
7399 None => Ok(false),
7400 },
7401 TriggerExpr::Gte { left, right } => match value_order(
7402 &eval_trigger_value(left, event, None)?,
7403 &eval_trigger_value(right, event, None)?,
7404 ) {
7405 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
7406 None => Ok(false),
7407 },
7408 TriggerExpr::IsNull(value) => Ok(matches!(
7409 eval_trigger_value(value, event, None)?,
7410 Value::Null
7411 )),
7412 TriggerExpr::IsNotNull(value) => Ok(!matches!(
7413 eval_trigger_value(value, event, None)?,
7414 Value::Null
7415 )),
7416 TriggerExpr::And { left, right } => {
7417 if !eval_trigger_expr(left, event)? {
7418 Ok(false)
7419 } else {
7420 Ok(eval_trigger_expr(right, event)?)
7421 }
7422 }
7423 TriggerExpr::Or { left, right } => {
7424 if eval_trigger_expr(left, event)? {
7425 Ok(true)
7426 } else {
7427 Ok(eval_trigger_expr(right, event)?)
7428 }
7429 }
7430 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
7431 }
7432}
7433
7434fn eval_trigger_condition(
7435 condition: &TriggerCondition,
7436 event: &WriteEvent,
7437 selected: &TriggerRowImage,
7438 schema: &Schema,
7439) -> Result<bool> {
7440 match condition {
7441 TriggerCondition::Pk { value } => {
7442 let pk = schema.primary_key().ok_or_else(|| {
7443 MongrelError::InvalidArgument(
7444 "trigger condition Pk references a table without a primary key".into(),
7445 )
7446 })?;
7447 let lhs = eval_trigger_value(value, event, Some(selected))?;
7448 Ok(values_equal(
7449 &lhs,
7450 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
7451 ))
7452 }
7453 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
7454 selected.columns.get(column_id).unwrap_or(&Value::Null),
7455 &eval_trigger_value(value, event, Some(selected))?,
7456 )),
7457 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
7458 selected.columns.get(column_id).unwrap_or(&Value::Null),
7459 &eval_trigger_value(value, event, Some(selected))?,
7460 )),
7461 TriggerCondition::Lt { column_id, value } => match value_order(
7462 selected.columns.get(column_id).unwrap_or(&Value::Null),
7463 &eval_trigger_value(value, event, Some(selected))?,
7464 ) {
7465 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
7466 None => Ok(false),
7467 },
7468 TriggerCondition::Lte { column_id, value } => match value_order(
7469 selected.columns.get(column_id).unwrap_or(&Value::Null),
7470 &eval_trigger_value(value, event, Some(selected))?,
7471 ) {
7472 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
7473 None => Ok(false),
7474 },
7475 TriggerCondition::Gt { column_id, value } => match value_order(
7476 selected.columns.get(column_id).unwrap_or(&Value::Null),
7477 &eval_trigger_value(value, event, Some(selected))?,
7478 ) {
7479 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
7480 None => Ok(false),
7481 },
7482 TriggerCondition::Gte { column_id, value } => match value_order(
7483 selected.columns.get(column_id).unwrap_or(&Value::Null),
7484 &eval_trigger_value(value, event, Some(selected))?,
7485 ) {
7486 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
7487 None => Ok(false),
7488 },
7489 TriggerCondition::IsNull { column_id } => Ok(matches!(
7490 selected.columns.get(column_id),
7491 None | Some(Value::Null)
7492 )),
7493 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
7494 selected.columns.get(column_id),
7495 None | Some(Value::Null)
7496 )),
7497 TriggerCondition::And { left, right } => {
7498 if !eval_trigger_condition(left, event, selected, schema)? {
7499 Ok(false)
7500 } else {
7501 Ok(eval_trigger_condition(right, event, selected, schema)?)
7502 }
7503 }
7504 TriggerCondition::Or { left, right } => {
7505 if eval_trigger_condition(left, event, selected, schema)? {
7506 Ok(true)
7507 } else {
7508 Ok(eval_trigger_condition(right, event, selected, schema)?)
7509 }
7510 }
7511 TriggerCondition::Not(condition) => {
7512 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
7513 }
7514 }
7515}
7516
7517fn eval_trigger_value(
7518 value: &TriggerValue,
7519 event: &WriteEvent,
7520 selected: Option<&TriggerRowImage>,
7521) -> Result<Value> {
7522 match value {
7523 TriggerValue::Literal(value) => Ok(value.clone()),
7524 TriggerValue::NewColumn(column_id) => event
7525 .new
7526 .as_ref()
7527 .and_then(|row| row.columns.get(column_id))
7528 .cloned()
7529 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
7530 TriggerValue::OldColumn(column_id) => event
7531 .old
7532 .as_ref()
7533 .and_then(|row| row.columns.get(column_id))
7534 .cloned()
7535 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
7536 TriggerValue::SelectedColumn(column_id) => selected
7537 .and_then(|row| row.columns.get(column_id))
7538 .cloned()
7539 .ok_or_else(|| {
7540 MongrelError::InvalidArgument("SELECTED column is not available".into())
7541 }),
7542 }
7543}
7544
7545fn values_equal(left: &Value, right: &Value) -> bool {
7546 match (left, right) {
7547 (Value::Null, Value::Null) => true,
7548 (Value::Bool(a), Value::Bool(b)) => a == b,
7549 (Value::Int64(a), Value::Int64(b)) => a == b,
7550 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
7551 (Value::Bytes(a), Value::Bytes(b)) => a == b,
7552 (Value::Embedding(a), Value::Embedding(b)) => {
7553 a.len() == b.len()
7554 && a.iter()
7555 .zip(b.iter())
7556 .all(|(a, b)| a.to_bits() == b.to_bits())
7557 }
7558 _ => false,
7559 }
7560}
7561
7562fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
7563 match (left, right) {
7564 (Value::Null, _) | (_, Value::Null) => None,
7565 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
7566 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
7567 (Value::Int64(a), Value::Float64(b)) => {
7570 let af = *a as f64;
7571 Some(af.total_cmp(b))
7572 }
7573 (Value::Float64(a), Value::Int64(b)) => {
7576 let bf = *b as f64;
7577 Some(a.total_cmp(&bf))
7578 }
7579 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
7580 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
7581 (Value::Embedding(_), Value::Embedding(_)) => None,
7582 _ => None,
7583 }
7584}
7585
7586fn trigger_message(value: Value) -> String {
7587 match value {
7588 Value::Null => "NULL".into(),
7589 Value::Bool(value) => value.to_string(),
7590 Value::Int64(value) => value.to_string(),
7591 Value::Float64(value) => value.to_string(),
7592 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
7593 Value::Embedding(value) => format!("{value:?}"),
7594 Value::Decimal(value) => value.to_string(),
7595 Value::Interval {
7596 months,
7597 days,
7598 nanos,
7599 } => format!("{months}m {days}d {nanos}ns"),
7600 Value::Uuid(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
7601 Value::Json(b) => String::from_utf8_lossy(&b).into_owned(),
7602 }
7603}
7604
7605fn validate_trigger_step<'a>(
7606 step: &TriggerStep,
7607 cat: &'a Catalog,
7608 target_schema: &Schema,
7609 event: TriggerEvent,
7610 select_schemas: &mut HashMap<String, &'a Schema>,
7611) -> Result<()> {
7612 match step {
7613 TriggerStep::SetNew { cells } => {
7614 if event == TriggerEvent::Delete {
7615 return Err(MongrelError::InvalidArgument(
7616 "SetNew trigger step is not valid for DELETE triggers".into(),
7617 ));
7618 }
7619 for cell in cells {
7620 validate_column_id(cell.column_id, target_schema)?;
7621 validate_trigger_value(&cell.value, target_schema, event)?;
7622 }
7623 }
7624 TriggerStep::Insert { table, cells } => {
7625 let schema = trigger_write_schema(cat, table, "insert")?;
7626 for cell in cells {
7627 validate_column_id(cell.column_id, schema)?;
7628 validate_trigger_value(&cell.value, target_schema, event)?;
7629 }
7630 }
7631 TriggerStep::UpdateByPk { table, pk, cells } => {
7632 let schema = trigger_write_schema(cat, table, "update")?;
7633 if schema.primary_key().is_none() {
7634 return Err(MongrelError::InvalidArgument(format!(
7635 "trigger update_by_pk references table {table:?} without a primary key"
7636 )));
7637 }
7638 validate_trigger_value(pk, target_schema, event)?;
7639 for cell in cells {
7640 validate_column_id(cell.column_id, schema)?;
7641 validate_trigger_value(&cell.value, target_schema, event)?;
7642 }
7643 }
7644 TriggerStep::DeleteByPk { table, pk } => {
7645 let schema = trigger_write_schema(cat, table, "delete")?;
7646 if schema.primary_key().is_none() {
7647 return Err(MongrelError::InvalidArgument(format!(
7648 "trigger delete_by_pk references table {table:?} without a primary key"
7649 )));
7650 }
7651 validate_trigger_value(pk, target_schema, event)?;
7652 }
7653 TriggerStep::Select {
7654 id,
7655 table,
7656 conditions,
7657 } => {
7658 let schema = trigger_read_schema(cat, table)?;
7659 for condition in conditions {
7660 validate_trigger_condition(condition, schema, target_schema, event)?;
7661 }
7662 if select_schemas.contains_key(id) {
7663 return Err(MongrelError::InvalidArgument(format!(
7664 "duplicate select id {id:?} in trigger program"
7665 )));
7666 }
7667 select_schemas.insert(id.clone(), schema);
7668 }
7669 TriggerStep::Foreach { id, steps } => {
7670 if !select_schemas.contains_key(id) {
7671 return Err(MongrelError::InvalidArgument(format!(
7672 "foreach references unknown select id {id:?}"
7673 )));
7674 }
7675 let mut inner_select_schemas = select_schemas.clone();
7676 for step in steps {
7677 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
7678 }
7679 }
7680 TriggerStep::DeleteWhere { table, conditions } => {
7681 let schema = trigger_write_schema(cat, table, "delete")?;
7682 for condition in conditions {
7683 validate_trigger_condition(condition, schema, target_schema, event)?;
7684 }
7685 }
7686 TriggerStep::UpdateWhere {
7687 table,
7688 conditions,
7689 cells,
7690 } => {
7691 let schema = trigger_write_schema(cat, table, "update")?;
7692 for condition in conditions {
7693 validate_trigger_condition(condition, schema, target_schema, event)?;
7694 }
7695 for cell in cells {
7696 validate_column_id(cell.column_id, schema)?;
7697 validate_trigger_value(&cell.value, target_schema, event)?;
7698 }
7699 }
7700 TriggerStep::Raise { message, .. } => {
7701 validate_trigger_value(message, target_schema, event)?
7702 }
7703 }
7704 Ok(())
7705}
7706
7707fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
7708 if let Some(entry) = cat.live(table) {
7709 return Ok(&entry.schema);
7710 }
7711 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
7712 let allowed = match op {
7713 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
7714 "update" | "delete" => entry.capabilities.writable,
7715 _ => false,
7716 };
7717 if !allowed {
7718 return Err(MongrelError::InvalidArgument(format!(
7719 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
7720 entry.module
7721 )));
7722 }
7723 if !entry.capabilities.transaction_safe {
7724 return Err(MongrelError::InvalidArgument(format!(
7725 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
7726 entry.module
7727 )));
7728 }
7729 return Ok(&entry.declared_schema);
7730 }
7731 Err(MongrelError::InvalidArgument(format!(
7732 "trigger references unknown table {table:?}"
7733 )))
7734}
7735
7736fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
7737 if let Some(entry) = cat.live(table) {
7738 return Ok(&entry.schema);
7739 }
7740 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
7741 if entry.capabilities.trigger_safe {
7742 return Ok(&entry.declared_schema);
7743 }
7744 return Err(MongrelError::InvalidArgument(format!(
7745 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
7746 entry.module
7747 )));
7748 }
7749 Err(MongrelError::InvalidArgument(format!(
7750 "trigger references unknown table {table:?}"
7751 )))
7752}
7753
7754fn validate_trigger_condition(
7755 condition: &TriggerCondition,
7756 schema: &Schema,
7757 target_schema: &Schema,
7758 event: TriggerEvent,
7759) -> Result<()> {
7760 match condition {
7761 TriggerCondition::Pk { value } => {
7762 if schema.primary_key().is_none() {
7763 return Err(MongrelError::InvalidArgument(
7764 "trigger condition Pk references a table without a primary key".into(),
7765 ));
7766 }
7767 validate_trigger_value(value, target_schema, event)
7768 }
7769 TriggerCondition::Eq { column_id, value }
7770 | TriggerCondition::NotEq { column_id, value }
7771 | TriggerCondition::Lt { column_id, value }
7772 | TriggerCondition::Lte { column_id, value }
7773 | TriggerCondition::Gt { column_id, value }
7774 | TriggerCondition::Gte { column_id, value } => {
7775 validate_column_id(*column_id, schema)?;
7776 validate_trigger_value(value, target_schema, event)
7777 }
7778 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
7779 validate_column_id(*column_id, schema)
7780 }
7781 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
7782 validate_trigger_condition(left, schema, target_schema, event)?;
7783 validate_trigger_condition(right, schema, target_schema, event)
7784 }
7785 TriggerCondition::Not(condition) => {
7786 validate_trigger_condition(condition, schema, target_schema, event)
7787 }
7788 }
7789}
7790
7791fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
7792 match expr {
7793 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
7794 validate_trigger_value(value, schema, event)
7795 }
7796 TriggerExpr::Eq { left, right }
7797 | TriggerExpr::NotEq { left, right }
7798 | TriggerExpr::Lt { left, right }
7799 | TriggerExpr::Lte { left, right }
7800 | TriggerExpr::Gt { left, right }
7801 | TriggerExpr::Gte { left, right } => {
7802 validate_trigger_value(left, schema, event)?;
7803 validate_trigger_value(right, schema, event)
7804 }
7805 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
7806 validate_trigger_expr(left, schema, event)?;
7807 validate_trigger_expr(right, schema, event)
7808 }
7809 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
7810 }
7811}
7812
7813fn validate_trigger_value(
7814 value: &TriggerValue,
7815 schema: &Schema,
7816 event: TriggerEvent,
7817) -> Result<()> {
7818 match value {
7819 TriggerValue::Literal(_) => Ok(()),
7820 TriggerValue::NewColumn(id) => {
7821 if event == TriggerEvent::Delete {
7822 return Err(MongrelError::InvalidArgument(
7823 "DELETE triggers cannot reference NEW".into(),
7824 ));
7825 }
7826 validate_column_id(*id, schema)
7827 }
7828 TriggerValue::OldColumn(id) => {
7829 if event == TriggerEvent::Insert {
7830 return Err(MongrelError::InvalidArgument(
7831 "INSERT triggers cannot reference OLD".into(),
7832 ));
7833 }
7834 validate_column_id(*id, schema)
7835 }
7836 TriggerValue::SelectedColumn(_) => Ok(()),
7840 }
7841}
7842
7843fn recover_ddl_from_wal(
7849 root: &Path,
7850 cat: &mut Catalog,
7851 meta_dek: Option<&[u8; META_DEK_LEN]>,
7852 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
7853) -> Result<()> {
7854 use crate::wal::{DdlOp, Op, SharedWal};
7855
7856 let records = match SharedWal::replay_with_dek(root, wal_dek) {
7857 Ok(r) => r,
7858 Err(_) => return Ok(()),
7859 };
7860
7861 let mut committed: HashMap<u64, u64> = HashMap::new();
7862 for r in &records {
7863 if let Op::TxnCommit { epoch: ce, .. } = r.op {
7864 committed.insert(r.txn_id, ce);
7865 }
7866 }
7867
7868 let mut changed = false;
7869 for r in records {
7870 let Some(&ce) = committed.get(&r.txn_id) else {
7871 continue;
7872 };
7873 match r.op {
7874 Op::Ddl(DdlOp::CreateTable {
7875 table_id,
7876 ref name,
7877 ref schema_json,
7878 }) => {
7879 if cat.tables.iter().any(|t| t.table_id == table_id) {
7880 continue;
7881 }
7882 let schema = DdlOp::decode_schema(schema_json)?;
7883 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
7884 if !tdir.exists() {
7885 std::fs::create_dir_all(tdir.join(crate::engine::WAL_DIR))?;
7886 std::fs::create_dir_all(tdir.join(crate::engine::RUNS_DIR))?;
7887 crate::engine::write_schema(&tdir, &schema)?;
7888 let mut m = crate::manifest::Manifest::new(table_id, schema.schema_id);
7894 crate::manifest::write_atomic(&tdir, &mut m, meta_dek)?;
7895 }
7896 cat.tables.push(CatalogEntry {
7897 table_id,
7898 name: name.clone(),
7899 schema,
7900 state: TableState::Live,
7901 created_epoch: ce,
7902 });
7903 cat.next_table_id = cat.next_table_id.max(table_id + 1);
7904 changed = true;
7905 }
7906 Op::Ddl(DdlOp::DropTable { table_id }) => {
7907 let mut dropped_name = None;
7908 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
7909 dropped_name = Some(entry.name.clone());
7910 if matches!(entry.state, TableState::Live) {
7911 entry.state = TableState::Dropped { at_epoch: ce };
7912 changed = true;
7913 }
7914 }
7915 if let Some(name) = dropped_name {
7916 let before = cat.materialized_views.len();
7917 cat.materialized_views
7918 .retain(|definition| definition.name != name);
7919 changed |= before != cat.materialized_views.len();
7920 cat.security.rls_tables.retain(|table| table != &name);
7921 cat.security.policies.retain(|policy| policy.table != name);
7922 cat.security.masks.retain(|mask| mask.table != name);
7923 for role in &mut cat.roles {
7924 role.permissions
7925 .retain(|permission| permission_table(permission) != Some(&name));
7926 }
7927 }
7928 }
7929 Op::Ddl(DdlOp::RenameTable {
7930 table_id,
7931 ref new_name,
7932 }) => {
7933 let mut old_name = None;
7934 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
7935 if entry.name != *new_name {
7936 old_name = Some(entry.name.clone());
7937 entry.name = new_name.clone();
7938 changed = true;
7939 }
7940 }
7941 if let Some(old_name) = old_name {
7942 if let Some(definition) = cat
7943 .materialized_views
7944 .iter_mut()
7945 .find(|definition| definition.name == old_name)
7946 {
7947 definition.name = new_name.clone();
7948 }
7949 for table in &mut cat.security.rls_tables {
7950 if *table == old_name {
7951 *table = new_name.clone();
7952 }
7953 }
7954 for policy in &mut cat.security.policies {
7955 if policy.table == old_name {
7956 policy.table = new_name.clone();
7957 }
7958 }
7959 for mask in &mut cat.security.masks {
7960 if mask.table == old_name {
7961 mask.table = new_name.clone();
7962 }
7963 }
7964 for role in &mut cat.roles {
7965 for permission in &mut role.permissions {
7966 rename_permission_table(permission, &old_name, new_name);
7967 }
7968 }
7969 }
7970 }
7974 Op::Ddl(DdlOp::AlterTable {
7975 table_id,
7976 ref column_json,
7977 }) => {
7978 let column = DdlOp::decode_column(column_json)?;
7979 let mut renamed = None;
7980 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
7981 renamed = entry
7982 .schema
7983 .columns
7984 .iter()
7985 .find(|existing| existing.id == column.id && existing.name != column.name)
7986 .map(|existing| {
7987 (
7988 entry.name.clone(),
7989 existing.name.clone(),
7990 column.name.clone(),
7991 )
7992 });
7993 if apply_recovered_column_def(&mut entry.schema, column) {
7994 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
7995 if tdir.exists() {
7996 crate::engine::write_schema(&tdir, &entry.schema)?;
7997 }
7998 changed = true;
7999 }
8000 }
8001 if let Some((table, old_name, new_name)) = renamed {
8002 for role in &mut cat.roles {
8003 for permission in &mut role.permissions {
8004 rename_permission_column(permission, &table, &old_name, &new_name);
8005 }
8006 }
8007 }
8008 }
8009 Op::Ddl(DdlOp::SetTtl {
8010 table_id,
8011 ref policy_json,
8012 }) => {
8013 let policy = DdlOp::decode_ttl(policy_json)?;
8014 if let Some(policy) = policy {
8015 let valid = cat
8016 .tables
8017 .iter()
8018 .find(|entry| entry.table_id == table_id)
8019 .and_then(|entry| {
8020 entry
8021 .schema
8022 .columns
8023 .iter()
8024 .find(|column| column.id == policy.column_id)
8025 })
8026 .is_some_and(|column| {
8027 column.ty == TypeId::TimestampNanos
8028 && policy.duration_nanos > 0
8029 && policy.duration_nanos <= i64::MAX as u64
8030 });
8031 if !valid {
8032 return Err(MongrelError::Schema(format!(
8033 "invalid recovered TTL policy for table id {table_id}"
8034 )));
8035 }
8036 }
8037 let tdir = root.join(TABLES_DIR).join(table_id.to_string());
8038 if tdir.exists() {
8039 let mut manifest = crate::manifest::read(&tdir, meta_dek)?;
8040 if manifest.ttl != policy || manifest.current_epoch < ce {
8041 manifest.ttl = policy;
8042 manifest.current_epoch = manifest.current_epoch.max(ce);
8043 crate::manifest::write_atomic(&tdir, &mut manifest, meta_dek)?;
8044 }
8045 }
8046 }
8047 Op::Ddl(DdlOp::SetMaterializedView {
8048 ref name,
8049 ref definition_json,
8050 }) => {
8051 let definition = DdlOp::decode_materialized_view(definition_json)?;
8052 if definition.name != *name {
8053 return Err(MongrelError::Schema(format!(
8054 "materialized view WAL name mismatch: {name:?}"
8055 )));
8056 }
8057 if cat.live(name).is_some() {
8058 if let Some(existing) = cat
8059 .materialized_views
8060 .iter_mut()
8061 .find(|existing| existing.name == *name)
8062 {
8063 if *existing != definition {
8064 *existing = definition;
8065 changed = true;
8066 }
8067 } else {
8068 cat.materialized_views.push(definition);
8069 changed = true;
8070 }
8071 }
8072 }
8073 Op::Ddl(DdlOp::SetSecurityCatalog { ref security_json }) => {
8074 let security = DdlOp::decode_security(security_json)?;
8075 validate_security_catalog(cat, &security)?;
8076 if cat.security != security {
8077 cat.security = security;
8078 changed = true;
8079 }
8080 }
8081 _ => {}
8082 }
8083 }
8084
8085 if changed {
8086 catalog::write_atomic(root, cat, meta_dek)?;
8087 }
8088 Ok(())
8089}
8090
8091fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> bool {
8092 match schema.columns.iter_mut().find(|c| c.id == column.id) {
8093 Some(existing) if *existing == column => false,
8094 Some(existing) => {
8095 *existing = column;
8096 schema.schema_id = schema.schema_id.saturating_add(1);
8097 true
8098 }
8099 None => {
8100 schema.columns.push(column);
8101 schema.schema_id = schema.schema_id.saturating_add(1);
8102 true
8103 }
8104 }
8105}
8106
8107fn permission_table(permission: &crate::auth::Permission) -> Option<&str> {
8108 use crate::auth::Permission;
8109 match permission {
8110 Permission::Select { table }
8111 | Permission::Insert { table }
8112 | Permission::Update { table }
8113 | Permission::Delete { table }
8114 | Permission::SelectColumns { table, .. }
8115 | Permission::InsertColumns { table, .. }
8116 | Permission::UpdateColumns { table, .. } => Some(table),
8117 Permission::All | Permission::Ddl | Permission::Admin => None,
8118 }
8119}
8120
8121fn rename_permission_table(permission: &mut crate::auth::Permission, old: &str, new: &str) {
8122 use crate::auth::Permission;
8123 let table = match permission {
8124 Permission::Select { table }
8125 | Permission::Insert { table }
8126 | Permission::Update { table }
8127 | Permission::Delete { table }
8128 | Permission::SelectColumns { table, .. }
8129 | Permission::InsertColumns { table, .. }
8130 | Permission::UpdateColumns { table, .. } => Some(table),
8131 Permission::All | Permission::Ddl | Permission::Admin => None,
8132 };
8133 if let Some(table) = table.filter(|table| table.as_str() == old) {
8134 *table = new.to_string();
8135 }
8136}
8137
8138fn rename_permission_column(
8139 permission: &mut crate::auth::Permission,
8140 target_table: &str,
8141 old: &str,
8142 new: &str,
8143) {
8144 use crate::auth::Permission;
8145 let columns = match permission {
8146 Permission::SelectColumns { table, columns }
8147 | Permission::InsertColumns { table, columns }
8148 | Permission::UpdateColumns { table, columns }
8149 if table == target_table =>
8150 {
8151 Some(columns)
8152 }
8153 _ => None,
8154 };
8155 if let Some(column) = columns
8156 .into_iter()
8157 .flatten()
8158 .find(|column| column.as_str() == old)
8159 {
8160 *column = new.to_string();
8161 }
8162}
8163
8164fn merge_permission(
8165 permissions: &mut Vec<crate::auth::Permission>,
8166 permission: crate::auth::Permission,
8167) {
8168 use crate::auth::Permission;
8169 let (kind, table, mut columns) = match permission {
8170 Permission::SelectColumns { table, columns } => (0, table, columns),
8171 Permission::InsertColumns { table, columns } => (1, table, columns),
8172 Permission::UpdateColumns { table, columns } => (2, table, columns),
8173 permission if !permissions.contains(&permission) => {
8174 permissions.push(permission);
8175 return;
8176 }
8177 _ => return,
8178 };
8179 for permission in permissions.iter_mut() {
8180 let existing = match permission {
8181 Permission::SelectColumns {
8182 table: existing_table,
8183 columns,
8184 } if kind == 0 && existing_table == &table => Some(columns),
8185 Permission::InsertColumns {
8186 table: existing_table,
8187 columns,
8188 } if kind == 1 && existing_table == &table => Some(columns),
8189 Permission::UpdateColumns {
8190 table: existing_table,
8191 columns,
8192 } if kind == 2 && existing_table == &table => Some(columns),
8193 _ => None,
8194 };
8195 if let Some(existing) = existing {
8196 existing.append(&mut columns);
8197 existing.sort();
8198 existing.dedup();
8199 return;
8200 }
8201 }
8202 columns.sort();
8203 columns.dedup();
8204 permissions.push(match kind {
8205 0 => Permission::SelectColumns { table, columns },
8206 1 => Permission::InsertColumns { table, columns },
8207 2 => Permission::UpdateColumns { table, columns },
8208 _ => unreachable!(),
8209 });
8210}
8211
8212fn revoke_permission_from(
8213 permissions: &mut Vec<crate::auth::Permission>,
8214 revoked: &crate::auth::Permission,
8215) {
8216 use crate::auth::Permission;
8217 let revoked_columns = match revoked {
8218 Permission::SelectColumns { table, columns } => Some((0, table, columns)),
8219 Permission::InsertColumns { table, columns } => Some((1, table, columns)),
8220 Permission::UpdateColumns { table, columns } => Some((2, table, columns)),
8221 _ => None,
8222 };
8223 let Some((kind, table, columns)) = revoked_columns else {
8224 permissions.retain(|permission| permission != revoked);
8225 return;
8226 };
8227 for permission in permissions.iter_mut() {
8228 let current = match permission {
8229 Permission::SelectColumns {
8230 table: current_table,
8231 columns,
8232 } if kind == 0 && current_table == table => Some(columns),
8233 Permission::InsertColumns {
8234 table: current_table,
8235 columns,
8236 } if kind == 1 && current_table == table => Some(columns),
8237 Permission::UpdateColumns {
8238 table: current_table,
8239 columns,
8240 } if kind == 2 && current_table == table => Some(columns),
8241 _ => None,
8242 };
8243 if let Some(current) = current {
8244 current.retain(|column| !columns.contains(column));
8245 }
8246 }
8247 permissions.retain(|permission| match permission {
8248 Permission::SelectColumns { columns, .. }
8249 | Permission::InsertColumns { columns, .. }
8250 | Permission::UpdateColumns { columns, .. } => !columns.is_empty(),
8251 _ => true,
8252 });
8253}
8254
8255fn validate_security_catalog(
8256 catalog: &Catalog,
8257 security: &crate::security::SecurityCatalog,
8258) -> Result<()> {
8259 let mut policy_names = HashSet::new();
8260 for table in &security.rls_tables {
8261 if catalog.live(table).is_none() {
8262 return Err(MongrelError::NotFound(format!(
8263 "RLS table {table:?} not found"
8264 )));
8265 }
8266 }
8267 for policy in &security.policies {
8268 if !policy_names.insert((policy.table.clone(), policy.name.clone())) {
8269 return Err(MongrelError::InvalidArgument(format!(
8270 "duplicate policy {:?} on {:?}",
8271 policy.name, policy.table
8272 )));
8273 }
8274 let schema = &catalog
8275 .live(&policy.table)
8276 .ok_or_else(|| {
8277 MongrelError::NotFound(format!("policy table {:?} not found", policy.table))
8278 })?
8279 .schema;
8280 if let Some(expression) = &policy.using {
8281 validate_security_expression(expression, schema)?;
8282 }
8283 if let Some(expression) = &policy.with_check {
8284 validate_security_expression(expression, schema)?;
8285 }
8286 }
8287 let mut mask_names = HashSet::new();
8288 for mask in &security.masks {
8289 if !mask_names.insert((mask.table.clone(), mask.name.clone())) {
8290 return Err(MongrelError::InvalidArgument(format!(
8291 "duplicate mask {:?} on {:?}",
8292 mask.name, mask.table
8293 )));
8294 }
8295 let column = catalog
8296 .live(&mask.table)
8297 .and_then(|entry| {
8298 entry
8299 .schema
8300 .columns
8301 .iter()
8302 .find(|column| column.id == mask.column)
8303 })
8304 .ok_or_else(|| {
8305 MongrelError::NotFound(format!(
8306 "mask column {} on {:?} not found",
8307 mask.column, mask.table
8308 ))
8309 })?;
8310 if matches!(
8311 mask.strategy,
8312 crate::security::MaskStrategy::Redact { .. } | crate::security::MaskStrategy::Sha256
8313 ) && !matches!(column.ty, TypeId::Bytes | TypeId::Enum { .. })
8314 {
8315 return Err(MongrelError::InvalidArgument(format!(
8316 "mask {:?} requires a string/bytes column",
8317 mask.name
8318 )));
8319 }
8320 }
8321 Ok(())
8322}
8323
8324fn validate_security_expression(
8325 expression: &crate::security::SecurityExpr,
8326 schema: &Schema,
8327) -> Result<()> {
8328 use crate::security::SecurityExpr;
8329 match expression {
8330 SecurityExpr::True => Ok(()),
8331 SecurityExpr::ColumnEqCurrentUser { column }
8332 | SecurityExpr::ColumnEqValue { column, .. } => {
8333 if schema
8334 .columns
8335 .iter()
8336 .any(|candidate| candidate.id == *column)
8337 {
8338 Ok(())
8339 } else {
8340 Err(MongrelError::InvalidArgument(format!(
8341 "security expression references unknown column id {column}"
8342 )))
8343 }
8344 }
8345 SecurityExpr::And { left, right } | SecurityExpr::Or { left, right } => {
8346 validate_security_expression(left, schema)?;
8347 validate_security_expression(right, schema)
8348 }
8349 SecurityExpr::Not { expression } => validate_security_expression(expression, schema),
8350 }
8351}
8352
8353fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
8358 for entry in &cat.tables {
8359 let txn_dir = root
8360 .join(TABLES_DIR)
8361 .join(entry.table_id.to_string())
8362 .join("_txn");
8363 if txn_dir.exists() {
8364 let _ = std::fs::remove_dir_all(&txn_dir);
8365 }
8366 }
8367}
8368
8369#[cfg(test)]
8370mod trigger_engine_tests {
8371 use super::*;
8372
8373 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
8374 WriteEvent {
8375 table: "test".into(),
8376 kind: TriggerEvent::Insert,
8377 new: Some(TriggerRowImage {
8378 columns: new_cells.iter().cloned().collect(),
8379 }),
8380 old: Some(TriggerRowImage {
8381 columns: old_cells.iter().cloned().collect(),
8382 }),
8383 changed_columns: Vec::new(),
8384 op_indices: Vec::new(),
8385 put_idx: None,
8386 trigger_stack: Vec::new(),
8387 }
8388 }
8389
8390 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
8391 WriteEvent {
8392 table: "test".into(),
8393 kind: TriggerEvent::Insert,
8394 new: Some(TriggerRowImage {
8395 columns: new_cells.iter().cloned().collect(),
8396 }),
8397 old: None,
8398 changed_columns: Vec::new(),
8399 op_indices: Vec::new(),
8400 put_idx: None,
8401 trigger_stack: Vec::new(),
8402 }
8403 }
8404
8405 #[test]
8406 fn value_order_int64_vs_float64() {
8407 assert_eq!(
8408 value_order(&Value::Int64(5), &Value::Float64(5.0)),
8409 Some(std::cmp::Ordering::Equal)
8410 );
8411 assert_eq!(
8412 value_order(&Value::Int64(5), &Value::Float64(3.0)),
8413 Some(std::cmp::Ordering::Greater)
8414 );
8415 assert_eq!(
8416 value_order(&Value::Int64(2), &Value::Float64(3.0)),
8417 Some(std::cmp::Ordering::Less)
8418 );
8419 }
8420
8421 #[test]
8422 fn value_order_null_returns_none() {
8423 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
8424 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
8425 assert_eq!(value_order(&Value::Null, &Value::Null), None);
8426 }
8427
8428 #[test]
8429 fn value_order_cross_group_returns_none() {
8430 assert_eq!(
8431 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
8432 None
8433 );
8434 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
8435 assert_eq!(
8436 value_order(
8437 &Value::Embedding(vec![1.0, 2.0]),
8438 &Value::Embedding(vec![1.0, 2.0])
8439 ),
8440 None
8441 );
8442 }
8443
8444 #[test]
8445 fn eval_trigger_expr_ranges_and_booleans() {
8446 let expr = TriggerExpr::And {
8447 left: Box::new(TriggerExpr::Gt {
8448 left: TriggerValue::NewColumn(1),
8449 right: TriggerValue::Literal(Value::Int64(0)),
8450 }),
8451 right: Box::new(TriggerExpr::Lte {
8452 left: TriggerValue::NewColumn(1),
8453 right: TriggerValue::Literal(Value::Int64(100)),
8454 }),
8455 };
8456 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
8457 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
8458 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
8459
8460 let or_expr = TriggerExpr::Or {
8461 left: Box::new(TriggerExpr::Lt {
8462 left: TriggerValue::NewColumn(1),
8463 right: TriggerValue::Literal(Value::Int64(0)),
8464 }),
8465 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
8466 TriggerValue::OldColumn(2),
8467 )))),
8468 };
8469 assert!(eval_trigger_expr(
8470 &or_expr,
8471 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
8472 )
8473 .unwrap());
8474 assert!(!eval_trigger_expr(
8475 &or_expr,
8476 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
8477 )
8478 .unwrap());
8479
8480 assert!(eval_trigger_expr(
8481 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
8482 &event_insert(&[])
8483 )
8484 .unwrap());
8485 assert!(!eval_trigger_expr(
8486 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
8487 &event_insert(&[])
8488 )
8489 .unwrap());
8490 assert!(!eval_trigger_expr(
8491 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
8492 &event_insert(&[])
8493 )
8494 .unwrap());
8495 }
8496}