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