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