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