1use crate::catalog::{self, Catalog, CatalogEntry, TableState, META_DEK_LEN};
10use crate::engine::{SharedCtx, Table};
11use crate::epoch::{Epoch, EpochAuthority, EpochGuard, MaintenanceReceipt, 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::{Read, 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";
38pub const CTAS_BUILD_TABLE_PREFIX: &str = "__mongreldb_ctas_build_";
39
40pub const WAL_TABLE_ID: u64 = u64::MAX;
44pub const EXTERNAL_TABLE_ID: u64 = u64::MAX - 1;
47
48fn advance_security_version(catalog: &mut Catalog) -> Result<()> {
49 catalog.security_version = catalog.security_version.checked_add(1).ok_or_else(|| {
50 MongrelError::Conflict("security catalog version space is exhausted".into())
51 })?;
52 Ok(())
53}
54
55fn process_database_locks() -> &'static Mutex<HashMap<PathBuf, std::sync::Weak<SharedDatabaseLock>>>
56{
57 static LOCKS: std::sync::OnceLock<
58 Mutex<HashMap<PathBuf, std::sync::Weak<SharedDatabaseLock>>>,
59 > = std::sync::OnceLock::new();
60 LOCKS.get_or_init(|| Mutex::new(HashMap::new()))
61}
62
63struct SharedDatabaseLock {
64 bootstrap_file: std::fs::File,
65 legacy_file: Mutex<Option<std::fs::File>>,
66 canonical_path: PathBuf,
67}
68
69struct DatabaseFileLock {
70 shared: Arc<SharedDatabaseLock>,
71 canonical_path: PathBuf,
72 durable_root: Option<Arc<crate::durable_file::DurableRoot>>,
73}
74
75impl Drop for SharedDatabaseLock {
76 fn drop(&mut self) {
77 if let Some(file) = self.legacy_file.get_mut().take() {
78 let _ = fs2::FileExt::unlock(&file);
79 }
80 let _ = fs2::FileExt::unlock(&self.bootstrap_file);
81 process_database_locks().lock().remove(&self.canonical_path);
82 }
83}
84
85fn commit_prepare_checkpoint(
86 control: Option<&crate::ExecutionControl>,
87 index: usize,
88) -> Result<()> {
89 if index.is_multiple_of(256) {
90 if let Some(control) = control {
91 control.checkpoint()?;
92 }
93 }
94 Ok(())
95}
96
97fn finish_controlled_commit_attempt(
98 result: Result<Epoch>,
99 after_commit: &mut Option<&mut dyn FnMut(Option<Epoch>) -> Result<()>>,
100) -> Result<Epoch> {
101 let Some(after_commit) = after_commit.as_mut() else {
102 return result;
103 };
104 match result {
105 Ok(epoch) => match (**after_commit)(Some(epoch)) {
106 Ok(()) => Ok(epoch),
107 Err(error) => Err(MongrelError::DurableCommit {
108 epoch: epoch.0,
109 message: error.to_string(),
110 }),
111 },
112 Err(MongrelError::DurableCommit { epoch, message }) => {
113 let callback_error = (**after_commit)(Some(Epoch(epoch))).err();
114 Err(MongrelError::DurableCommit {
115 epoch,
116 message: callback_error
117 .map(|error| format!("{message}; commit callback: {error}"))
118 .unwrap_or(message),
119 })
120 }
121 Err(error) => match (**after_commit)(None) {
122 Ok(()) => Err(error),
123 Err(callback_error) => Err(MongrelError::Other(format!(
124 "{error}; commit callback: {callback_error}"
125 ))),
126 },
127 }
128}
129
130fn current_unix_nanos() -> u64 {
131 std::time::SystemTime::now()
132 .duration_since(std::time::UNIX_EPOCH)
133 .unwrap_or_default()
134 .as_nanos() as u64
135}
136
137#[cfg(feature = "encryption")]
138fn read_encryption_salt(
139 root: &crate::durable_file::DurableRoot,
140) -> Result<[u8; crate::encryption::SALT_LEN]> {
141 let mut file = root
142 .open_regular(Path::new(META_DIR).join(KEYS_FILENAME))
143 .map_err(|error| MongrelError::NotFound(format!("encryption salt file: {error}")))?;
144 let length = file.metadata()?.len();
145 if length != crate::encryption::SALT_LEN as u64 {
146 return Err(MongrelError::Encryption(format!(
147 "invalid encryption salt length: got {length}, expected {}",
148 crate::encryption::SALT_LEN
149 )));
150 }
151 let mut salt = [0_u8; crate::encryption::SALT_LEN];
152 file.read_exact(&mut salt)?;
153 Ok(salt)
154}
155
156fn incremental_aggregate_cache_key(
157 table: &str,
158 conditions: &[crate::query::Condition],
159 column: Option<u16>,
160 agg: crate::engine::NativeAgg,
161 principal: Option<&crate::auth::Principal>,
162 security_version: u64,
163) -> u64 {
164 use std::hash::{Hash, Hasher};
165 let projection = column.as_ref().map(std::slice::from_ref);
166 let query_key = crate::query::canonical_query_key(conditions, projection, security_version);
167 let mut hasher = std::collections::hash_map::DefaultHasher::new();
168 table.hash(&mut hasher);
169 query_key.hash(&mut hasher);
170 match agg {
171 crate::engine::NativeAgg::Count => 0u8,
172 crate::engine::NativeAgg::Sum => 1,
173 crate::engine::NativeAgg::Min => 2,
174 crate::engine::NativeAgg::Max => 3,
175 crate::engine::NativeAgg::Avg => 4,
176 }
177 .hash(&mut hasher);
178 if let Some(principal) = principal {
179 principal.user_id.hash(&mut hasher);
180 principal.created_epoch.hash(&mut hasher);
181 principal.username.hash(&mut hasher);
182 principal.is_admin.hash(&mut hasher);
183 let mut roles = principal.roles.clone();
184 roles.sort_unstable();
185 roles.hash(&mut hasher);
186 }
187 hasher.finish()
188}
189
190fn read_history_retention(
191 root: &crate::durable_file::DurableRoot,
192 current_epoch: Epoch,
193) -> Result<(u64, Epoch)> {
194 const MAX_HISTORY_RETENTION_BYTES: u64 = 128;
195 let file = match root.open_regular(Path::new(META_DIR).join(HISTORY_RETENTION_FILENAME)) {
196 Ok(file) => file,
197 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
198 return Ok((0, current_epoch));
199 }
200 Err(error) => return Err(error.into()),
201 };
202 let length = file.metadata()?.len();
203 if length > MAX_HISTORY_RETENTION_BYTES {
204 return Err(MongrelError::ResourceLimitExceeded {
205 resource: "history retention bytes",
206 requested: usize::try_from(length).unwrap_or(usize::MAX),
207 limit: MAX_HISTORY_RETENTION_BYTES as usize,
208 });
209 }
210 let mut bytes = Vec::with_capacity(length as usize);
211 file.take(MAX_HISTORY_RETENTION_BYTES + 1)
212 .read_to_end(&mut bytes)?;
213 if bytes.len() as u64 != length {
214 return Err(MongrelError::Other(
215 "history retention length changed while reading".into(),
216 ));
217 }
218 let text = std::str::from_utf8(&bytes)
219 .map_err(|error| MongrelError::Other(format!("history retention encoding: {error}")))?;
220 let mut fields = text.split_whitespace();
221 let epochs = fields
222 .next()
223 .ok_or_else(|| MongrelError::Other("history retention file is empty".into()))?
224 .parse::<u64>()
225 .map_err(|error| MongrelError::Other(format!("history retention epochs: {error}")))?;
226 let start = fields
227 .next()
228 .ok_or_else(|| MongrelError::Other("history retention start is missing".into()))?
229 .parse::<u64>()
230 .map_err(|error| MongrelError::Other(format!("history retention start: {error}")))?;
231 if fields.next().is_some() || start > current_epoch.0 {
232 return Err(MongrelError::Other(
233 "history retention file has trailing fields or a future start epoch".into(),
234 ));
235 }
236 Ok((epochs, Epoch(start)))
237}
238
239fn write_history_retention<F>(
240 root: &Path,
241 epochs: u64,
242 start: Epoch,
243 after_publish: F,
244) -> Result<()>
245where
246 F: FnOnce(),
247{
248 let meta = root.join(META_DIR);
249 let path = meta.join(HISTORY_RETENTION_FILENAME);
250 let bytes = format!("{epochs} {}\n", start.0);
251 crate::durable_file::write_atomic_with_after(&path, bytes.as_bytes(), after_publish)?;
252 Ok(())
253}
254
255struct PreparedBackupDestination {
256 parent: crate::durable_file::DurableRoot,
257 destination_name: std::ffi::OsString,
258 destination_path: PathBuf,
259 stage_name: std::ffi::OsString,
260 stage: Option<Box<crate::durable_file::DurableRoot>>,
261}
262
263fn prepare_backup_destination(
264 source: &Path,
265 destination: &Path,
266) -> Result<PreparedBackupDestination> {
267 let destination_name = destination
268 .file_name()
269 .ok_or_else(|| MongrelError::InvalidArgument("invalid backup destination".into()))?
270 .to_os_string();
271 let requested_parent = destination
272 .parent()
273 .filter(|path| !path.as_os_str().is_empty())
274 .unwrap_or_else(|| Path::new("."));
275 crate::durable_file::create_directory_all(requested_parent)?;
276 let parent = crate::durable_file::DurableRoot::open(requested_parent)?;
277 prepare_backup_destination_in(source, &parent, &destination_name)
278}
279
280fn prepare_backup_destination_in(
281 source: &Path,
282 parent: &crate::durable_file::DurableRoot,
283 destination_name: &std::ffi::OsStr,
284) -> Result<PreparedBackupDestination> {
285 let source = source.canonicalize()?;
286 if parent.canonical_path().starts_with(&source) {
287 return Err(MongrelError::InvalidArgument(
288 "backup destination must not be inside the source database".into(),
289 ));
290 }
291 if parent.entry_exists(Path::new(&destination_name))? {
292 return Err(MongrelError::Conflict(format!(
293 "backup destination already exists: {}",
294 parent.canonical_path().join(destination_name).display()
295 )));
296 }
297 let mut stage_name = None;
298 for _ in 0..128 {
299 let mut nonce = [0_u8; 8];
300 crate::encryption::fill_random(&mut nonce)?;
301 let suffix = nonce
302 .iter()
303 .map(|byte| format!("{byte:02x}"))
304 .collect::<String>();
305 let name = std::ffi::OsString::from(format!(
306 ".{}.backup-stage-{}-{suffix}",
307 destination_name.to_string_lossy(),
308 std::process::id()
309 ));
310 match parent.create_directory_new(Path::new(&name)) {
311 Ok(()) => {
312 stage_name = Some(name);
313 break;
314 }
315 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
316 Err(error) => return Err(error.into()),
317 }
318 }
319 let stage_name = stage_name
320 .ok_or_else(|| MongrelError::Conflict("could not allocate backup staging path".into()))?;
321 let stage = parent.open_directory(Path::new(&stage_name))?;
322 Ok(PreparedBackupDestination {
323 destination_path: parent.canonical_path().join(destination_name),
324 destination_name: destination_name.to_os_string(),
325 stage_name,
326 stage: Some(Box::new(stage)),
327 parent: parent.try_clone()?,
328 })
329}
330
331fn copy_backup_boundary(
332 source_root: &Path,
333 destination_root: &crate::durable_file::DurableRoot,
334 deferred_runs: &HashSet<PathBuf>,
335 copied: &mut Vec<PathBuf>,
336 control: Option<&crate::ExecutionControl>,
337) -> Result<()> {
338 let mut visited = 0;
339 crate::durable_file::walk_regular_files_nofollow(
340 source_root,
341 |relative, is_directory| {
342 if visited % 256 == 0 {
343 if let Some(control) = control {
344 control.checkpoint()?;
345 }
346 }
347 visited += 1;
348 if backup_path_excluded(relative) {
349 return Ok(false);
350 }
351 if is_directory {
352 return Ok(true);
353 }
354 if deferred_runs.contains(relative) {
355 return Ok(false);
356 }
357 Ok(!(relative
358 .parent()
359 .and_then(Path::file_name)
360 .is_some_and(|parent| parent == "_runs")
361 && relative
362 .extension()
363 .is_some_and(|extension| extension == "sr")))
364 },
365 |relative| {
366 destination_root.create_directory_all(relative)?;
367 Ok(())
368 },
369 |relative, source| {
370 destination_root.copy_new_from(relative, source)?;
371 copied.push(relative.to_path_buf());
372 Ok(())
373 },
374 )
375}
376
377fn backup_path_excluded(relative: &Path) -> bool {
378 relative == Path::new("_meta/.lock")
379 || relative == Path::new("_meta/replica")
380 || relative == Path::new("_meta/repl_epoch")
381 || relative == Path::new(crate::backup::BACKUP_MANIFEST_PATH)
382 || relative.components().any(|component| {
383 matches!(component, std::path::Component::Normal(name) if name == "_cache" || name == "_txn" || name == "backup-pins")
384 })
385}
386
387#[derive(Debug, Clone)]
388pub enum ExternalTriggerWrite {
389 Insert {
390 table: String,
391 cells: Vec<(u16, Value)>,
392 },
393 UpdateByPk {
394 table: String,
395 pk: Value,
396 cells: Vec<(u16, Value)>,
397 },
398 DeleteByPk {
399 table: String,
400 pk: Value,
401 },
402}
403
404impl ExternalTriggerWrite {
405 fn table(&self) -> &str {
406 match self {
407 Self::Insert { table, .. }
408 | Self::UpdateByPk { table, .. }
409 | Self::DeleteByPk { table, .. } => table,
410 }
411 }
412}
413
414#[derive(Debug, Clone, PartialEq)]
415pub enum ExternalTriggerBaseWrite {
416 Put {
417 table: String,
418 cells: Vec<(u16, Value)>,
419 },
420 Delete {
421 table: String,
422 row_id: RowId,
423 },
424}
425
426#[derive(Debug, Clone, PartialEq)]
427pub struct ExternalTriggerWriteResult {
428 pub state: Vec<u8>,
429 pub base_writes: Vec<ExternalTriggerBaseWrite>,
430}
431
432impl ExternalTriggerWriteResult {
433 pub fn new(state: Vec<u8>) -> Self {
434 Self {
435 state,
436 base_writes: Vec::new(),
437 }
438 }
439}
440
441pub trait ExternalTriggerBridge: Send + Sync {
442 fn apply_trigger_external_write(
443 &self,
444 entry: &ExternalTableEntry,
445 base_state: Vec<u8>,
446 op: ExternalTriggerWrite,
447 ) -> Result<ExternalTriggerWriteResult>;
448}
449
450struct SpilledRun {
452 table_id: u64,
453 run_id: u128,
454 pending_path: PathBuf,
455 final_path: PathBuf,
456 rows: Vec<crate::memtable::Row>,
457 row_count: u64,
458 min_rid: u64,
459 max_rid: u64,
460 content_hash: [u8; 32],
461}
462
463const SPILLED_WAL_PAYLOAD_MAX_BYTES: usize = 24 * 1024 * 1024;
464const SPILLED_WAL_TOTAL_MAX_BYTES: usize = 256 * 1024 * 1024;
465
466fn encode_spilled_row_chunks(
467 rows: &[crate::memtable::Row],
468 total_bytes: &mut usize,
469 total_limit: usize,
470 control: Option<&crate::ExecutionControl>,
471) -> Result<Vec<Vec<u8>>> {
472 let mut output = Vec::new();
473 let mut start = 0;
474 while start < rows.len() {
475 let mut estimated_bytes = std::mem::size_of::<u64>();
479 let mut end = start;
480 while end < rows.len() {
481 if end % 256 == 0 {
482 if let Some(control) = control {
483 control.checkpoint()?;
484 }
485 }
486 let row_bytes =
487 usize::try_from(bincode::serialized_size(&rows[end])?).map_err(|_| {
488 MongrelError::ResourceLimitExceeded {
489 resource: "spilled WAL row bytes",
490 requested: usize::MAX,
491 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
492 }
493 })?;
494 let next_bytes = estimated_bytes.checked_add(row_bytes).ok_or(
495 MongrelError::ResourceLimitExceeded {
496 resource: "spilled WAL row bytes",
497 requested: usize::MAX,
498 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
499 },
500 )?;
501 if next_bytes > SPILLED_WAL_PAYLOAD_MAX_BYTES {
502 break;
503 }
504 estimated_bytes = next_bytes;
505 end += 1;
506 }
507 if end == start {
508 return Err(MongrelError::ResourceLimitExceeded {
509 resource: "spilled WAL row bytes",
510 requested: estimated_bytes.saturating_add(1),
511 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
512 });
513 }
514 let payload = bincode::serialize(&rows[start..end])?;
515 if payload.len() > SPILLED_WAL_PAYLOAD_MAX_BYTES {
516 return Err(MongrelError::ResourceLimitExceeded {
517 resource: "spilled WAL row bytes",
518 requested: payload.len(),
519 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
520 });
521 }
522 let requested = total_bytes.checked_add(payload.len()).unwrap_or(usize::MAX);
523 if requested > total_limit {
524 return Err(MongrelError::ResourceLimitExceeded {
525 resource: "spilled WAL transaction bytes",
526 requested,
527 limit: total_limit,
528 });
529 }
530 *total_bytes = requested;
531 output.push(payload);
532 start = end;
533 }
534 Ok(output)
535}
536
537#[cfg(test)]
538mod spilled_wal_encoding_tests {
539 use super::*;
540
541 #[test]
542 fn logical_spill_payload_has_a_total_bound() {
543 let rows = (0..4)
544 .map(|row_id| crate::memtable::Row {
545 row_id: crate::rowid::RowId(row_id),
546 committed_epoch: Epoch::ZERO,
547 columns: [(1, Value::Bytes(vec![0; 64]))].into_iter().collect(),
548 deleted: false,
549 })
550 .collect::<Vec<_>>();
551 let mut total = 0;
552 let error = encode_spilled_row_chunks(&rows, &mut total, 32, None).unwrap_err();
553 assert!(matches!(
554 error,
555 MongrelError::ResourceLimitExceeded {
556 resource: "spilled WAL transaction bytes",
557 ..
558 }
559 ));
560 }
561}
562
563struct PreparedRunLinks {
568 links: Vec<(PathBuf, PathBuf)>,
569 armed: bool,
570}
571
572impl PreparedRunLinks {
573 fn prepare(spilled: &[SpilledRun]) -> Result<Self> {
574 let mut guard = Self {
575 links: Vec::with_capacity(spilled.len()),
576 armed: true,
577 };
578 for run in spilled {
579 crate::durable_file::rename(&run.pending_path, &run.final_path)?;
580 guard
581 .links
582 .push((run.pending_path.clone(), run.final_path.clone()));
583 }
584 Ok(guard)
585 }
586
587 fn disarm(&mut self) {
588 self.armed = false;
589 for (pending, _) in &self.links {
590 if let Some(parent) = pending.parent() {
591 let _ = std::fs::remove_dir_all(parent);
592 }
593 }
594 }
595}
596
597impl Drop for PreparedRunLinks {
598 fn drop(&mut self) {
599 if !self.armed {
600 return;
601 }
602 for (pending, final_path) in self.links.iter().rev() {
603 let _ = std::fs::rename(final_path, pending);
604 }
605 }
606}
607
608struct TableApplyBatch {
609 table_id: u64,
610 handle: TableHandle,
611 ops: Vec<crate::txn::StagedOp>,
612}
613
614#[derive(Debug, Clone)]
615struct TriggerRowImage {
616 columns: HashMap<u16, Value>,
617}
618
619impl TriggerRowImage {
620 fn from_row(row: crate::memtable::Row) -> Self {
621 Self {
622 columns: row.columns,
623 }
624 }
625
626 fn from_cells(cells: &[(u16, Value)]) -> Self {
627 Self {
628 columns: cells.iter().cloned().collect(),
629 }
630 }
631}
632
633#[derive(Debug, Clone)]
634struct WriteEvent {
635 table: String,
636 kind: TriggerEvent,
637 old: Option<TriggerRowImage>,
638 new: Option<TriggerRowImage>,
639 changed_columns: Vec<u16>,
640 op_indices: Vec<usize>,
641 put_idx: Option<usize>,
642 trigger_stack: Vec<String>,
643}
644
645#[derive(Default)]
646struct TriggerExpansion {
647 before: Vec<(u64, crate::txn::Staged)>,
648 before_stacks: Vec<Vec<String>>,
649 before_external: Vec<ExternalTriggerWrite>,
650 after: Vec<(u64, crate::txn::Staged)>,
651 after_stacks: Vec<Vec<String>>,
652 after_external: Vec<ExternalTriggerWrite>,
653 ignored_indices: std::collections::BTreeSet<usize>,
654}
655
656#[derive(Clone, PartialEq)]
657struct TriggerCatalogBinding {
658 triggers: Vec<TriggerEntry>,
659 tables: Vec<(String, u64, u64)>,
660 external_tables: Vec<(String, u64, u64)>,
661}
662
663fn trigger_catalog_binding(catalog: &Catalog) -> Option<TriggerCatalogBinding> {
664 let mut triggers = catalog
665 .triggers
666 .iter()
667 .filter(|entry| entry.trigger.enabled)
668 .cloned()
669 .collect::<Vec<_>>();
670 if triggers.is_empty() {
671 return None;
672 }
673 triggers.sort_by(|left, right| left.trigger.name.cmp(&right.trigger.name));
674 let mut tables = catalog
675 .tables
676 .iter()
677 .filter(|entry| matches!(entry.state, TableState::Live))
678 .map(|entry| (entry.name.clone(), entry.table_id, entry.schema.schema_id))
679 .collect::<Vec<_>>();
680 tables.sort_unstable();
681 let mut external_tables = catalog
682 .external_tables
683 .iter()
684 .map(|entry| {
685 (
686 entry.name.clone(),
687 entry.created_epoch,
688 entry.declared_schema.schema_id,
689 )
690 })
691 .collect::<Vec<_>>();
692 external_tables.sort_unstable();
693 Some(TriggerCatalogBinding {
694 triggers,
695 tables,
696 external_tables,
697 })
698}
699
700struct TriggerProgramOutput<'a> {
701 added: &'a mut Vec<(u64, crate::txn::Staged)>,
702 added_stacks: &'a mut Vec<Vec<String>>,
703 added_external: &'a mut Vec<ExternalTriggerWrite>,
704 ignored_indices: &'a mut std::collections::BTreeSet<usize>,
705}
706
707#[derive(Debug, Clone, Copy, PartialEq, Eq)]
708enum TriggerProgramOutcome {
709 Continue,
710 Ignore,
711}
712
713#[derive(Debug, Clone)]
715pub struct CheckIssue {
716 pub table_id: u64,
717 pub table_name: String,
718 pub severity: String,
719 pub description: String,
720}
721
722#[derive(Debug, Clone)]
724pub struct AuthorizedReadSnapshot {
725 pub table: String,
726 pub table_snapshot: Snapshot,
727 pub data_generation: u64,
728 pub security_version: u64,
729 pub allowed_row_ids: Option<HashSet<RowId>>,
730}
731
732#[derive(Debug, Clone, Copy, PartialEq, Eq)]
734pub struct AuthorizedReadStamp {
735 pub table_id: u64,
736 pub schema_id: u64,
737 pub data_generation: u64,
738 pub security_version: u64,
739 pub snapshot: Snapshot,
740}
741
742type RlsCacheKey = (String, u64, u64, String);
743
744#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
746pub struct RlsCacheStats {
747 pub entries: usize,
748 pub bytes: usize,
749 pub hits: u64,
750 pub misses: u64,
751 pub evictions: u64,
752 pub build_nanos: u64,
753 pub rows_evaluated: u64,
754}
755
756const RLS_CACHE_MAX_BYTES: usize = 64 * 1024 * 1024;
757const CDC_MAX_WAL_RECORDS: usize = 1_000_000;
758const CDC_MAX_WAL_REPLAY_BYTES: usize = 256 * 1024 * 1024;
759const CDC_MAX_EVENTS: usize = 100_000;
760const CDC_MAX_ROWS: usize = 1_000_000;
761const CDC_MAX_INLINE_PAYLOAD_BYTES: usize = 32 * 1024 * 1024;
762const CDC_MAX_RETAINED_BYTES: usize = 256 * 1024 * 1024;
763
764fn charge_cdc_bytes(total: &mut usize, amount: usize, resource: &'static str) -> Result<()> {
765 let requested = total.saturating_add(amount);
766 if requested > CDC_MAX_RETAINED_BYTES {
767 return Err(MongrelError::ResourceLimitExceeded {
768 resource,
769 requested,
770 limit: CDC_MAX_RETAINED_BYTES,
771 });
772 }
773 *total = requested;
774 Ok(())
775}
776
777fn cdc_row_storage_bytes(row: &crate::memtable::Row) -> usize {
778 usize::try_from(row.estimated_bytes())
779 .unwrap_or(usize::MAX)
780 .saturating_add(std::mem::size_of::<crate::memtable::Row>())
781}
782
783fn cdc_row_json_bytes(row: &crate::memtable::Row) -> usize {
784 let value_slot = std::mem::size_of::<serde_json::Value>();
785 row.columns.values().fold(512_usize, |bytes, value| {
786 let values = match value {
787 Value::Bytes(values) => values.len(),
788 Value::Json(values) => values.len(),
789 Value::Embedding(values) => values.len(),
790 _ => 1,
791 };
792 bytes.saturating_add(values.saturating_mul(value_slot))
793 })
794}
795
796fn cdc_rows_json_bytes(rows: &[crate::memtable::Row]) -> usize {
797 rows.iter().fold(0_usize, |bytes, row| {
798 bytes.saturating_add(cdc_row_json_bytes(row))
799 })
800}
801
802#[derive(Default)]
803struct RlsCache {
804 entries: HashMap<RlsCacheKey, (Arc<HashSet<RowId>>, usize)>,
805 lru: VecDeque<RlsCacheKey>,
806 bytes: usize,
807 hits: u64,
808 misses: u64,
809 evictions: u64,
810 build_nanos: u64,
811 rows_evaluated: u64,
812}
813
814impl RlsCache {
815 fn get(&mut self, key: &RlsCacheKey) -> Option<Arc<HashSet<RowId>>> {
816 let value = self.entries.get(key).map(|(value, _)| Arc::clone(value));
817 if value.is_some() {
818 self.hits = self.hits.saturating_add(1);
819 self.touch(key);
820 } else {
821 self.misses = self.misses.saturating_add(1);
822 }
823 value
824 }
825
826 fn insert(&mut self, key: RlsCacheKey, value: Arc<HashSet<RowId>>) {
827 let bytes = key
828 .0
829 .len()
830 .saturating_add(key.3.len())
831 .saturating_add(
832 value
833 .capacity()
834 .saturating_mul(std::mem::size_of::<RowId>() * 3),
835 )
836 .saturating_add(std::mem::size_of::<RlsCacheKey>());
837 if bytes > RLS_CACHE_MAX_BYTES {
838 return;
839 }
840 if let Some((_, old_bytes)) = self.entries.remove(&key) {
841 self.bytes = self.bytes.saturating_sub(old_bytes);
842 }
843 self.lru.retain(|candidate| candidate != &key);
844 while self.bytes.saturating_add(bytes) > RLS_CACHE_MAX_BYTES {
845 let Some(oldest) = self.lru.pop_front() else {
846 break;
847 };
848 if let Some((_, old_bytes)) = self.entries.remove(&oldest) {
849 self.bytes = self.bytes.saturating_sub(old_bytes);
850 self.evictions = self.evictions.saturating_add(1);
851 }
852 }
853 self.bytes = self.bytes.saturating_add(bytes);
854 self.lru.push_back(key.clone());
855 self.entries.insert(key, (value, bytes));
856 }
857
858 fn touch(&mut self, key: &RlsCacheKey) {
859 self.lru.retain(|candidate| candidate != key);
860 self.lru.push_back(key.clone());
861 }
862
863 fn stats(&self) -> RlsCacheStats {
864 RlsCacheStats {
865 entries: self.entries.len(),
866 bytes: self.bytes,
867 hits: self.hits,
868 misses: self.misses,
869 evictions: self.evictions,
870 build_nanos: self.build_nanos,
871 rows_evaluated: self.rows_evaluated,
872 }
873 }
874}
875
876#[derive(Clone)]
878pub struct TableHandle {
879 inner: TableHandleInner,
880 generation_metrics: Arc<TableGenerationMetrics>,
881}
882
883#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
884pub struct TableGenerationStats {
885 pub active_read_generations: usize,
886 pub max_live_read_generations: usize,
887 pub cow_clone_count: u64,
888 pub cow_clone_nanos: u64,
889 pub estimated_cow_clone_bytes: u64,
890 pub writer_wait_nanos: u64,
891}
892
893#[derive(Default)]
894#[doc(hidden)]
895pub struct TableGenerationMetrics {
896 active_read_generations: AtomicUsize,
897 max_live_read_generations: AtomicUsize,
898 cow_clone_count: AtomicU64,
899 cow_clone_nanos: AtomicU64,
900 estimated_cow_clone_bytes: AtomicU64,
901 writer_wait_nanos: AtomicU64,
902}
903
904impl TableGenerationMetrics {
905 fn activate(self: &Arc<Self>, table: Table) -> Arc<TableReadGeneration> {
906 let active = self.active_read_generations.fetch_add(1, Ordering::Relaxed) + 1;
907 self.max_live_read_generations
908 .fetch_max(active, Ordering::Relaxed);
909 Arc::new(TableReadGeneration {
910 table,
911 metrics: Arc::clone(self),
912 })
913 }
914
915 fn stats(&self) -> TableGenerationStats {
916 TableGenerationStats {
917 active_read_generations: self.active_read_generations.load(Ordering::Relaxed),
918 max_live_read_generations: self.max_live_read_generations.load(Ordering::Relaxed),
919 cow_clone_count: self.cow_clone_count.load(Ordering::Relaxed),
920 cow_clone_nanos: self.cow_clone_nanos.load(Ordering::Relaxed),
921 estimated_cow_clone_bytes: self.estimated_cow_clone_bytes.load(Ordering::Relaxed),
922 writer_wait_nanos: self.writer_wait_nanos.load(Ordering::Relaxed),
923 }
924 }
925}
926
927pub struct TableReadGeneration {
929 table: Table,
930 metrics: Arc<TableGenerationMetrics>,
931}
932
933impl std::ops::Deref for TableReadGeneration {
934 type Target = Table;
935
936 fn deref(&self) -> &Self::Target {
937 &self.table
938 }
939}
940
941impl Drop for TableReadGeneration {
942 fn drop(&mut self) {
943 self.metrics
944 .active_read_generations
945 .fetch_sub(1, Ordering::Relaxed);
946 }
947}
948
949#[derive(Clone)]
950enum TableHandleInner {
951 CopyOnWrite(Arc<RwLock<Arc<Table>>>),
952 Direct(Arc<Mutex<Table>>),
953}
954
955pub enum TableGuard<'a> {
956 CopyOnWrite {
957 table: parking_lot::RwLockWriteGuard<'a, Arc<Table>>,
958 metrics: Arc<TableGenerationMetrics>,
959 },
960 Direct {
961 table: parking_lot::MutexGuard<'a, Table>,
962 },
963}
964
965impl TableHandle {
966 fn new(table: Table) -> Self {
967 Self {
968 inner: TableHandleInner::CopyOnWrite(Arc::new(RwLock::new(Arc::new(table)))),
969 generation_metrics: Arc::new(TableGenerationMetrics::default()),
970 }
971 }
972
973 pub fn from_table(table: Table) -> Self {
974 Self::new(table)
975 }
976
977 pub fn lock(&self) -> TableGuard<'_> {
978 let started = std::time::Instant::now();
979 let guard = match &self.inner {
980 TableHandleInner::CopyOnWrite(table) => TableGuard::CopyOnWrite {
981 table: table.write(),
982 metrics: Arc::clone(&self.generation_metrics),
983 },
984 TableHandleInner::Direct(table) => TableGuard::Direct {
985 table: table.lock(),
986 },
987 };
988 self.generation_metrics.writer_wait_nanos.fetch_add(
989 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
990 Ordering::Relaxed,
991 );
992 guard
993 }
994
995 fn try_lock_for(&self, timeout: std::time::Duration) -> Option<TableGuard<'_>> {
996 let started = std::time::Instant::now();
997 let guard = match &self.inner {
998 TableHandleInner::CopyOnWrite(table) => {
999 table
1000 .try_write_for(timeout)
1001 .map(|table| TableGuard::CopyOnWrite {
1002 table,
1003 metrics: Arc::clone(&self.generation_metrics),
1004 })
1005 }
1006 TableHandleInner::Direct(table) => table
1007 .try_lock_for(timeout)
1008 .map(|table| TableGuard::Direct { table }),
1009 };
1010 self.generation_metrics.writer_wait_nanos.fetch_add(
1011 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1012 Ordering::Relaxed,
1013 );
1014 guard
1015 }
1016
1017 pub fn ptr_eq(&self, other: &Self) -> bool {
1018 match (&self.inner, &other.inner) {
1019 (TableHandleInner::CopyOnWrite(left), TableHandleInner::CopyOnWrite(right)) => {
1020 Arc::ptr_eq(left, right)
1021 }
1022 (TableHandleInner::Direct(left), TableHandleInner::Direct(right)) => {
1023 Arc::ptr_eq(left, right)
1024 }
1025 _ => false,
1026 }
1027 }
1028
1029 pub fn read_generation_with_context(
1030 &self,
1031 context: Option<&crate::query::AiExecutionContext>,
1032 ) -> Result<(Arc<TableReadGeneration>, Snapshot)> {
1033 let mut table = if let Some(context) = context {
1034 loop {
1035 context.checkpoint()?;
1036 let wait = context
1037 .remaining_duration()
1038 .unwrap_or(std::time::Duration::from_millis(5))
1039 .min(std::time::Duration::from_millis(5));
1040 if let Some(table) = self.try_lock_for(wait) {
1041 break table;
1042 }
1043 }
1044 } else {
1045 self.lock()
1046 };
1047 let snapshot = table.snapshot();
1048 let generation = table.clone_read_generation()?;
1049 Ok((self.generation_metrics.activate(generation), snapshot))
1050 }
1051
1052 pub fn generation_stats(&self) -> TableGenerationStats {
1053 self.generation_metrics.stats()
1054 }
1055}
1056
1057impl From<Arc<Mutex<Table>>> for TableHandle {
1058 fn from(table: Arc<Mutex<Table>>) -> Self {
1059 Self {
1060 inner: TableHandleInner::Direct(table),
1061 generation_metrics: Arc::new(TableGenerationMetrics::default()),
1062 }
1063 }
1064}
1065
1066impl std::ops::Deref for TableGuard<'_> {
1067 type Target = Table;
1068
1069 fn deref(&self) -> &Self::Target {
1070 match self {
1071 Self::CopyOnWrite { table, .. } => table.as_ref(),
1072 Self::Direct { table } => table,
1073 }
1074 }
1075}
1076
1077impl std::ops::DerefMut for TableGuard<'_> {
1078 fn deref_mut(&mut self) -> &mut Self::Target {
1079 match self {
1080 Self::CopyOnWrite { table, metrics } => {
1081 if Arc::strong_count(table) > 1 || Arc::weak_count(table) > 0 {
1082 let estimated_bytes = table.estimated_clone_bytes();
1083 let started = std::time::Instant::now();
1084 let table = Arc::make_mut(table);
1085 metrics.cow_clone_count.fetch_add(1, Ordering::Relaxed);
1086 metrics.cow_clone_nanos.fetch_add(
1087 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1088 Ordering::Relaxed,
1089 );
1090 metrics
1091 .estimated_cow_clone_bytes
1092 .fetch_add(estimated_bytes, Ordering::Relaxed);
1093 table
1094 } else {
1095 Arc::make_mut(table)
1096 }
1097 }
1098 Self::Direct { table } => table,
1099 }
1100 }
1101}
1102
1103#[derive(Clone, Debug)]
1104pub struct ReadAuthorization {
1105 pub operation: crate::auth::ColumnOperation,
1106 pub columns: Vec<u16>,
1107 pub permissions: Vec<crate::auth::Permission>,
1108}
1109
1110#[derive(Default, Debug)]
1111struct TableWritePermissionNeeds {
1112 insert: bool,
1113 insert_columns: Vec<u16>,
1114 update: bool,
1115 update_columns: Vec<u16>,
1116 delete: bool,
1117 truncate: bool,
1118}
1119
1120#[cfg(test)]
1121thread_local! {
1122 static WRITE_PERMISSION_DECISIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1123 static AUTO_INCREMENT_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1124 static PREBUILD_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1125 static PUBLISH_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1126 static COMMIT_MANIFEST_WRITES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1127 static TABLE_PERMISSION_DECISIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1128}
1129
1130fn summarize_write_permissions(
1131 staging: &[(u64, crate::txn::Staged)],
1132) -> HashMap<u64, TableWritePermissionNeeds> {
1133 use crate::txn::Staged;
1134
1135 let mut needs = HashMap::<u64, TableWritePermissionNeeds>::new();
1136 for (table_id, operation) in staging {
1137 let table = needs.entry(*table_id).or_default();
1138 match operation {
1139 Staged::Put(cells) => {
1140 table.insert = true;
1141 table
1142 .insert_columns
1143 .extend(cells.iter().map(|(column, _)| *column));
1144 }
1145 Staged::Update {
1146 changed_columns, ..
1147 } => {
1148 table.update = true;
1149 table.update_columns.extend(changed_columns);
1150 }
1151 Staged::Delete(_) => table.delete = true,
1152 Staged::Truncate => table.truncate = true,
1153 }
1154 }
1155 for table in needs.values_mut() {
1156 table.insert_columns.sort_unstable();
1157 table.insert_columns.dedup();
1158 table.update_columns.sort_unstable();
1159 table.update_columns.dedup();
1160 }
1161 needs
1162}
1163
1164struct SecurityCoordinator {
1165 gate: RwLock<()>,
1167 version: AtomicU64,
1168}
1169
1170fn security_coordinator(root: &Path, version: u64) -> Arc<SecurityCoordinator> {
1171 static COORDINATORS: std::sync::OnceLock<
1172 Mutex<HashMap<PathBuf, std::sync::Weak<SecurityCoordinator>>>,
1173 > = std::sync::OnceLock::new();
1174
1175 let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
1176 let mut coordinators = COORDINATORS
1177 .get_or_init(|| Mutex::new(HashMap::new()))
1178 .lock();
1179 coordinators.retain(|_, coordinator| coordinator.strong_count() > 0);
1180 if let Some(coordinator) = coordinators.get(&root).and_then(std::sync::Weak::upgrade) {
1181 return coordinator;
1182 }
1183 let coordinator = Arc::new(SecurityCoordinator {
1184 gate: RwLock::new(()),
1185 version: AtomicU64::new(version),
1186 });
1187 coordinators.insert(root, Arc::downgrade(&coordinator));
1188 coordinator
1189}
1190
1191pub fn lock_table_with_context<'a>(
1192 handle: &'a TableHandle,
1193 context: Option<&crate::query::AiExecutionContext>,
1194) -> Result<TableGuard<'a>> {
1195 let Some(context) = context else {
1196 return Ok(handle.lock());
1197 };
1198 loop {
1199 context.checkpoint()?;
1200 let wait = context
1201 .remaining_duration()
1202 .unwrap_or(std::time::Duration::from_millis(5))
1203 .min(std::time::Duration::from_millis(5));
1204 if let Some(guard) = handle.try_lock_for(wait) {
1205 return Ok(guard);
1206 }
1207 }
1208}
1209
1210#[derive(Clone, Debug, Default)]
1216pub struct OpenOptions {
1217 pub lock_timeout_ms: u32,
1232}
1233
1234impl OpenOptions {
1235 pub fn with_lock_timeout_ms(mut self, ms: u32) -> Self {
1238 self.lock_timeout_ms = ms;
1239 self
1240 }
1241}
1242
1243pub struct Database {
1246 root: PathBuf,
1247 durable_root: Arc<crate::durable_file::DurableRoot>,
1248 read_only: bool,
1250 catalog: RwLock<Catalog>,
1251 security_coordinator: Arc<SecurityCoordinator>,
1252 security_catalog_disk_reads: AtomicU64,
1253 rls_cache: Mutex<RlsCache>,
1254 epoch: Arc<EpochAuthority>,
1255 snapshots: Arc<SnapshotRegistry>,
1256 page_cache: Arc<crate::cache::Sharded<crate::cache::PageCache>>,
1257 decoded_cache: Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>,
1258 commit_lock: Arc<Mutex<()>>,
1259 shared_wal: Arc<Mutex<crate::wal::SharedWal>>,
1264 next_txn_id: Arc<Mutex<u64>>,
1268 tables: RwLock<HashMap<u64, TableHandle>>,
1269 kek: Option<Arc<crate::encryption::Kek>>,
1270 ddl_lock: Mutex<()>,
1273 meta_dek: Option<[u8; META_DEK_LEN]>,
1274 spill_threshold: std::sync::atomic::AtomicU64,
1277 conflicts: crate::txn::ConflictIndex,
1280 active_txns: crate::txn::ActiveTxns,
1283 poisoned: Arc<std::sync::atomic::AtomicBool>,
1286 group: Arc<crate::txn::GroupCommit>,
1290 active_spills: Arc<crate::retention::ActiveSpills>,
1293 replication_barrier: parking_lot::RwLock<()>,
1296 replication_wal_retention_segments: AtomicUsize,
1298 backup_pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
1301 #[doc(hidden)]
1305 spill_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1306 #[doc(hidden)]
1308 security_commit_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1309 #[doc(hidden)]
1312 catalog_commit_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1313 #[doc(hidden)]
1316 backup_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1317 replication_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1318 trigger_recursive: AtomicBool,
1319 trigger_max_depth: AtomicU32,
1320 trigger_max_loop_iterations: AtomicU32,
1321 _lock: Option<DatabaseFileLock>,
1324 notify: tokio::sync::broadcast::Sender<ChangeEvent>,
1327 change_wake: tokio::sync::broadcast::Sender<()>,
1330 principal: RwLock<Option<crate::auth::Principal>>,
1340 auth_state: crate::auth_state::AuthState,
1346}
1347
1348struct RunPins {
1349 pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
1350 runs: Vec<(u64, u128)>,
1351}
1352
1353struct BackupFilePins {
1354 root: PathBuf,
1355}
1356
1357struct PendingTableDir {
1358 path: PathBuf,
1359 armed: bool,
1360}
1361
1362impl PendingTableDir {
1363 fn new(path: PathBuf) -> Self {
1364 Self { path, armed: true }
1365 }
1366
1367 fn disarm(&mut self) {
1368 self.armed = false;
1369 }
1370}
1371
1372impl Drop for PendingTableDir {
1373 fn drop(&mut self) {
1374 if self.armed {
1375 let _ = std::fs::remove_dir_all(&self.path);
1376 }
1377 }
1378}
1379
1380impl Drop for BackupFilePins {
1381 fn drop(&mut self) {
1382 let _ = std::fs::remove_dir_all(&self.root);
1383 }
1384}
1385
1386impl Drop for RunPins {
1387 fn drop(&mut self) {
1388 let mut pins = self.pins.lock();
1389 for run in &self.runs {
1390 if let Some(count) = pins.get_mut(run) {
1391 *count -= 1;
1392 if *count == 0 {
1393 pins.remove(run);
1394 }
1395 }
1396 }
1397 }
1398}
1399
1400#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1403pub struct ChangeEvent {
1404 pub id: Option<String>,
1405 pub channel: String,
1406 pub table_id: Option<u64>,
1407 pub table: String,
1408 pub op: String,
1409 pub epoch: u64,
1410 pub txn_id: Option<u64>,
1411 pub message: Option<String>,
1412 pub data: Option<serde_json::Value>,
1413}
1414
1415#[derive(Debug, Clone)]
1416pub struct CdcBatch {
1417 pub events: Vec<ChangeEvent>,
1418 pub current_epoch: u64,
1419 pub earliest_epoch: Option<u64>,
1420 pub gap: bool,
1421}
1422
1423impl std::fmt::Debug for Database {
1430 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1431 let cat = self.catalog.read();
1432 let principal_guard = self.principal.read();
1433 let principal: &str = principal_guard
1434 .as_ref()
1435 .map(|p| p.username.as_str())
1436 .unwrap_or("<none>");
1437 f.debug_struct("Database")
1438 .field("root", &self.root)
1439 .field("db_epoch", &cat.db_epoch)
1440 .field("open_generation", &"sidecar")
1441 .field("tables", &cat.tables.len())
1442 .field("visible_epoch", &self.epoch.visible().0)
1443 .field("encrypted", &self.kek.is_some())
1444 .field("require_auth", &cat.require_auth)
1445 .field("principal", &principal)
1446 .finish()
1447 }
1448}
1449
1450impl Database {
1451 fn canonical_lock_target(root: &Path) -> std::io::Result<(PathBuf, PathBuf)> {
1452 if let Ok(canonical) = root.canonicalize() {
1453 let lock_dir = canonical.parent().ok_or_else(|| {
1454 std::io::Error::new(
1455 std::io::ErrorKind::InvalidInput,
1456 "database root must have a parent directory",
1457 )
1458 })?;
1459 return Ok((canonical.clone(), lock_dir.to_path_buf()));
1460 }
1461
1462 let absolute = if root.is_absolute() {
1463 root.to_path_buf()
1464 } else {
1465 std::env::current_dir()?.join(root)
1466 };
1467 let mut cursor = absolute.as_path();
1468 let mut suffix = Vec::new();
1469 while !cursor.exists() {
1470 let name = cursor.file_name().ok_or_else(|| {
1471 std::io::Error::new(
1472 std::io::ErrorKind::NotFound,
1473 format!("no existing ancestor for database root {}", root.display()),
1474 )
1475 })?;
1476 suffix.push(name.to_os_string());
1477 cursor = cursor.parent().ok_or_else(|| {
1478 std::io::Error::new(
1479 std::io::ErrorKind::NotFound,
1480 format!("no existing ancestor for database root {}", root.display()),
1481 )
1482 })?;
1483 }
1484 let lock_dir = cursor.canonicalize()?;
1485 let mut canonical = lock_dir.clone();
1486 for component in suffix.iter().rev() {
1487 canonical.push(component);
1488 }
1489 Ok((canonical, lock_dir))
1490 }
1491
1492 fn acquire_database_lock(root: &Path, timeout_ms: u32) -> Result<DatabaseFileLock> {
1493 use std::hash::{Hash, Hasher};
1494
1495 let (canonical_path, lock_dir) = Self::canonical_lock_target(root)?;
1496 let mut process_locks = process_database_locks().lock();
1497 if let Some(shared) = process_locks
1498 .get(&canonical_path)
1499 .and_then(std::sync::Weak::upgrade)
1500 {
1501 return Ok(DatabaseFileLock {
1502 shared,
1503 canonical_path,
1504 durable_root: None,
1505 });
1506 }
1507
1508 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1509 canonical_path.hash(&mut hasher);
1510 let lock_path = lock_dir.join(format!(".mongreldb-{:016x}.lock", hasher.finish()));
1511 let file = std::fs::OpenOptions::new()
1512 .create(true)
1513 .truncate(false)
1514 .write(true)
1515 .open(lock_path)?;
1516 if let Err(error) = Self::fs_lock_exclusive(&file, timeout_ms) {
1517 return Err(MongrelError::DatabaseLocked {
1518 path: root.to_path_buf(),
1519 message: error.to_string(),
1520 });
1521 }
1522 let shared = Arc::new(SharedDatabaseLock {
1523 bootstrap_file: file,
1524 legacy_file: Mutex::new(None),
1525 canonical_path: canonical_path.clone(),
1526 });
1527 process_locks.insert(canonical_path.clone(), Arc::downgrade(&shared));
1528 Ok(DatabaseFileLock {
1529 shared,
1530 canonical_path,
1531 durable_root: None,
1532 })
1533 }
1534
1535 fn acquire_legacy_database_lock(
1536 lock: &mut DatabaseFileLock,
1537 root: &Path,
1538 timeout_ms: u32,
1539 ) -> Result<()> {
1540 let durable_root = lock
1541 .durable_root
1542 .as_ref()
1543 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?;
1544 let mut legacy_file = lock.shared.legacy_file.lock();
1545 if legacy_file.is_some() {
1546 return Ok(());
1547 }
1548 let file = durable_root.open_lock_file(Path::new(META_DIR).join(".lock"))?;
1549 if let Err(error) = Self::fs_lock_exclusive(&file, timeout_ms) {
1550 return Err(MongrelError::DatabaseLocked {
1551 path: root.to_path_buf(),
1552 message: error.to_string(),
1553 });
1554 }
1555 *legacy_file = Some(file);
1556 Ok(())
1557 }
1558
1559 fn pin_or_create_database_root(path: &Path) -> Result<crate::durable_file::DurableRoot> {
1560 if path.exists() {
1561 return crate::durable_file::DurableRoot::open(path).map_err(Into::into);
1562 }
1563 let mut ancestor = path;
1564 while !ancestor.exists() {
1565 ancestor = ancestor.parent().ok_or_else(|| {
1566 MongrelError::NotFound(format!(
1567 "no existing ancestor for database root {}",
1568 path.display()
1569 ))
1570 })?;
1571 }
1572 let relative = path.strip_prefix(ancestor).map_err(|error| {
1573 MongrelError::InvalidArgument(format!("invalid database root: {error}"))
1574 })?;
1575 crate::durable_file::DurableRoot::open(ancestor)?
1576 .create_directory_all_pinned(relative)
1577 .map_err(Into::into)
1578 }
1579
1580 fn begin_create(root: impl AsRef<Path>) -> Result<(PathBuf, DatabaseFileLock)> {
1581 let requested_root = root.as_ref();
1582 let mut lock = Self::acquire_database_lock(requested_root, 0)?;
1583 let root = lock.canonical_path.clone();
1584 Self::reject_existing_database(&root)?;
1585 let durable_root = Arc::new(Self::pin_or_create_database_root(&root)?);
1586 if durable_root.canonical_path() != lock.canonical_path {
1587 return Err(MongrelError::Conflict(
1588 "database root changed while it was being created".into(),
1589 ));
1590 }
1591 durable_root.create_directory_all(META_DIR)?;
1592 lock.durable_root = Some(durable_root);
1593 let io_root = lock
1594 .durable_root
1595 .as_ref()
1596 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
1597 .io_path()?;
1598 Self::acquire_legacy_database_lock(&mut lock, &io_root, 0)?;
1599 Self::reject_existing_database(&io_root)?;
1600 Ok((io_root, lock))
1601 }
1602
1603 fn begin_open(
1604 root: impl AsRef<Path>,
1605 lock_timeout_ms: u32,
1606 ) -> Result<(PathBuf, DatabaseFileLock)> {
1607 let root = root.as_ref();
1608 let durable_root = crate::durable_file::DurableRoot::open(root).map_err(|error| {
1609 if error.kind() == std::io::ErrorKind::NotFound {
1610 MongrelError::NotFound(format!("database root {}: {error}", root.display()))
1611 } else {
1612 error.into()
1613 }
1614 })?;
1615 Self::begin_open_durable(durable_root, lock_timeout_ms)
1616 }
1617
1618 fn begin_open_durable(
1619 durable_root: crate::durable_file::DurableRoot,
1620 lock_timeout_ms: u32,
1621 ) -> Result<(PathBuf, DatabaseFileLock)> {
1622 let io_root = durable_root.io_path()?;
1623 let current_root = io_root.canonicalize()?;
1624 let mut lock = Self::acquire_database_lock(¤t_root, lock_timeout_ms)?;
1625 lock.durable_root = Some(Arc::new(durable_root));
1626 let io_root = lock
1627 .durable_root
1628 .as_ref()
1629 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
1630 .io_path()?;
1631 if lock
1632 .durable_root
1633 .as_ref()
1634 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
1635 .open_directory(META_DIR)
1636 .is_err()
1637 {
1638 return Err(MongrelError::NotFound(format!(
1639 "no database metadata found at {:?}",
1640 current_root
1641 )));
1642 }
1643 Self::acquire_legacy_database_lock(&mut lock, &io_root, lock_timeout_ms)?;
1644 Ok((io_root, lock))
1645 }
1646
1647 pub fn create(root: impl AsRef<Path>) -> Result<Self> {
1649 let (root, lock) = Self::begin_create(root)?;
1650 Self::create_inner(root, None, lock)
1651 }
1652
1653 #[cfg(feature = "encryption")]
1656 pub fn create_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
1657 let (root, lock) = Self::begin_create(root)?;
1658 let salt = crate::encryption::random_salt()?;
1659 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
1660 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1661 Self::create_inner(root, Some(kek), lock)
1662 }
1663
1664 #[cfg(feature = "encryption")]
1667 pub fn create_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
1668 let (root, lock) = Self::begin_create(root)?;
1669 let salt = crate::encryption::random_salt()?;
1670 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
1671 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
1672 Self::create_inner(root, Some(kek), lock)
1673 }
1674
1675 fn create_inner(
1676 root: PathBuf,
1677 kek: Option<Arc<crate::encryption::Kek>>,
1678 lock: DatabaseFileLock,
1679 ) -> Result<Self> {
1680 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
1681 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
1682 let cat = Catalog::empty();
1683 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
1684 Self::finish_open(root, cat, kek, meta_dek, false, None, None, None, lock)
1685 }
1686
1687 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
1689 Self::open_inner(root, None, None)
1690 }
1691
1692 #[cfg(feature = "encryption")]
1694 pub fn open_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
1695 let (root, lock) = Self::begin_open(root, 0)?;
1696 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
1697 MongrelError::Other("database root descriptor was not pinned".into())
1698 })?)?;
1699 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1700 Self::open_inner_locked(root, Some(kek), lock)
1701 }
1702
1703 #[cfg(feature = "encryption")]
1706 pub fn open_encrypted_with_options(
1707 root: impl AsRef<Path>,
1708 passphrase: &str,
1709 options: OpenOptions,
1710 ) -> Result<Self> {
1711 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
1712 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
1713 MongrelError::Other("database root descriptor was not pinned".into())
1714 })?)?;
1715 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1716 Self::open_inner_locked(root, Some(kek), lock)
1717 }
1718
1719 #[cfg(feature = "encryption")]
1721 pub fn open_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
1722 let (root, lock) = Self::begin_open(root, 0)?;
1723 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
1724 MongrelError::Other("database root descriptor was not pinned".into())
1725 })?)?;
1726 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
1727 Self::open_inner_locked(root, Some(kek), lock)
1728 }
1729
1730 pub fn open_with_credentials(
1742 root: impl AsRef<Path>,
1743 username: &str,
1744 password: &str,
1745 ) -> Result<Self> {
1746 Self::open_inner_with_credentials(root, None, username, password)
1747 }
1748
1749 pub fn open_with_credentials_and_options(
1753 root: impl AsRef<Path>,
1754 username: &str,
1755 password: &str,
1756 options: OpenOptions,
1757 ) -> Result<Self> {
1758 Self::open_inner_with_credentials_and_lock_timeout(
1759 root,
1760 None,
1761 username,
1762 password,
1763 options.lock_timeout_ms,
1764 )
1765 }
1766
1767 #[cfg(feature = "encryption")]
1770 pub fn open_encrypted_with_credentials(
1771 root: impl AsRef<Path>,
1772 passphrase: &str,
1773 username: &str,
1774 password: &str,
1775 ) -> Result<Self> {
1776 let (root, lock) = Self::begin_open(root, 0)?;
1777 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
1778 MongrelError::Other("database root descriptor was not pinned".into())
1779 })?)?;
1780 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1781 Self::open_inner_with_credentials_locked(root, Some(kek), username, password, lock)
1782 }
1783
1784 #[cfg(feature = "encryption")]
1788 pub fn open_encrypted_with_credentials_and_options(
1789 root: impl AsRef<Path>,
1790 passphrase: &str,
1791 username: &str,
1792 password: &str,
1793 options: OpenOptions,
1794 ) -> Result<Self> {
1795 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
1796 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
1797 MongrelError::Other("database root descriptor was not pinned".into())
1798 })?)?;
1799 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1800 Self::open_inner_with_credentials_locked(root, Some(kek), username, password, lock)
1801 }
1802
1803 pub fn open_with_options(root: impl AsRef<Path>, options: OpenOptions) -> Result<Self> {
1810 Self::open_inner_with_lock_timeout(root, None, None, options.lock_timeout_ms)
1813 }
1814
1815 fn open_inner_with_lock_timeout(
1816 root: impl AsRef<Path>,
1817 kek: Option<Arc<crate::encryption::Kek>>,
1818 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
1819 lock_timeout_ms: u32,
1820 ) -> Result<Self> {
1821 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
1822 Self::open_inner_locked(root, kek, lock)
1823 }
1824
1825 fn open_inner_locked(
1826 root: PathBuf,
1827 kek: Option<Arc<crate::encryption::Kek>>,
1828 lock: DatabaseFileLock,
1829 ) -> Result<Self> {
1830 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
1831 let mut cat = catalog::read_durable(
1832 lock.durable_root.as_deref().ok_or_else(|| {
1833 MongrelError::Other("database root descriptor was not pinned".into())
1834 })?,
1835 meta_dek.as_ref(),
1836 )?
1837 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
1838 let recovery_checkpoint = cat.clone();
1839
1840 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
1843 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
1844 lock.durable_root.as_deref().ok_or_else(|| {
1845 MongrelError::Other("database root descriptor was not pinned".into())
1846 })?,
1847 wal_dek.as_ref(),
1848 )?;
1849 recover_ddl_from_records(
1850 &root,
1851 Some(lock.durable_root.as_deref().ok_or_else(|| {
1852 MongrelError::Other("database root descriptor was not pinned".into())
1853 })?),
1854 &mut cat,
1855 meta_dek.as_ref(),
1856 false,
1857 None,
1858 &recovery_records,
1859 )?;
1860 Self::finish_open(
1861 root,
1862 cat,
1863 kek,
1864 meta_dek,
1865 true,
1866 Some(recovery_checkpoint),
1867 Some(recovery_records),
1868 None,
1869 lock,
1870 )
1871 }
1872
1873 fn open_inner_with_credentials(
1880 root: impl AsRef<Path>,
1881 kek: Option<Arc<crate::encryption::Kek>>,
1882 username: &str,
1883 password: &str,
1884 ) -> Result<Self> {
1885 Self::open_inner_with_credentials_and_lock_timeout(root, kek, username, password, 0)
1886 }
1887
1888 fn open_inner_with_credentials_and_lock_timeout(
1892 root: impl AsRef<Path>,
1893 kek: Option<Arc<crate::encryption::Kek>>,
1894 username: &str,
1895 password: &str,
1896 lock_timeout_ms: u32,
1897 ) -> Result<Self> {
1898 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
1899 Self::open_inner_with_credentials_locked(root, kek, username, password, lock)
1900 }
1901
1902 fn open_inner_with_credentials_locked(
1903 root: PathBuf,
1904 kek: Option<Arc<crate::encryption::Kek>>,
1905 username: &str,
1906 password: &str,
1907 lock: DatabaseFileLock,
1908 ) -> Result<Self> {
1909 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
1910 let mut cat = catalog::read_durable(
1911 lock.durable_root.as_deref().ok_or_else(|| {
1912 MongrelError::Other("database root descriptor was not pinned".into())
1913 })?,
1914 meta_dek.as_ref(),
1915 )?
1916 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
1917 let recovery_checkpoint = cat.clone();
1918
1919 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
1922 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
1923 lock.durable_root.as_deref().ok_or_else(|| {
1924 MongrelError::Other("database root descriptor was not pinned".into())
1925 })?,
1926 wal_dek.as_ref(),
1927 )?;
1928 recover_ddl_from_records(
1929 &root,
1930 Some(lock.durable_root.as_deref().ok_or_else(|| {
1931 MongrelError::Other("database root descriptor was not pinned".into())
1932 })?),
1933 &mut cat,
1934 meta_dek.as_ref(),
1935 false,
1936 None,
1937 &recovery_records,
1938 )?;
1939
1940 if !cat.require_auth {
1943 return Err(MongrelError::AuthNotRequired);
1944 }
1945
1946 let user = cat
1951 .users
1952 .iter()
1953 .find(|u| u.username == username)
1954 .filter(|u| !u.password_hash.is_empty())
1955 .ok_or_else(|| MongrelError::InvalidCredentials {
1956 username: username.to_string(),
1957 })?;
1958 let password_ok = crate::auth::verify_password(password, &user.password_hash)
1959 .map_err(MongrelError::Other)?;
1960 if !password_ok {
1961 return Err(MongrelError::InvalidCredentials {
1962 username: username.to_string(),
1963 });
1964 }
1965
1966 let principal =
1968 Self::resolve_principal_from_catalog(&cat, &user.username).ok_or_else(|| {
1969 MongrelError::InvalidCredentials {
1970 username: username.to_string(),
1971 }
1972 })?;
1973
1974 Self::finish_open(
1975 root,
1976 cat,
1977 kek,
1978 meta_dek,
1979 true,
1980 Some(recovery_checkpoint),
1981 Some(recovery_records),
1982 Some(principal),
1983 lock,
1984 )
1985 }
1986
1987 pub fn create_with_credentials(
1997 root: impl AsRef<Path>,
1998 admin_username: &str,
1999 admin_password: &str,
2000 ) -> Result<Self> {
2001 let (root, lock) = Self::begin_create(root)?;
2002 Self::create_inner_with_credentials(root, None, admin_username, admin_password, lock)
2003 }
2004
2005 #[cfg(feature = "encryption")]
2009 pub fn create_encrypted_with_credentials(
2010 root: impl AsRef<Path>,
2011 passphrase: &str,
2012 admin_username: &str,
2013 admin_password: &str,
2014 ) -> Result<Self> {
2015 let (root, lock) = Self::begin_create(root)?;
2016 let salt = crate::encryption::random_salt()?;
2017 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
2018 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2019 Self::create_inner_with_credentials(root, Some(kek), admin_username, admin_password, lock)
2020 }
2021
2022 fn create_inner_with_credentials(
2023 root: PathBuf,
2024 kek: Option<Arc<crate::encryption::Kek>>,
2025 admin_username: &str,
2026 admin_password: &str,
2027 lock: DatabaseFileLock,
2028 ) -> Result<Self> {
2029 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
2030 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2031
2032 let password_hash =
2034 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
2035 let mut cat = Catalog::empty();
2036 cat.require_auth = true;
2037 cat.next_user_id = 2;
2038 cat.users.push(crate::auth::UserEntry {
2039 id: 1,
2040 username: admin_username.to_string(),
2041 password_hash,
2042 roles: Vec::new(),
2043 is_admin: true,
2044 created_epoch: 0,
2045 });
2046 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
2047
2048 let admin_principal = crate::auth::Principal {
2051 user_id: 1,
2052 created_epoch: 0,
2053 username: admin_username.to_string(),
2054 is_admin: true,
2055 roles: Vec::new(),
2056 permissions: Vec::new(),
2057 };
2058 Self::finish_open(
2059 root,
2060 cat,
2061 kek,
2062 meta_dek,
2063 false,
2064 None,
2065 None,
2066 Some(admin_principal),
2067 lock,
2068 )
2069 }
2070
2071 fn reject_existing_database(root: &Path) -> Result<()> {
2072 if root.join(catalog::CATALOG_FILENAME).exists() {
2075 return Err(MongrelError::InvalidArgument(format!(
2076 "database already exists at {}; use Database::open() to open it, \
2077 or remove the directory first",
2078 root.display()
2079 )));
2080 }
2081 Ok(())
2082 }
2083
2084 fn open_inner(
2085 root: impl AsRef<Path>,
2086 kek: Option<Arc<crate::encryption::Kek>>,
2087 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
2088 ) -> Result<Self> {
2089 Self::open_inner_with_lock_timeout(root, kek, None, 0)
2090 }
2091
2092 pub(crate) fn open_replica_recovery_durable(
2096 root: &crate::durable_file::DurableRoot,
2097 ) -> Result<Self> {
2098 let (root, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
2099 Self::open_replica_recovery_inner(root, None, lock)
2100 }
2101
2102 #[cfg(feature = "encryption")]
2103 pub(crate) fn open_encrypted_replica_recovery_durable(
2104 root: &crate::durable_file::DurableRoot,
2105 passphrase: &str,
2106 ) -> Result<Self> {
2107 let (root_path, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
2108 let salt = read_encryption_salt(root)?;
2109 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2110 Self::open_replica_recovery_inner(root_path, Some(kek), lock)
2111 }
2112
2113 fn open_replica_recovery_inner(
2114 root: PathBuf,
2115 kek: Option<Arc<crate::encryption::Kek>>,
2116 lock: DatabaseFileLock,
2117 ) -> Result<Self> {
2118 if !root.join(META_DIR).join("replica").is_file() {
2119 return Err(MongrelError::InvalidArgument(
2120 "recovery auth bypass requires a marked replica staging directory".into(),
2121 ));
2122 }
2123 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2124 let mut cat = catalog::read_durable(
2125 lock.durable_root.as_deref().ok_or_else(|| {
2126 MongrelError::Other("database root descriptor was not pinned".into())
2127 })?,
2128 meta_dek.as_ref(),
2129 )?
2130 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
2131 let recovery_checkpoint = cat.clone();
2132 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
2133 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
2134 lock.durable_root.as_deref().ok_or_else(|| {
2135 MongrelError::Other("database root descriptor was not pinned".into())
2136 })?,
2137 wal_dek.as_ref(),
2138 )?;
2139 recover_ddl_from_records(
2140 &root,
2141 Some(lock.durable_root.as_deref().ok_or_else(|| {
2142 MongrelError::Other("database root descriptor was not pinned".into())
2143 })?),
2144 &mut cat,
2145 meta_dek.as_ref(),
2146 false,
2147 None,
2148 &recovery_records,
2149 )?;
2150 let principal = if cat.require_auth {
2151 cat.users
2152 .iter()
2153 .find(|user| user.is_admin)
2154 .and_then(|user| Self::resolve_principal_from_catalog(&cat, &user.username))
2155 .ok_or_else(|| {
2156 MongrelError::Schema(
2157 "authenticated replica catalog has no recoverable admin".into(),
2158 )
2159 })?
2160 .into()
2161 } else {
2162 None
2163 };
2164 Self::finish_open(
2165 root,
2166 cat,
2167 kek,
2168 meta_dek,
2169 true,
2170 Some(recovery_checkpoint),
2171 Some(recovery_records),
2172 principal,
2173 lock,
2174 )
2175 }
2176
2177 fn fs_lock_exclusive(f: &std::fs::File, timeout_ms: u32) -> std::io::Result<()> {
2189 use fs2::FileExt;
2190 if timeout_ms == 0 {
2191 return f.try_lock_exclusive();
2192 }
2193 let deadline =
2195 std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms as u64);
2196 let mut next_sleep = std::time::Duration::from_millis(1);
2197 loop {
2198 match f.try_lock_exclusive() {
2199 Ok(()) => return Ok(()),
2200 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
2201 let now = std::time::Instant::now();
2202 if now >= deadline {
2203 return Err(std::io::Error::new(
2204 std::io::ErrorKind::WouldBlock,
2205 format!("could not acquire database lock within {timeout_ms}ms"),
2206 ));
2207 }
2208 let remaining = deadline - now;
2209 let sleep = next_sleep.min(remaining);
2210 std::thread::sleep(sleep);
2211 next_sleep = next_sleep
2214 .saturating_mul(10)
2215 .min(std::time::Duration::from_millis(50));
2216 }
2217 Err(e) => return Err(e),
2218 }
2219 }
2220 }
2221
2222 #[allow(clippy::too_many_arguments)]
2223 fn finish_open(
2224 root: PathBuf,
2225 cat: Catalog,
2226 kek: Option<Arc<crate::encryption::Kek>>,
2227 meta_dek: Option<[u8; META_DEK_LEN]>,
2228 existing: bool,
2229 recovery_checkpoint: Option<Catalog>,
2230 recovery_records: Option<Vec<crate::wal::Record>>,
2231 principal: Option<crate::auth::Principal>,
2232 lock: DatabaseFileLock,
2233 ) -> Result<Self> {
2234 let durable_root = Arc::clone(lock.durable_root.as_ref().ok_or_else(|| {
2235 MongrelError::Other("database root descriptor was not pinned".into())
2236 })?);
2237 let read_only = if existing {
2238 match durable_root.open_regular(Path::new(META_DIR).join("replica")) {
2239 Ok(_) => true,
2240 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
2241 Err(error) => return Err(error.into()),
2242 }
2243 } else {
2244 false
2245 };
2246 let recovered_catalog = cat;
2247 let mut cat = recovered_catalog.clone();
2248 let abandoned = if existing && !read_only {
2249 let abandoned = cat
2250 .tables
2251 .iter()
2252 .filter(|entry| matches!(entry.state, TableState::Building { .. }))
2253 .map(|entry| entry.table_id)
2254 .collect::<Vec<_>>();
2255 for entry in &mut cat.tables {
2256 if abandoned.contains(&entry.table_id) {
2257 entry.state = TableState::Dropped {
2258 at_epoch: cat.db_epoch,
2259 };
2260 }
2261 }
2262 abandoned
2263 } else {
2264 Vec::new()
2265 };
2266 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
2267 let recovery_records = match (existing, recovery_records) {
2268 (true, Some(records)) => records,
2269 (true, None) => {
2270 return Err(MongrelError::Other(
2271 "existing open has no validated WAL recovery plan".into(),
2272 ))
2273 }
2274 (false, _) => Vec::new(),
2275 };
2276 let (history_epochs, history_start) =
2277 read_history_retention(&durable_root, Epoch(cat.db_epoch))?;
2278 let open_generation = if existing {
2279 let checkpoint = recovery_checkpoint.as_ref().ok_or_else(|| {
2280 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
2281 })?;
2282 let recovered_table_ids = cat
2283 .tables
2284 .iter()
2285 .filter(|entry| {
2286 checkpoint
2287 .tables
2288 .iter()
2289 .all(|checkpoint| checkpoint.table_id != entry.table_id)
2290 })
2291 .map(|entry| entry.table_id)
2292 .collect::<HashSet<_>>();
2293 let reconciled_table_ids = cat
2294 .tables
2295 .iter()
2296 .filter(|entry| {
2297 checkpoint
2298 .tables
2299 .iter()
2300 .find(|checkpoint| checkpoint.table_id == entry.table_id)
2301 .is_some_and(|checkpoint| {
2302 crate::wal::DdlOp::encode_schema(&checkpoint.schema).ok()
2303 != crate::wal::DdlOp::encode_schema(&entry.schema).ok()
2304 })
2305 })
2306 .map(|entry| entry.table_id)
2307 .collect::<HashSet<_>>();
2308 validate_shared_wal_recovery_plan(
2309 &durable_root,
2310 &cat,
2311 &recovered_table_ids,
2312 &reconciled_table_ids,
2313 meta_dek.as_ref(),
2314 kek.clone(),
2315 &recovery_records,
2316 )?;
2317 let retained_generation = recovery_records
2318 .iter()
2319 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
2320 .map(|record| record.txn_id >> 32)
2321 .max()
2322 .unwrap_or(0);
2323 let head_generation =
2324 crate::wal::SharedWal::durable_open_generation(&durable_root, wal_dek.as_ref())?;
2325 let durable_floor = match head_generation {
2326 Some(head) if retained_generation > head => {
2327 return Err(MongrelError::CorruptWal {
2328 offset: retained_generation,
2329 reason: format!(
2330 "retained transaction generation {retained_generation} exceeds WAL head generation {head}"
2331 ),
2332 })
2333 }
2334 Some(head) => head,
2335 None => retained_generation,
2336 };
2337 let stored = catalog::read_generation(&durable_root)?;
2338 if stored.is_some_and(|generation| generation < durable_floor) {
2339 return Err(MongrelError::Other(format!(
2340 "open-generation {stored:?} precedes durable WAL generation {durable_floor}"
2341 )));
2342 }
2343 let bumped = stored
2344 .unwrap_or(durable_floor)
2345 .max(durable_floor)
2346 .checked_add(1)
2347 .ok_or_else(|| MongrelError::Full("open-generation namespace exhausted".into()))?;
2348 if bumped > u32::MAX as u64 {
2349 return Err(MongrelError::Full(
2350 "open-generation namespace exhausted".into(),
2351 ));
2352 }
2353 bumped
2354 } else {
2355 0
2356 };
2357 let principal = if cat.require_auth {
2358 let supplied = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
2359 Some(
2360 Self::resolve_bound_principal_from_catalog(&cat, supplied)
2361 .ok_or(MongrelError::AuthRequired)?,
2362 )
2363 } else {
2364 principal
2365 };
2366 let mut table_roots = HashMap::<u64, Arc<crate::durable_file::DurableRoot>>::new();
2367 if existing {
2368 for entry in &cat.tables {
2369 if !matches!(entry.state, TableState::Live) {
2370 continue;
2371 }
2372 match durable_root
2373 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))
2374 {
2375 Ok(root) => {
2376 table_roots.insert(entry.table_id, Arc::new(root));
2377 }
2378 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
2379 Err(error) => return Err(error.into()),
2380 }
2381 }
2382 }
2383
2384 if existing {
2388 let mut applied = recovery_checkpoint.ok_or_else(|| {
2389 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
2390 })?;
2391 recover_ddl_from_records(
2392 &root,
2393 Some(&durable_root),
2394 &mut applied,
2395 meta_dek.as_ref(),
2396 true,
2397 Some(&table_roots),
2398 &recovery_records,
2399 )?;
2400 let catalog_value = |catalog: &Catalog| {
2401 serde_json::to_value(catalog)
2402 .map_err(|error| MongrelError::Other(format!("catalog compare: {error}")))
2403 };
2404 if catalog_value(&applied)? != catalog_value(&recovered_catalog)? {
2405 return Err(MongrelError::CorruptWal {
2406 offset: 0,
2407 reason: "validated and applied DDL recovery plans differ".into(),
2408 });
2409 }
2410 if catalog_value(&cat)? != catalog_value(&applied)? {
2411 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
2412 }
2413 validate_catalog_table_storage(&durable_root, &cat, meta_dek.as_ref())?;
2414 if !read_only {
2415 sweep_unreferenced_table_dirs(&root, &cat)?;
2416 }
2417 match durable_root.remove_directory_all(Path::new(META_DIR).join("backup-pins")) {
2418 Ok(()) => {}
2419 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
2420 Err(error) => return Err(error.into()),
2421 }
2422 }
2423
2424 let epoch = Arc::new(EpochAuthority::new(cat.db_epoch));
2425 let snapshots = Arc::new(SnapshotRegistry::new());
2426 snapshots.configure_history(history_epochs, history_start);
2427 let page_cache = Arc::new(crate::cache::Sharded::new(
2428 crate::cache::CACHE_SHARDS,
2429 || {
2430 crate::cache::PageCache::new(
2431 crate::engine::PAGE_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
2432 )
2433 },
2434 ));
2435 let decoded_cache = Arc::new(crate::cache::Sharded::new(
2436 crate::cache::CACHE_SHARDS,
2437 || {
2438 crate::cache::DecodedPageCache::new(
2439 crate::engine::DECODED_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
2440 )
2441 },
2442 ));
2443 let commit_lock = Arc::new(Mutex::new(()));
2444 let shared_wal = Arc::new(Mutex::new(if existing {
2445 crate::wal::SharedWal::open_durable_root_validated(
2446 Arc::clone(&durable_root),
2447 Epoch(cat.db_epoch),
2448 wal_dek.clone(),
2449 Some(&recovery_records),
2450 )?
2451 } else {
2452 crate::wal::SharedWal::create_with_durable_root(
2453 Arc::clone(&durable_root),
2454 Epoch(cat.db_epoch),
2455 wal_dek.clone(),
2456 )?
2457 }));
2458 let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));
2462 let group = Arc::new(crate::txn::GroupCommit::new(
2463 shared_wal.lock().durable_seq(),
2464 ));
2465 let (change_wake, _change_rx) = tokio::sync::broadcast::channel(256);
2466 let txn_ids = Arc::new(Mutex::new(1u64));
2470 let _ = abandoned;
2471
2472 let auth_state = crate::auth_state::AuthState::new(cat.require_auth, principal.clone());
2477 let security_coordinator = security_coordinator(&root, cat.security_version);
2478 let auth_checker: Option<Arc<dyn crate::auth_state::TableAuthChecker>> = Some(Arc::new(
2479 crate::auth_state::DefaultTableAuthChecker::new(auth_state.clone()),
2480 ));
2481
2482 let mut tables: HashMap<u64, TableHandle> = HashMap::new();
2488 for entry in &cat.tables {
2489 if !matches!(entry.state, TableState::Live) {
2490 continue;
2491 }
2492 let table_root = match table_roots.remove(&entry.table_id) {
2493 Some(root) => root,
2494 None => Arc::new(
2495 durable_root
2496 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))?,
2497 ),
2498 };
2499 let tdir = table_root.io_path()?;
2500 let ctx = SharedCtx {
2501 root_guard: Some(table_root),
2502 epoch: Arc::clone(&epoch),
2503 page_cache: Arc::clone(&page_cache),
2504 decoded_cache: Arc::clone(&decoded_cache),
2505 snapshots: Arc::clone(&snapshots),
2506 kek: kek.clone(),
2507 commit_lock: Arc::clone(&commit_lock),
2508 shared: Some(crate::engine::SharedWalCtx {
2509 wal: Arc::clone(&shared_wal),
2510 group: Arc::clone(&group),
2511 poisoned: Arc::clone(&poisoned),
2512 txn_ids: Arc::clone(&txn_ids),
2513 change_wake: change_wake.clone(),
2514 }),
2515 table_name: Some(entry.name.clone()),
2516 auth: auth_checker.clone(),
2517 read_only,
2518 };
2519 let t = Table::open_in(&tdir, ctx)?;
2520 tables.insert(entry.table_id, TableHandle::new(t));
2521 }
2522
2523 if existing {
2529 recover_shared_wal(&durable_root, &tables, &cat, &epoch, &recovery_records)?;
2530 reconcile_recovered_table_metadata(&tables, epoch.visible())?;
2531 if read_only {
2532 crate::replication::reconcile_replica_epoch_durable(
2533 &durable_root,
2534 epoch.visible().0,
2535 )?;
2536 }
2537 sweep_pending_txn_dirs(&root, &cat);
2540 }
2541
2542 catalog::write_generation(&durable_root, open_generation)?;
2544 shared_wal.lock().seal_open_generation(open_generation)?;
2545 crate::replication::replication_identity_durable(&durable_root)?;
2546 let next_txn_id = (open_generation << 32) | 1;
2547 *txn_ids.lock() = next_txn_id;
2549
2550 Ok(Self {
2551 root,
2552 durable_root,
2553 read_only,
2554 catalog: RwLock::new(cat),
2555 security_coordinator,
2556 security_catalog_disk_reads: AtomicU64::new(0),
2557 rls_cache: Mutex::new(RlsCache::default()),
2558 epoch,
2559 snapshots,
2560 page_cache,
2561 decoded_cache,
2562 commit_lock,
2563 shared_wal,
2564 next_txn_id: txn_ids,
2565 tables: RwLock::new(tables),
2566 kek,
2567 ddl_lock: Mutex::new(()),
2568 meta_dek,
2569 conflicts: crate::txn::ConflictIndex::new(),
2570 active_txns: crate::txn::ActiveTxns::new(),
2571 poisoned,
2572 group,
2573 spill_threshold: std::sync::atomic::AtomicU64::new(64 * 1024 * 1024),
2574 active_spills: Arc::new(crate::retention::ActiveSpills::new()),
2575 replication_barrier: parking_lot::RwLock::new(()),
2576 replication_wal_retention_segments: AtomicUsize::new(0),
2577 backup_pins: Arc::new(Mutex::new(HashMap::new())),
2578 spill_hook: Mutex::new(None),
2579 security_commit_hook: Mutex::new(None),
2580 catalog_commit_hook: Mutex::new(None),
2581 backup_hook: Mutex::new(None),
2582 replication_hook: Mutex::new(None),
2583 trigger_recursive: AtomicBool::new(TriggerConfig::default().recursive_triggers),
2584 trigger_max_depth: AtomicU32::new(TriggerConfig::default().max_depth),
2585 trigger_max_loop_iterations: AtomicU32::new(
2586 TriggerConfig::default().max_loop_iterations,
2587 ),
2588 _lock: Some(lock),
2589 notify: {
2590 let (tx, _rx) = tokio::sync::broadcast::channel(256);
2591 tx
2592 },
2593 change_wake,
2594 principal: RwLock::new(principal),
2595 auth_state,
2596 })
2597 }
2598
2599 pub fn visible_epoch(&self) -> Epoch {
2601 self.epoch.visible()
2602 }
2603
2604 pub fn catalog_snapshot(&self) -> Catalog {
2606 self.catalog.read().clone()
2607 }
2608
2609 pub fn sql_pragma_i64(&self, key: &str) -> Result<Option<i64>> {
2611 let catalog = self.catalog.read();
2612 match key {
2613 "user_version" => Ok(catalog.user_version),
2614 "application_id" => Ok(catalog.application_id),
2615 _ => Err(MongrelError::InvalidArgument(format!(
2616 "unsupported persistent SQL pragma {key:?}"
2617 ))),
2618 }
2619 }
2620
2621 pub fn set_sql_pragma_i64_with_epoch(&self, key: &str, value: i64) -> Result<Option<Epoch>> {
2624 self.set_sql_pragma_i64_with_epoch_inner(key, value, None)
2625 }
2626
2627 pub fn set_sql_pragma_i64_with_epoch_controlled<F>(
2628 &self,
2629 key: &str,
2630 value: i64,
2631 mut before_commit: F,
2632 ) -> Result<Option<Epoch>>
2633 where
2634 F: FnMut() -> Result<()>,
2635 {
2636 self.set_sql_pragma_i64_with_epoch_inner(key, value, Some(&mut before_commit))
2637 }
2638
2639 fn set_sql_pragma_i64_with_epoch_inner(
2640 &self,
2641 key: &str,
2642 value: i64,
2643 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
2644 ) -> Result<Option<Epoch>> {
2645 use crate::wal::DdlOp;
2646
2647 self.require(&crate::auth::Permission::Ddl)?;
2648 if self.read_only {
2649 return Err(MongrelError::ReadOnlyReplica);
2650 }
2651 if self.poisoned.load(Ordering::Relaxed) {
2652 return Err(MongrelError::Other(
2653 "database poisoned by fsync error".into(),
2654 ));
2655 }
2656 let _ddl = self.ddl_lock.lock();
2657 let _security_write = self.security_write()?;
2658 self.require(&crate::auth::Permission::Ddl)?;
2659 let mut next_catalog = self.catalog.read().clone();
2660 let target = match key {
2661 "user_version" => &mut next_catalog.user_version,
2662 "application_id" => &mut next_catalog.application_id,
2663 _ => {
2664 return Err(MongrelError::InvalidArgument(format!(
2665 "unsupported persistent SQL pragma {key:?}"
2666 )))
2667 }
2668 };
2669 if *target == Some(value) {
2670 return Ok(None);
2671 }
2672 *target = Some(value);
2673
2674 let _commit = self.commit_lock.lock();
2675 let epoch = self.epoch.bump_assigned();
2676 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2677 let txn_id = self.alloc_txn_id()?;
2678 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
2679 let commit_seq = {
2680 let mut wal = self.shared_wal.lock();
2681 if let Some(before_commit) = before_commit {
2682 before_commit()?;
2683 }
2684 let append: Result<u64> = (|| {
2685 wal.append(
2686 txn_id,
2687 WAL_TABLE_ID,
2688 crate::wal::Op::Ddl(DdlOp::SetSqlPragma {
2689 key: key.to_string(),
2690 value,
2691 }),
2692 )?;
2693 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
2694 wal.append_commit(txn_id, epoch, &[])
2695 })();
2696 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
2697 };
2698 self.await_durable_commit(commit_seq, epoch)?;
2699 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
2700 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
2701 Ok(Some(epoch))
2702 }
2703
2704 pub fn materialized_view(&self, name: &str) -> Option<crate::catalog::MaterializedViewEntry> {
2705 self.catalog
2706 .read()
2707 .materialized_views
2708 .iter()
2709 .find(|definition| definition.name == name)
2710 .cloned()
2711 }
2712
2713 pub fn materialized_views(&self) -> Vec<crate::catalog::MaterializedViewEntry> {
2714 self.catalog.read().materialized_views.clone()
2715 }
2716
2717 pub fn security_catalog(&self) -> crate::security::SecurityCatalog {
2718 self.catalog.read().security.clone()
2719 }
2720
2721 pub fn security_active_for(&self, table: &str) -> bool {
2722 self.catalog.read().security.table_has_security(table)
2723 }
2724
2725 fn refresh_security_catalog_if_stale(&self, expected_version: u64) -> Result<()> {
2726 if self.catalog.read().security_version == expected_version {
2727 return Ok(());
2728 }
2729 self.security_catalog_disk_reads
2730 .fetch_add(1, Ordering::Relaxed);
2731 let fresh = catalog::read_durable(&self.durable_root, self.meta_dek.as_ref())?
2732 .ok_or_else(|| MongrelError::NotFound("catalog vanished during write".into()))?;
2733 let principal = self.principal.read().clone();
2734 let principal = if fresh.require_auth {
2735 principal
2736 .as_ref()
2737 .and_then(|principal| Self::resolve_bound_principal_from_catalog(&fresh, principal))
2738 } else {
2739 principal
2740 };
2741 self.auth_state.set_require_auth(fresh.require_auth);
2742 *self.catalog.write() = fresh;
2743 *self.principal.write() = principal.clone();
2744 self.auth_state.set_principal(principal);
2745 Ok(())
2746 }
2747
2748 fn security_write(&self) -> Result<parking_lot::RwLockWriteGuard<'_, ()>> {
2749 let guard = self.security_coordinator.gate.write();
2750 let version = self.security_coordinator.version.load(Ordering::Acquire);
2751 self.refresh_security_catalog_if_stale(version)?;
2752 Ok(guard)
2753 }
2754
2755 fn publish_catalog_candidate(
2759 &self,
2760 catalog: Catalog,
2761 epoch: Epoch,
2762 epoch_guard: &mut EpochGuard<'_>,
2763 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
2764 ) -> Result<()> {
2765 self.publish_catalog_candidate_with_prelude(
2766 catalog,
2767 epoch,
2768 epoch_guard,
2769 before_publish,
2770 Vec::new(),
2771 )
2772 }
2773
2774 fn publish_catalog_candidate_with_prelude(
2775 &self,
2776 catalog: Catalog,
2777 epoch: Epoch,
2778 epoch_guard: &mut EpochGuard<'_>,
2779 mut before_publish: Option<&mut dyn FnMut() -> Result<()>>,
2780 prelude: Vec<(u64, crate::wal::Op)>,
2781 ) -> Result<()> {
2782 use crate::wal::DdlOp;
2783
2784 if self.read_only {
2785 return Err(MongrelError::ReadOnlyReplica);
2786 }
2787 if self.poisoned.load(Ordering::Relaxed) {
2788 return Err(MongrelError::Other(
2789 "database poisoned by fsync error".into(),
2790 ));
2791 }
2792 if let Some(before_publish) = before_publish.as_mut() {
2793 (**before_publish)()?;
2794 }
2795 if catalog.db_epoch != epoch.0 {
2796 return Err(MongrelError::InvalidArgument(format!(
2797 "catalog epoch {} does not match commit epoch {}",
2798 catalog.db_epoch, epoch.0
2799 )));
2800 }
2801 {
2802 let current = self.catalog.read();
2803 validate_catalog_transition(¤t, &catalog)?;
2804 }
2805 validate_recovered_catalog(&catalog)?;
2806 let catalog_json = DdlOp::encode_catalog(&catalog)?;
2807 let txn_id = self.alloc_txn_id()?;
2808 let commit_seq = {
2809 let mut wal = self.shared_wal.lock();
2810 let append: Result<u64> = (|| {
2811 for (table_id, op) in prelude {
2812 wal.append(txn_id, table_id, op)?;
2813 }
2814 wal.append(
2815 txn_id,
2816 WAL_TABLE_ID,
2817 crate::wal::Op::Ddl(DdlOp::CatalogSnapshot { catalog_json }),
2818 )?;
2819 wal.append_commit(txn_id, epoch, &[])
2820 })();
2821 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
2822 };
2823 self.await_durable_commit(commit_seq, epoch)?;
2824 let checkpoint = self.checkpoint_catalog_after_durable(catalog);
2825 self.finish_durable_publish(epoch, epoch_guard, checkpoint)
2826 }
2827
2828 fn checkpoint_catalog_after_durable(&self, catalog: Catalog) -> Result<()> {
2832 let checkpoint = catalog::write_atomic(&self.root, &catalog, self.meta_dek.as_ref());
2833 let version = catalog.security_version;
2834 let principal = self.principal.read().clone();
2835 let principal = if catalog.require_auth {
2836 principal.as_ref().and_then(|principal| {
2837 Self::resolve_bound_principal_from_catalog(&catalog, principal)
2838 })
2839 } else {
2840 principal
2841 };
2842 *self.catalog.write() = catalog;
2843 self.security_coordinator
2844 .version
2845 .store(version, Ordering::Release);
2846 self.auth_state
2847 .set_require_auth(self.catalog.read().require_auth);
2848 *self.principal.write() = principal.clone();
2849 self.auth_state.set_principal(principal);
2850 checkpoint
2851 }
2852
2853 fn finish_durable_publish(
2854 &self,
2855 epoch: Epoch,
2856 epoch_guard: &mut EpochGuard<'_>,
2857 post_step: Result<()>,
2858 ) -> Result<()> {
2859 self.epoch.publish_in_order(epoch);
2860 epoch_guard.disarm();
2861 match post_step {
2862 Ok(()) => Ok(()),
2863 Err(error) => {
2864 self.poisoned.store(true, Ordering::Relaxed);
2865 Err(MongrelError::DurableCommit {
2866 epoch: epoch.0,
2867 message: error.to_string(),
2868 })
2869 }
2870 }
2871 }
2872
2873 fn await_durable_commit(&self, commit_seq: u64, epoch: Epoch) -> Result<()> {
2877 match self.group.await_durable(&self.shared_wal, commit_seq) {
2878 Ok(()) => Ok(()),
2879 Err(error) => {
2880 self.poisoned.store(true, Ordering::Relaxed);
2881 Err(MongrelError::CommitOutcomeUnknown {
2882 epoch: epoch.0,
2883 message: error.to_string(),
2884 })
2885 }
2886 }
2887 }
2888
2889 fn commit_outcome_unknown(&self, epoch: Epoch, error: impl std::fmt::Display) -> MongrelError {
2890 self.poisoned.store(true, Ordering::Relaxed);
2891 MongrelError::CommitOutcomeUnknown {
2892 epoch: epoch.0,
2893 message: error.to_string(),
2894 }
2895 }
2896
2897 pub fn set_security_catalog(&self, security: crate::security::SecurityCatalog) -> Result<()> {
2899 self.set_security_catalog_as_with_epoch(security, None)
2900 .map(|_| ())
2901 }
2902
2903 pub fn set_security_catalog_as(
2905 &self,
2906 security: crate::security::SecurityCatalog,
2907 principal: Option<&crate::auth::Principal>,
2908 ) -> Result<()> {
2909 self.set_security_catalog_as_with_epoch(security, principal)
2910 .map(|_| ())
2911 }
2912
2913 pub fn set_security_catalog_as_with_epoch(
2915 &self,
2916 security: crate::security::SecurityCatalog,
2917 principal: Option<&crate::auth::Principal>,
2918 ) -> Result<Epoch> {
2919 self.set_security_catalog_as_with_epoch_inner(security, principal, None)
2920 }
2921
2922 pub fn set_security_catalog_as_with_epoch_controlled<F>(
2925 &self,
2926 security: crate::security::SecurityCatalog,
2927 principal: Option<&crate::auth::Principal>,
2928 mut before_commit: F,
2929 ) -> Result<Epoch>
2930 where
2931 F: FnMut() -> Result<()>,
2932 {
2933 self.set_security_catalog_as_with_epoch_inner(security, principal, Some(&mut before_commit))
2934 }
2935
2936 fn set_security_catalog_as_with_epoch_inner(
2937 &self,
2938 security: crate::security::SecurityCatalog,
2939 principal: Option<&crate::auth::Principal>,
2940 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
2941 ) -> Result<Epoch> {
2942 use crate::wal::DdlOp;
2943 use std::sync::atomic::Ordering;
2944
2945 self.require_for(principal, &crate::auth::Permission::Admin)?;
2946 if self.poisoned.load(Ordering::Relaxed) {
2947 return Err(MongrelError::Other(
2948 "database poisoned by fsync error".into(),
2949 ));
2950 }
2951 let _ddl = self.ddl_lock.lock();
2952 let _security_write = self.security_write()?;
2955 self.require_for(principal, &crate::auth::Permission::Admin)?;
2956 let mut next_catalog = self.catalog.read().clone();
2957 validate_security_catalog(&next_catalog, &security)?;
2958 let payload = DdlOp::encode_security(&security)?;
2959 let _commit = self.commit_lock.lock();
2960 let epoch = self.epoch.bump_assigned();
2961 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2962 let txn_id = self.alloc_txn_id()?;
2963 next_catalog.security = security;
2964 advance_security_version(&mut next_catalog)?;
2965 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
2966 let commit_seq = {
2967 let mut wal = self.shared_wal.lock();
2968 if let Some(before_commit) = before_commit {
2969 before_commit()?;
2970 }
2971 let append: Result<u64> = (|| {
2972 wal.append(
2973 txn_id,
2974 WAL_TABLE_ID,
2975 crate::wal::Op::Ddl(DdlOp::SetSecurityCatalog {
2976 security_json: payload,
2977 }),
2978 )?;
2979 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
2980 wal.append_commit(txn_id, epoch, &[])
2981 })();
2982 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
2983 };
2984 self.await_durable_commit(commit_seq, epoch)?;
2985 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
2986 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
2987 Ok(epoch)
2988 }
2989
2990 pub fn require_for(
2991 &self,
2992 principal: Option<&crate::auth::Principal>,
2993 permission: &crate::auth::Permission,
2994 ) -> Result<()> {
2995 let Some(principal) = principal else {
2996 return self.require(permission);
2997 };
2998 let resolved;
2999 let principal = if self.auth_state.require_auth() || principal.user_id != 0 {
3000 resolved = Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
3001 .ok_or(MongrelError::AuthRequired)?;
3002 &resolved
3003 } else {
3004 principal
3005 };
3006 #[cfg(test)]
3007 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
3008 if principal.has_permission(permission) {
3009 Ok(())
3010 } else {
3011 Err(MongrelError::PermissionDenied {
3012 required: permission.clone(),
3013 principal: principal.username.clone(),
3014 })
3015 }
3016 }
3017
3018 fn require_exact_principal_current(
3022 &self,
3023 principal: Option<&crate::auth::Principal>,
3024 permission: &crate::auth::Permission,
3025 ) -> Result<()> {
3026 let catalog = self.catalog.read();
3027 if !catalog.require_auth {
3028 return Ok(());
3029 }
3030 let supplied = principal.ok_or(MongrelError::AuthRequired)?;
3031 let current = Self::resolve_bound_principal_from_catalog(&catalog, supplied)
3032 .ok_or(MongrelError::AuthRequired)?;
3033 if current.has_permission(permission) {
3034 Ok(())
3035 } else {
3036 Err(MongrelError::PermissionDenied {
3037 required: permission.clone(),
3038 principal: current.username,
3039 })
3040 }
3041 }
3042
3043 pub(crate) fn with_exact_principal_current<T, F>(
3044 &self,
3045 principal: Option<&crate::auth::Principal>,
3046 permission: &crate::auth::Permission,
3047 operation: F,
3048 ) -> Result<T>
3049 where
3050 F: FnOnce() -> Result<T>,
3051 {
3052 let _security = self.security_coordinator.gate.read();
3053 self.require_exact_principal_current(principal, permission)?;
3054 operation()
3055 }
3056
3057 pub fn principal_snapshot(&self) -> Option<crate::auth::Principal> {
3058 self.principal.read().clone()
3059 }
3060
3061 #[cfg(test)]
3062 pub(crate) fn set_cached_principal_for_test(&self, principal: Option<crate::auth::Principal>) {
3063 *self.principal.write() = principal.clone();
3064 self.auth_state.set_principal(principal);
3065 }
3066
3067 pub fn require_columns_for(
3068 &self,
3069 table: &str,
3070 operation: crate::auth::ColumnOperation,
3071 column_ids: &[u16],
3072 principal: Option<&crate::auth::Principal>,
3073 ) -> Result<()> {
3074 if principal.is_none() && !self.auth_state.require_auth() {
3075 return Ok(());
3076 }
3077 let cached = self.principal.read().clone();
3078 let principal = principal.or(cached.as_ref());
3079 let Some(principal) = principal else {
3080 let permission = match operation {
3081 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
3082 table: table.to_string(),
3083 },
3084 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
3085 table: table.to_string(),
3086 },
3087 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
3088 table: table.to_string(),
3089 },
3090 };
3091 return self.require(&permission);
3092 };
3093 let catalog = self.catalog.read();
3094 let resolved;
3095 let principal = if catalog.require_auth || principal.user_id != 0 {
3096 resolved = Self::resolve_bound_principal_from_catalog(&catalog, principal)
3097 .ok_or(MongrelError::AuthRequired)?;
3098 &resolved
3099 } else {
3100 principal
3101 };
3102 let schema = &catalog
3103 .live(table)
3104 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
3105 .schema;
3106 Self::require_columns_for_principal(table, schema, operation, column_ids, principal)
3107 }
3108
3109 fn require_columns_for_principal(
3110 table: &str,
3111 schema: &Schema,
3112 operation: crate::auth::ColumnOperation,
3113 column_ids: &[u16],
3114 principal: &crate::auth::Principal,
3115 ) -> Result<()> {
3116 #[cfg(test)]
3117 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
3118 match principal.column_access(table, operation) {
3119 crate::auth::ColumnAccess::All => Ok(()),
3120 crate::auth::ColumnAccess::Columns(allowed) => {
3121 let denied = column_ids.iter().find_map(|column_id| {
3122 schema
3123 .columns
3124 .iter()
3125 .find(|column| column.id == *column_id)
3126 .filter(|column| !allowed.contains(&column.name))
3127 });
3128 if denied.is_none() {
3129 Ok(())
3130 } else {
3131 Err(MongrelError::PermissionDenied {
3132 required: match operation {
3133 crate::auth::ColumnOperation::Select => {
3134 crate::auth::Permission::SelectColumns {
3135 table: table.to_string(),
3136 columns: denied
3137 .into_iter()
3138 .map(|column| column.name.clone())
3139 .collect(),
3140 }
3141 }
3142 crate::auth::ColumnOperation::Insert => {
3143 crate::auth::Permission::InsertColumns {
3144 table: table.to_string(),
3145 columns: denied
3146 .into_iter()
3147 .map(|column| column.name.clone())
3148 .collect(),
3149 }
3150 }
3151 crate::auth::ColumnOperation::Update => {
3152 crate::auth::Permission::UpdateColumns {
3153 table: table.to_string(),
3154 columns: denied
3155 .into_iter()
3156 .map(|column| column.name.clone())
3157 .collect(),
3158 }
3159 }
3160 },
3161 principal: principal.username.clone(),
3162 })
3163 }
3164 }
3165 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
3166 required: match operation {
3167 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
3168 table: table.to_string(),
3169 },
3170 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
3171 table: table.to_string(),
3172 },
3173 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
3174 table: table.to_string(),
3175 },
3176 },
3177 principal: principal.username.clone(),
3178 }),
3179 }
3180 }
3181
3182 pub fn select_column_ids_for(
3183 &self,
3184 table: &str,
3185 principal: Option<&crate::auth::Principal>,
3186 ) -> Result<Vec<u16>> {
3187 let catalog = self.catalog.read();
3188 let columns = catalog
3189 .live(table)
3190 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
3191 .schema
3192 .columns
3193 .iter()
3194 .map(|column| (column.id, column.name.clone()))
3195 .collect::<Vec<_>>();
3196 let principal = self.principal_for_authorized_read(&catalog, principal, false)?;
3197 drop(catalog);
3198 let Some(principal) = principal.as_ref() else {
3199 self.require(&crate::auth::Permission::Select {
3200 table: table.to_string(),
3201 })?;
3202 return Ok(columns.iter().map(|(id, _)| *id).collect());
3203 };
3204 match principal.column_access(table, crate::auth::ColumnOperation::Select) {
3205 crate::auth::ColumnAccess::All => Ok(columns.iter().map(|(id, _)| *id).collect()),
3206 crate::auth::ColumnAccess::Columns(allowed) => Ok(columns
3207 .iter()
3208 .filter(|(_, name)| allowed.contains(name))
3209 .map(|(id, _)| *id)
3210 .collect()),
3211 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
3212 required: crate::auth::Permission::Select {
3213 table: table.to_string(),
3214 },
3215 principal: principal.username.clone(),
3216 }),
3217 }
3218 }
3219
3220 pub fn secure_rows_for(
3221 &self,
3222 table: &str,
3223 rows: Vec<crate::memtable::Row>,
3224 principal: Option<&crate::auth::Principal>,
3225 ) -> Result<Vec<crate::memtable::Row>> {
3226 self.secure_rows_for_with_context(table, rows, principal, None)
3227 }
3228
3229 pub fn secure_rows_for_with_context(
3230 &self,
3231 table: &str,
3232 rows: Vec<crate::memtable::Row>,
3233 principal: Option<&crate::auth::Principal>,
3234 context: Option<&crate::query::AiExecutionContext>,
3235 ) -> Result<Vec<crate::memtable::Row>> {
3236 let (security, principal) = {
3237 let catalog = self.catalog.read();
3238 (
3239 catalog.security.clone(),
3240 self.principal_for_authorized_read(&catalog, principal, false)?,
3241 )
3242 };
3243 if !security.table_has_security(table) {
3244 return Ok(rows);
3245 }
3246 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
3247 let mut output = Vec::new();
3248 for mut row in rows {
3249 if let Some(context) = context {
3250 context.consume(1)?;
3251 }
3252 if security.row_allowed(
3253 table,
3254 crate::security::PolicyCommand::Select,
3255 &row,
3256 principal,
3257 false,
3258 ) {
3259 security.apply_masks(table, &mut row, principal);
3260 output.push(row);
3261 }
3262 }
3263 Ok(output)
3264 }
3265
3266 pub fn mask_search_hits_for(
3269 &self,
3270 table: &str,
3271 hits: &mut [crate::query::SearchHit],
3272 principal: Option<&crate::auth::Principal>,
3273 ) -> Result<()> {
3274 let (security, principal) = {
3275 let catalog = self.catalog.read();
3276 (
3277 catalog.security.clone(),
3278 self.principal_for_authorized_read(&catalog, principal, false)?,
3279 )
3280 };
3281 if !security.table_has_security(table) {
3282 return Ok(());
3283 }
3284 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
3285 for hit in hits {
3286 security.apply_masks_to_cells(table, &mut hit.cells, principal);
3287 }
3288 Ok(())
3289 }
3290
3291 pub fn mask_rows_for(
3293 &self,
3294 table: &str,
3295 rows: &mut [crate::memtable::Row],
3296 principal: Option<&crate::auth::Principal>,
3297 ) -> Result<()> {
3298 let (security, principal) = {
3299 let catalog = self.catalog.read();
3300 (
3301 catalog.security.clone(),
3302 self.principal_for_authorized_read(&catalog, principal, false)?,
3303 )
3304 };
3305 if !security.table_has_security(table) {
3306 return Ok(());
3307 }
3308 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
3309 for row in rows {
3310 security.apply_masks(table, row, principal);
3311 }
3312 Ok(())
3313 }
3314
3315 pub fn authorized_candidate_ids_for(
3317 &self,
3318 table: &str,
3319 principal: Option<&crate::auth::Principal>,
3320 ) -> Result<Option<std::collections::HashSet<RowId>>> {
3321 Ok(self
3322 .authorized_read_snapshot(table, principal)?
3323 .allowed_row_ids)
3324 }
3325
3326 fn allowed_row_ids_locked(
3327 &self,
3328 table_name: &str,
3329 table: &Table,
3330 table_snapshot: Snapshot,
3331 security_state: (&crate::security::SecurityCatalog, u64),
3332 principal: Option<&crate::auth::Principal>,
3333 context: Option<&crate::query::AiExecutionContext>,
3334 ) -> Result<Option<Arc<HashSet<RowId>>>> {
3335 let (security, security_version) = security_state;
3336 if !security.rls_enabled(table_name) {
3337 return Ok(None);
3338 }
3339 let authorization_started = std::time::Instant::now();
3340 let principal = principal.ok_or(MongrelError::AuthRequired)?;
3341 let mut roles = principal.roles.clone();
3342 roles.sort_unstable();
3343 let principal_key = format!(
3344 "{}:{}:{}:{}:{roles:?}",
3345 principal.user_id, principal.created_epoch, principal.username, principal.is_admin
3346 );
3347 let cache_key = (
3348 table_name.to_string(),
3349 table.data_generation(),
3350 security_version,
3351 principal_key,
3352 );
3353 if let Some(allowed) = self.rls_cache.lock().get(&cache_key) {
3354 crate::trace::QueryTrace::record(|trace| {
3355 trace.rls_cache_hit = true;
3356 trace.authorization_nanos = trace
3357 .authorization_nanos
3358 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
3359 });
3360 return Ok(Some(allowed));
3361 }
3362 if let Some(context) = context {
3363 context.checkpoint()?;
3364 }
3365 let started = std::time::Instant::now();
3367 let rows = table.visible_rows(table_snapshot)?;
3368 let rows_evaluated = rows.len() as u64;
3369 let mut allowed = HashSet::new();
3370 for chunk in rows.chunks(256) {
3371 if let Some(context) = context {
3372 context.consume(chunk.len())?;
3373 }
3374 allowed.extend(chunk.iter().filter_map(|row| {
3375 security
3376 .row_allowed(
3377 table_name,
3378 crate::security::PolicyCommand::Select,
3379 row,
3380 principal,
3381 false,
3382 )
3383 .then_some(row.row_id)
3384 }));
3385 }
3386 let allowed = Arc::new(allowed);
3387 let mut cache = self.rls_cache.lock();
3388 cache.build_nanos = cache
3389 .build_nanos
3390 .saturating_add(started.elapsed().as_nanos() as u64);
3391 cache.rows_evaluated = cache.rows_evaluated.saturating_add(rows_evaluated);
3392 cache.insert(cache_key, Arc::clone(&allowed));
3393 crate::trace::QueryTrace::record(|trace| {
3394 trace.rls_rows_evaluated = trace
3395 .rls_rows_evaluated
3396 .saturating_add(rows_evaluated as usize);
3397 trace.authorization_nanos = trace
3398 .authorization_nanos
3399 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
3400 });
3401 Ok(Some(allowed))
3402 }
3403
3404 fn principal_for_authorized_read(
3405 &self,
3406 catalog: &Catalog,
3407 principal: Option<&crate::auth::Principal>,
3408 catalog_bound: bool,
3409 ) -> Result<Option<crate::auth::Principal>> {
3410 let principal = principal.cloned().or_else(|| self.principal.read().clone());
3411 let Some(principal) = principal else {
3412 return Ok(None);
3413 };
3414 if catalog.require_auth || catalog_bound || principal.user_id != 0 {
3415 return Self::resolve_bound_principal_from_catalog(catalog, &principal)
3416 .map(Some)
3417 .ok_or(MongrelError::AuthRequired);
3418 }
3419 Ok(Some(principal))
3420 }
3421
3422 pub fn with_authorized_read<T, F>(
3426 &self,
3427 table_name: &str,
3428 principal: Option<&crate::auth::Principal>,
3429 catalog_bound: bool,
3430 read: F,
3431 ) -> Result<T>
3432 where
3433 F: FnMut(
3434 &mut Table,
3435 Snapshot,
3436 Option<&HashSet<RowId>>,
3437 Option<&crate::auth::Principal>,
3438 ) -> Result<T>,
3439 {
3440 self.with_authorized_read_context(
3441 table_name,
3442 principal,
3443 catalog_bound,
3444 None,
3445 None,
3446 None,
3447 read,
3448 )
3449 }
3450
3451 #[allow(clippy::too_many_arguments)]
3452 pub fn with_authorized_read_context<T, F>(
3453 &self,
3454 table_name: &str,
3455 principal: Option<&crate::auth::Principal>,
3456 catalog_bound: bool,
3457 authorization: Option<&ReadAuthorization>,
3458 context: Option<&crate::query::AiExecutionContext>,
3459 snapshot_override: Option<Snapshot>,
3460 read: F,
3461 ) -> Result<T>
3462 where
3463 F: FnMut(
3464 &mut Table,
3465 Snapshot,
3466 Option<&HashSet<RowId>>,
3467 Option<&crate::auth::Principal>,
3468 ) -> Result<T>,
3469 {
3470 self.with_authorized_read_context_stamped(
3471 table_name,
3472 principal,
3473 catalog_bound,
3474 authorization,
3475 context,
3476 snapshot_override,
3477 read,
3478 )
3479 .map(|(result, _)| result)
3480 }
3481
3482 #[allow(clippy::too_many_arguments)]
3483 pub fn with_authorized_read_context_stamped<T, F>(
3484 &self,
3485 table_name: &str,
3486 principal: Option<&crate::auth::Principal>,
3487 catalog_bound: bool,
3488 authorization: Option<&ReadAuthorization>,
3489 context: Option<&crate::query::AiExecutionContext>,
3490 snapshot_override: Option<Snapshot>,
3491 mut read: F,
3492 ) -> Result<(T, AuthorizedReadStamp)>
3493 where
3494 F: FnMut(
3495 &mut Table,
3496 Snapshot,
3497 Option<&HashSet<RowId>>,
3498 Option<&crate::auth::Principal>,
3499 ) -> Result<T>,
3500 {
3501 if principal.is_none() && self.principal.read().is_some() {
3502 self.refresh_principal()?;
3503 }
3504 const RETRIES: usize = 3;
3505 let handle = self.table(table_name)?;
3506 for attempt in 0..RETRIES {
3507 crate::trace::QueryTrace::record(|trace| {
3508 trace.authorization_retries = attempt;
3509 });
3510 let (security, security_version, effective_principal) = {
3511 let catalog = self.catalog.read();
3512 (
3513 catalog.security.clone(),
3514 catalog.security_version,
3515 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
3516 )
3517 };
3518 if let Some(authorization) = authorization {
3519 for permission in &authorization.permissions {
3520 self.require_for(effective_principal.as_ref(), permission)?;
3521 }
3522 self.require_columns_for(
3523 table_name,
3524 authorization.operation,
3525 &authorization.columns,
3526 effective_principal.as_ref(),
3527 )?;
3528 }
3529 let result = {
3530 let mut table = lock_table_with_context(&handle, context)?;
3531 let snapshot = snapshot_override.unwrap_or_else(|| table.snapshot());
3532 let allowed = self.allowed_row_ids_locked(
3533 table_name,
3534 &table,
3535 snapshot,
3536 (&security, security_version),
3537 effective_principal.as_ref(),
3538 context,
3539 )?;
3540 let stamp = AuthorizedReadStamp {
3541 table_id: table.table_id(),
3542 schema_id: table.schema().schema_id,
3543 data_generation: table.data_generation(),
3544 security_version,
3545 snapshot,
3546 };
3547 let result = read(
3548 &mut table,
3549 snapshot,
3550 allowed.as_deref(),
3551 effective_principal.as_ref(),
3552 )?;
3553 (result, stamp)
3554 };
3555 if let Some(context) = context {
3556 context.checkpoint()?;
3557 }
3558 if self.catalog.read().security_version == security_version {
3559 return Ok(result);
3560 }
3561 if attempt + 1 == RETRIES {
3562 return Err(MongrelError::Conflict(
3563 "security policy changed during scored read".into(),
3564 ));
3565 }
3566 }
3567 Err(MongrelError::Conflict(
3568 "authorization retry loop exhausted".into(),
3569 ))
3570 }
3571
3572 fn with_authorized_aggregate_table<T, F>(
3573 &self,
3574 table_name: &str,
3575 columns: &[u16],
3576 principal: Option<&crate::auth::Principal>,
3577 catalog_bound: bool,
3578 allow_table_security: bool,
3579 mut aggregate: F,
3580 ) -> Result<T>
3581 where
3582 F: FnMut(
3583 &mut Table,
3584 Option<&crate::security::CandidateAuthorization<'_>>,
3585 Option<&crate::auth::Principal>,
3586 u64,
3587 ) -> Result<T>,
3588 {
3589 if principal.is_none() && self.principal.read().is_some() {
3590 self.refresh_principal()?;
3591 }
3592 const RETRIES: usize = 3;
3593 let handle = self.table(table_name)?;
3594 for attempt in 0..RETRIES {
3595 let (security, security_version, effective_principal) = {
3596 let catalog = self.catalog.read();
3597 (
3598 catalog.security.clone(),
3599 catalog.security_version,
3600 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
3601 )
3602 };
3603 self.require_columns_for(
3604 table_name,
3605 crate::auth::ColumnOperation::Select,
3606 columns,
3607 effective_principal.as_ref(),
3608 )?;
3609 if !allow_table_security && security.table_has_security(table_name) {
3610 return Err(MongrelError::InvalidArgument(
3611 "incremental aggregate is unsupported while RLS or column masks are active"
3612 .into(),
3613 ));
3614 }
3615 let result = {
3616 let mut table = handle.lock();
3617 let authorization = if security.rls_enabled(table_name) {
3618 Some(crate::security::CandidateAuthorization {
3619 table: table_name,
3620 security: &security,
3621 principal: effective_principal
3622 .as_ref()
3623 .ok_or(MongrelError::AuthRequired)?,
3624 })
3625 } else {
3626 None
3627 };
3628 aggregate(
3629 &mut table,
3630 authorization.as_ref(),
3631 effective_principal.as_ref(),
3632 security_version,
3633 )?
3634 };
3635 if self.catalog.read().security_version == security_version {
3636 return Ok(result);
3637 }
3638 if attempt + 1 == RETRIES {
3639 return Err(MongrelError::Conflict(
3640 "security policy changed during aggregate read".into(),
3641 ));
3642 }
3643 }
3644 Err(MongrelError::Conflict(
3645 "aggregate authorization retry loop exhausted".into(),
3646 ))
3647 }
3648
3649 pub fn with_authorized_scored_read_context<T, F>(
3653 &self,
3654 table_name: &str,
3655 principal: Option<&crate::auth::Principal>,
3656 catalog_bound: bool,
3657 authorization: Option<&ReadAuthorization>,
3658 context: Option<&crate::query::AiExecutionContext>,
3659 mut read: F,
3660 ) -> Result<T>
3661 where
3662 F: FnMut(
3663 &mut Table,
3664 Snapshot,
3665 Option<&crate::security::CandidateAuthorization<'_>>,
3666 Option<&crate::auth::Principal>,
3667 ) -> Result<T>,
3668 {
3669 self.with_authorized_scored_read_context_at(
3670 table_name,
3671 principal,
3672 catalog_bound,
3673 authorization,
3674 context,
3675 None,
3676 |table, snapshot, authorization, principal| {
3677 let mut table = table.clone();
3678 read(&mut table, snapshot, authorization, principal)
3679 },
3680 )
3681 }
3682
3683 #[allow(clippy::too_many_arguments)]
3684 pub fn with_authorized_scored_read_context_at<T, F>(
3685 &self,
3686 table_name: &str,
3687 principal: Option<&crate::auth::Principal>,
3688 catalog_bound: bool,
3689 authorization: Option<&ReadAuthorization>,
3690 context: Option<&crate::query::AiExecutionContext>,
3691 snapshot_override: Option<Snapshot>,
3692 read: F,
3693 ) -> Result<T>
3694 where
3695 F: FnMut(
3696 &Table,
3697 Snapshot,
3698 Option<&crate::security::CandidateAuthorization<'_>>,
3699 Option<&crate::auth::Principal>,
3700 ) -> Result<T>,
3701 {
3702 self.with_authorized_scored_read_context_at_stamped(
3703 table_name,
3704 principal,
3705 catalog_bound,
3706 authorization,
3707 context,
3708 snapshot_override,
3709 read,
3710 )
3711 .map(|(result, _)| result)
3712 }
3713
3714 #[allow(clippy::too_many_arguments)]
3715 pub fn with_authorized_scored_read_context_at_stamped<T, F>(
3716 &self,
3717 table_name: &str,
3718 principal: Option<&crate::auth::Principal>,
3719 catalog_bound: bool,
3720 authorization: Option<&ReadAuthorization>,
3721 context: Option<&crate::query::AiExecutionContext>,
3722 snapshot_override: Option<Snapshot>,
3723 mut read: F,
3724 ) -> Result<(T, AuthorizedReadStamp)>
3725 where
3726 F: FnMut(
3727 &Table,
3728 Snapshot,
3729 Option<&crate::security::CandidateAuthorization<'_>>,
3730 Option<&crate::auth::Principal>,
3731 ) -> Result<T>,
3732 {
3733 if principal.is_none() && self.principal.read().is_some() {
3734 self.refresh_principal()?;
3735 }
3736 const RETRIES: usize = 3;
3737 let handle = self.table(table_name)?;
3738 for attempt in 0..RETRIES {
3739 if let Some(context) = context {
3740 context.checkpoint()?;
3741 }
3742 crate::trace::QueryTrace::record(|trace| {
3743 trace.authorization_retries = attempt;
3744 });
3745 let (security, security_version, effective_principal) = {
3746 let catalog = self.catalog.read();
3747 (
3748 catalog.security.clone(),
3749 catalog.security_version,
3750 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
3751 )
3752 };
3753 if let Some(authorization) = authorization {
3754 for permission in &authorization.permissions {
3755 self.require_for(effective_principal.as_ref(), permission)?;
3756 }
3757 self.require_columns_for(
3758 table_name,
3759 authorization.operation,
3760 &authorization.columns,
3761 effective_principal.as_ref(),
3762 )?;
3763 }
3764 let result = {
3765 let (table, snapshot, _snapshot_guard, _run_pins) =
3766 self.scored_read_generation(&handle, context, snapshot_override)?;
3767 let candidate_authorization = if security.rls_enabled(table_name) {
3768 Some(crate::security::CandidateAuthorization {
3769 table: table_name,
3770 security: &security,
3771 principal: effective_principal
3772 .as_ref()
3773 .ok_or(MongrelError::AuthRequired)?,
3774 })
3775 } else {
3776 None
3777 };
3778 let stamp = AuthorizedReadStamp {
3779 table_id: table.table_id(),
3780 schema_id: table.schema().schema_id,
3781 data_generation: table.data_generation(),
3782 security_version,
3783 snapshot,
3784 };
3785 let result = read(
3786 table.as_ref(),
3787 snapshot,
3788 candidate_authorization.as_ref(),
3789 effective_principal.as_ref(),
3790 )?;
3791 (result, stamp)
3792 };
3793 if let Some(context) = context {
3794 context.checkpoint()?;
3795 }
3796 if self.catalog.read().security_version == security_version {
3797 return Ok(result);
3798 }
3799 if attempt + 1 == RETRIES {
3800 return Err(MongrelError::Conflict(
3801 "security policy changed during scored read".into(),
3802 ));
3803 }
3804 }
3805 Err(MongrelError::Conflict(
3806 "scored-read authorization retry loop exhausted".into(),
3807 ))
3808 }
3809
3810 fn scored_read_generation(
3811 &self,
3812 handle: &TableHandle,
3813 context: Option<&crate::query::AiExecutionContext>,
3814 snapshot_override: Option<Snapshot>,
3815 ) -> Result<(
3816 Arc<TableReadGeneration>,
3817 Snapshot,
3818 crate::retention::OwnedSnapshotGuard,
3819 RunPins,
3820 )> {
3821 let mut table = if let Some(context) = context {
3822 loop {
3823 context.checkpoint()?;
3824 let wait = context
3825 .remaining_duration()
3826 .unwrap_or(std::time::Duration::from_millis(5))
3827 .min(std::time::Duration::from_millis(5));
3828 if let Some(table) = handle.try_lock_for(wait) {
3829 break table;
3830 }
3831 }
3832 } else {
3833 handle.lock()
3834 };
3835 let (snapshot, snapshot_guard) = if let Some(snapshot) = snapshot_override {
3836 self.snapshot_at_owned(snapshot.epoch)?
3837 } else {
3838 let snapshot = table.snapshot();
3839 let guard = self.snapshots.register_owned(snapshot.epoch);
3840 (snapshot, guard)
3841 };
3842 let table_id = table.table_id();
3843 let run_keys: Vec<_> = table
3844 .active_run_ids()
3845 .map(|run_id| (table_id, run_id))
3846 .collect();
3847 let generation = handle
3848 .generation_metrics
3849 .activate(table.clone_read_generation()?);
3850 let run_pins = self.pin_runs(&run_keys);
3851 Ok((generation, snapshot, snapshot_guard, run_pins))
3852 }
3853
3854 fn pin_runs(&self, runs: &[(u64, u128)]) -> RunPins {
3855 let mut pins = self.backup_pins.lock();
3856 for run in runs {
3857 *pins.entry(*run).or_insert(0) += 1;
3858 }
3859 drop(pins);
3860 RunPins {
3861 pins: Arc::clone(&self.backup_pins),
3862 runs: runs.to_vec(),
3863 }
3864 }
3865
3866 pub fn query_for_current_principal(
3870 &self,
3871 table_name: &str,
3872 query: &crate::query::Query,
3873 projection: Option<&[u16]>,
3874 ) -> Result<Vec<crate::memtable::Row>> {
3875 let condition_columns = crate::query::condition_columns(&query.conditions);
3876 self.with_authorized_read(
3877 table_name,
3878 None,
3879 true,
3880 |table, snapshot, allowed, principal| {
3881 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
3882 self.require_columns_for(
3883 table_name,
3884 crate::auth::ColumnOperation::Select,
3885 &condition_columns,
3886 principal,
3887 )?;
3888 if let Some(projection) = projection {
3889 self.require_columns_for(
3890 table_name,
3891 crate::auth::ColumnOperation::Select,
3892 projection,
3893 principal,
3894 )?;
3895 }
3896 let mut rows = table.query_at_with_allowed(query, snapshot, allowed)?;
3897 let projection =
3898 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
3899 for row in &mut rows {
3900 row.columns.retain(|column, _| {
3901 allowed_columns.contains(column)
3902 && projection
3903 .as_ref()
3904 .is_none_or(|projection| projection.contains(column))
3905 });
3906 }
3907 self.secure_rows_for(table_name, rows, principal)
3908 },
3909 )
3910 }
3911
3912 pub fn query_for_current_principal_controlled(
3916 &self,
3917 table_name: &str,
3918 query: &crate::query::Query,
3919 projection: Option<&[u16]>,
3920 control: &crate::ExecutionControl,
3921 ) -> Result<Vec<crate::memtable::Row>> {
3922 self.query_for_principal_controlled(table_name, query, projection, None, true, control)
3923 }
3924
3925 fn query_for_principal_controlled(
3926 &self,
3927 table_name: &str,
3928 query: &crate::query::Query,
3929 projection: Option<&[u16]>,
3930 principal: Option<&crate::auth::Principal>,
3931 catalog_bound: bool,
3932 control: &crate::ExecutionControl,
3933 ) -> Result<Vec<crate::memtable::Row>> {
3934 control.checkpoint()?;
3935 let context = crate::query::AiExecutionContext::with_control(
3936 control.clone(),
3937 usize::MAX,
3938 crate::query::MAX_FUSED_CANDIDATES,
3939 );
3940 let condition_columns = crate::query::condition_columns(&query.conditions);
3941 self.with_authorized_read_context(
3942 table_name,
3943 principal,
3944 catalog_bound,
3945 None,
3946 Some(&context),
3947 None,
3948 |table, snapshot, allowed, principal| {
3949 control.checkpoint()?;
3950 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
3951 self.require_columns_for(
3952 table_name,
3953 crate::auth::ColumnOperation::Select,
3954 &condition_columns,
3955 principal,
3956 )?;
3957 if let Some(projection) = projection {
3958 self.require_columns_for(
3959 table_name,
3960 crate::auth::ColumnOperation::Select,
3961 projection,
3962 principal,
3963 )?;
3964 }
3965 let rows =
3966 table.query_at_with_allowed_controlled(query, snapshot, allowed, control)?;
3967 let projection =
3968 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
3969 let mut projected = Vec::with_capacity(rows.len());
3970 for (index, mut row) in rows.into_iter().enumerate() {
3971 if index & 255 == 0 {
3972 control.checkpoint()?;
3973 }
3974 row.columns.retain(|column, _| {
3975 allowed_columns.contains(column)
3976 && projection
3977 .as_ref()
3978 .is_none_or(|projection| projection.contains(column))
3979 });
3980 projected.push(row);
3981 }
3982 self.secure_rows_for_with_context(table_name, projected, principal, Some(&context))
3983 },
3984 )
3985 }
3986
3987 pub fn approx_aggregate_for_current_principal(
3990 &self,
3991 table_name: &str,
3992 conditions: &[crate::query::Condition],
3993 column: Option<u16>,
3994 agg: crate::engine::ApproxAgg,
3995 z: f64,
3996 ) -> Result<Option<crate::engine::ApproxResult>> {
3997 if !z.is_finite() || z <= 0.0 {
3998 return Err(MongrelError::InvalidArgument(
3999 "z must be finite and > 0".into(),
4000 ));
4001 }
4002 let mut columns = crate::query::condition_columns(conditions);
4003 columns.extend(column);
4004 columns.sort_unstable();
4005 columns.dedup();
4006 self.with_authorized_aggregate_table(
4007 table_name,
4008 &columns,
4009 None,
4010 true,
4011 true,
4012 |table, authorization, _, _| {
4013 table.approx_aggregate_with_candidate_authorization(
4014 conditions,
4015 column,
4016 agg,
4017 z,
4018 authorization,
4019 )
4020 },
4021 )
4022 }
4023
4024 pub fn incremental_aggregate_for_current_principal(
4028 &self,
4029 table_name: &str,
4030 conditions: &[crate::query::Condition],
4031 column: Option<u16>,
4032 agg: crate::engine::NativeAgg,
4033 ) -> Result<crate::engine::IncrementalAggResult> {
4034 self.incremental_aggregate_for_principal(table_name, conditions, column, agg, None, true)
4035 }
4036
4037 pub fn incremental_aggregate_for_principal(
4041 &self,
4042 table_name: &str,
4043 conditions: &[crate::query::Condition],
4044 column: Option<u16>,
4045 agg: crate::engine::NativeAgg,
4046 principal: Option<&crate::auth::Principal>,
4047 catalog_bound: bool,
4048 ) -> Result<crate::engine::IncrementalAggResult> {
4049 let mut columns = crate::query::condition_columns(conditions);
4050 columns.extend(column);
4051 columns.sort_unstable();
4052 columns.dedup();
4053 self.with_authorized_aggregate_table(
4054 table_name,
4055 &columns,
4056 principal,
4057 catalog_bound,
4058 false,
4059 |table, _, principal, security_version| {
4060 let cache_key = incremental_aggregate_cache_key(
4061 table_name,
4062 conditions,
4063 column,
4064 agg,
4065 principal,
4066 security_version,
4067 );
4068 table.aggregate_incremental(cache_key, conditions, column, agg)
4069 },
4070 )
4071 }
4072
4073 pub fn get_for_current_principal(
4076 &self,
4077 table_name: &str,
4078 row_id: RowId,
4079 ) -> Result<Option<crate::memtable::Row>> {
4080 self.with_authorized_read(
4081 table_name,
4082 None,
4083 true,
4084 |table, snapshot, allowed, principal| {
4085 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
4086 let Some(row) = table.get(row_id, snapshot) else {
4087 return Ok(None);
4088 };
4089 if allowed.is_some_and(|allowed| !allowed.contains(&row.row_id)) {
4090 return Ok(None);
4091 }
4092 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
4093 if let Some(row) = rows.first_mut() {
4094 row.columns
4095 .retain(|column, _| allowed_columns.contains(column));
4096 }
4097 Ok(rows.pop())
4098 },
4099 )
4100 }
4101
4102 pub fn ann_rerank_for_current_principal(
4105 &self,
4106 table_name: &str,
4107 request: &crate::query::AnnRerankRequest,
4108 ) -> Result<Vec<crate::query::AnnRerankHit>> {
4109 self.with_authorized_scored_read_context_at(
4110 table_name,
4111 None,
4112 true,
4113 Some(&ReadAuthorization {
4114 operation: crate::auth::ColumnOperation::Select,
4115 columns: vec![request.column_id],
4116 permissions: Vec::new(),
4117 }),
4118 None,
4119 None,
4120 |table, snapshot, authorization, principal| {
4121 self.require_columns_for(
4122 table_name,
4123 crate::auth::ColumnOperation::Select,
4124 &[request.column_id],
4125 principal,
4126 )?;
4127 table.ann_rerank_at_with_candidate_authorization_on_generation(
4128 request,
4129 snapshot,
4130 authorization,
4131 None,
4132 )
4133 },
4134 )
4135 }
4136
4137 pub fn authorized_read_snapshot(
4140 &self,
4141 table: &str,
4142 principal: Option<&crate::auth::Principal>,
4143 ) -> Result<AuthorizedReadSnapshot> {
4144 let (security, security_version, effective_principal) = {
4145 let catalog = self.catalog.read();
4146 (
4147 catalog.security.clone(),
4148 catalog.security_version,
4149 self.principal_for_authorized_read(&catalog, principal, false)?,
4150 )
4151 };
4152 let handle = self.table(table)?;
4153 let (table_snapshot, data_generation, allowed_row_ids) = {
4154 let table_handle = handle.lock();
4155 let table_snapshot = table_handle.snapshot();
4156 let data_generation = table_handle.data_generation();
4157 let allowed = self.allowed_row_ids_locked(
4158 table,
4159 &table_handle,
4160 table_snapshot,
4161 (&security, security_version),
4162 effective_principal.as_ref(),
4163 None,
4164 )?;
4165 (
4166 table_snapshot,
4167 data_generation,
4168 allowed.map(|allowed| (*allowed).clone()),
4169 )
4170 };
4171 Ok(AuthorizedReadSnapshot {
4172 table: table.to_string(),
4173 table_snapshot,
4174 data_generation,
4175 security_version,
4176 allowed_row_ids,
4177 })
4178 }
4179
4180 pub fn authorized_read_snapshot_valid(&self, snapshot: &AuthorizedReadSnapshot) -> bool {
4181 if self.catalog.read().security_version != snapshot.security_version {
4182 return false;
4183 }
4184 self.table(&snapshot.table)
4185 .ok()
4186 .is_some_and(|table| table.lock().data_generation() == snapshot.data_generation)
4187 }
4188
4189 pub fn rls_cache_stats(&self) -> RlsCacheStats {
4190 self.rls_cache.lock().stats()
4191 }
4192
4193 pub fn rows_for(
4195 &self,
4196 table: &str,
4197 principal: Option<&crate::auth::Principal>,
4198 ) -> Result<Vec<crate::memtable::Row>> {
4199 if principal.is_none() && self.principal.read().is_some() {
4200 self.refresh_principal()?;
4201 }
4202 let allowed = self.select_column_ids_for(table, principal)?;
4203 let handle = self.table(table)?;
4204 let rows = {
4205 let table = handle.lock();
4206 table.visible_rows(table.snapshot())?
4207 };
4208 let mut rows = self.secure_rows_for(table, rows, principal)?;
4209 for row in &mut rows {
4210 row.columns.retain(|column, _| allowed.contains(column));
4211 }
4212 Ok(rows)
4213 }
4214
4215 pub fn rows_at_epoch_for_current_principal(
4218 &self,
4219 table_name: &str,
4220 snapshot: Snapshot,
4221 ) -> Result<Vec<crate::memtable::Row>> {
4222 self.with_authorized_read_context(
4223 table_name,
4224 None,
4225 true,
4226 Some(&ReadAuthorization {
4227 operation: crate::auth::ColumnOperation::Select,
4228 columns: Vec::new(),
4229 permissions: Vec::new(),
4230 }),
4231 None,
4232 Some(snapshot),
4233 |table, snapshot, allowed, principal| {
4234 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
4235 let mut rows = table.visible_rows(snapshot)?;
4236 if let Some(allowed) = allowed {
4237 rows.retain(|row| allowed.contains(&row.row_id));
4238 }
4239 rows = self.secure_rows_for(table_name, rows, principal)?;
4240 for row in &mut rows {
4241 row.columns
4242 .retain(|column, _| allowed_columns.contains(column));
4243 }
4244 Ok(rows)
4245 },
4246 )
4247 }
4248
4249 pub fn count_for(
4251 &self,
4252 table: &str,
4253 principal: Option<&crate::auth::Principal>,
4254 ) -> Result<u64> {
4255 if principal.is_none() && self.principal.read().is_some() {
4256 self.refresh_principal()?;
4257 }
4258 self.select_column_ids_for(table, principal)?;
4259 if self.security_active_for(table) {
4260 Ok(self.rows_for(table, principal)?.len() as u64)
4261 } else {
4262 Ok(self.table(table)?.lock().count())
4263 }
4264 }
4265
4266 pub fn put_for(
4268 &self,
4269 table: &str,
4270 mut cells: Vec<(u16, crate::memtable::Value)>,
4271 principal: Option<&crate::auth::Principal>,
4272 ) -> Result<RowId> {
4273 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
4274 self.require_columns_for(
4275 table,
4276 crate::auth::ColumnOperation::Insert,
4277 &columns,
4278 principal,
4279 )?;
4280 let handle = self.table(table)?;
4281 let mut table_handle = handle.lock();
4282 table_handle.fill_auto_inc(&mut cells)?;
4283 table_handle.apply_defaults(&mut cells)?;
4284 let mut row = crate::memtable::Row::new(RowId(0), self.epoch.visible());
4285 row.columns.extend(cells.iter().cloned());
4286 self.check_row_policy_for(
4287 table,
4288 crate::security::PolicyCommand::Insert,
4289 &row,
4290 true,
4291 principal,
4292 )?;
4293 table_handle.put(cells)
4294 }
4295
4296 pub fn check_row_policy_for(
4297 &self,
4298 table: &str,
4299 command: crate::security::PolicyCommand,
4300 row: &crate::memtable::Row,
4301 check_new: bool,
4302 principal: Option<&crate::auth::Principal>,
4303 ) -> Result<()> {
4304 let security = self.catalog.read().security.clone();
4305 if !security.rls_enabled(table) {
4306 return Ok(());
4307 }
4308 let cached = self.principal.read().clone();
4309 let principal = principal
4310 .or(cached.as_ref())
4311 .ok_or(MongrelError::AuthRequired)?;
4312 if security.row_allowed(table, command, row, principal, check_new) {
4313 return Ok(());
4314 }
4315 let required = match command {
4316 crate::security::PolicyCommand::Insert => crate::auth::Permission::Insert {
4317 table: table.to_string(),
4318 },
4319 crate::security::PolicyCommand::Update => crate::auth::Permission::Update {
4320 table: table.to_string(),
4321 },
4322 crate::security::PolicyCommand::Select => crate::auth::Permission::Select {
4323 table: table.to_string(),
4324 },
4325 crate::security::PolicyCommand::Delete | crate::security::PolicyCommand::All => {
4326 crate::auth::Permission::Delete {
4327 table: table.to_string(),
4328 }
4329 }
4330 };
4331 Err(MongrelError::PermissionDenied {
4332 required,
4333 principal: principal.username.clone(),
4334 })
4335 }
4336
4337 pub fn set_materialized_view(
4340 &self,
4341 definition: crate::catalog::MaterializedViewEntry,
4342 ) -> Result<()> {
4343 self.set_materialized_view_with_epoch(definition)
4344 .map(|_| ())
4345 }
4346
4347 pub fn set_materialized_view_with_epoch(
4349 &self,
4350 definition: crate::catalog::MaterializedViewEntry,
4351 ) -> Result<Epoch> {
4352 use crate::wal::DdlOp;
4353 use std::sync::atomic::Ordering;
4354
4355 self.require(&crate::auth::Permission::Ddl)?;
4356 if self.poisoned.load(Ordering::Relaxed) {
4357 return Err(MongrelError::Other(
4358 "database poisoned by fsync error".into(),
4359 ));
4360 }
4361 if definition.name.is_empty() || definition.query.trim().is_empty() {
4362 return Err(MongrelError::InvalidArgument(
4363 "materialized view name and query must not be empty".into(),
4364 ));
4365 }
4366
4367 let _ddl = self.ddl_lock.lock();
4368 let _security_write = self.security_write()?;
4369 self.require(&crate::auth::Permission::Ddl)?;
4370 let table_id = self
4371 .catalog
4372 .read()
4373 .live(&definition.name)
4374 .ok_or_else(|| {
4375 MongrelError::NotFound(format!(
4376 "materialized view table {:?} not found",
4377 definition.name
4378 ))
4379 })?
4380 .table_id;
4381 let definition_json = DdlOp::encode_materialized_view(&definition)?;
4382 let _commit = self.commit_lock.lock();
4383 let epoch = self.epoch.bump_assigned();
4384 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
4385 let txn_id = self.alloc_txn_id()?;
4386 let mut next_catalog = self.catalog.read().clone();
4387 if let Some(existing) = next_catalog
4388 .materialized_views
4389 .iter_mut()
4390 .find(|existing| existing.name == definition.name)
4391 {
4392 *existing = definition.clone();
4393 } else {
4394 next_catalog.materialized_views.push(definition.clone());
4395 }
4396 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
4397 let commit_seq = {
4398 let mut wal = self.shared_wal.lock();
4399 let append: Result<u64> = (|| {
4400 wal.append(
4401 txn_id,
4402 table_id,
4403 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
4404 name: definition.name.clone(),
4405 definition_json,
4406 }),
4407 )?;
4408 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
4409 wal.append_commit(txn_id, epoch, &[])
4410 })();
4411 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
4412 };
4413 self.await_durable_commit(commit_seq, epoch)?;
4414
4415 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
4416 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
4417 Ok(epoch)
4418 }
4419
4420 pub fn root(&self) -> &Path {
4422 self.durable_root.canonical_path()
4423 }
4424
4425 pub fn durable_root(&self) -> Arc<crate::durable_file::DurableRoot> {
4428 Arc::clone(&self.durable_root)
4429 }
4430
4431 #[cfg(feature = "encryption")]
4435 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
4436 self.kek
4437 .as_deref()
4438 .map(|kek| kek.derive_subkey(b"mongreldb/server/idempotency/v1"))
4439 }
4440
4441 #[cfg(not(feature = "encryption"))]
4442 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
4443 None
4444 }
4445
4446 pub fn is_read_only_replica(&self) -> bool {
4447 self.read_only
4448 }
4449
4450 pub fn ensure_consistent_read(&self) -> Result<()> {
4454 if self.poisoned.load(Ordering::Relaxed) {
4455 return Err(MongrelError::Other(
4456 "database poisoned by post-commit failure; reopen required".into(),
4457 ));
4458 }
4459 Ok(())
4460 }
4461
4462 pub fn set_replication_wal_retention_segments(&self, segments: usize) {
4463 self.replication_wal_retention_segments
4464 .store(segments, std::sync::atomic::Ordering::Relaxed);
4465 }
4466
4467 pub fn replication_snapshot(&self) -> Result<crate::replication::ReplicationSnapshot> {
4472 let admin = crate::auth::Permission::Admin;
4473 self.require(&admin)?;
4474 let operation_principal = self.principal_snapshot();
4475 let _barrier = self.replication_barrier.write();
4476 let _ddl = self.ddl_lock.lock();
4477 let _security = self.security_coordinator.gate.read();
4478 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4479 let mut handles: Vec<_> = self
4480 .tables
4481 .read()
4482 .iter()
4483 .map(|(id, handle)| (*id, handle.clone()))
4484 .collect();
4485 handles.sort_by_key(|(id, _)| *id);
4486 let _table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
4487 let _commit = self.commit_lock.lock();
4488 let mut wal = self.shared_wal.lock();
4489 wal.group_sync()?;
4490 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
4491 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
4492 let epoch = records
4493 .iter()
4494 .filter_map(|record| match &record.op {
4495 crate::wal::Op::TxnCommit { epoch, .. } => Some(*epoch),
4496 _ => None,
4497 })
4498 .max()
4499 .unwrap_or(0)
4500 .max(self.epoch.committed().0);
4501 let files = crate::replication::capture_files(&self.root)?;
4502 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
4503 drop(wal);
4504 Ok(crate::replication::ReplicationSnapshot::new(
4505 source_id, epoch, files,
4506 ))
4507 }
4508
4509 pub fn hot_backup(&self, destination: impl AsRef<Path>) -> Result<crate::backup::BackupReport> {
4517 let control = crate::ExecutionControl::new(None);
4518 self.hot_backup_controlled(destination, &control, || true)
4519 }
4520
4521 pub(crate) fn hot_backup_to_durable_child(
4522 &self,
4523 parent: &crate::durable_file::DurableRoot,
4524 child: &Path,
4525 control: &crate::ExecutionControl,
4526 ) -> Result<crate::backup::BackupReport> {
4527 let mut components = child.components();
4528 if !matches!(components.next(), Some(std::path::Component::Normal(_)))
4529 || components.next().is_some()
4530 {
4531 return Err(MongrelError::InvalidArgument(
4532 "durable backup child must be one relative path component".into(),
4533 ));
4534 }
4535 let destination_name = child.file_name().ok_or_else(|| {
4536 MongrelError::InvalidArgument("durable backup child has no filename".into())
4537 })?;
4538 let prepared = prepare_backup_destination_in(&self.root, parent, destination_name)?;
4539 self.hot_backup_prepared(prepared, control, || true)
4540 }
4541
4542 #[doc(hidden)]
4545 pub fn hot_backup_controlled<F>(
4546 &self,
4547 destination: impl AsRef<Path>,
4548 control: &crate::ExecutionControl,
4549 before_publish: F,
4550 ) -> Result<crate::backup::BackupReport>
4551 where
4552 F: FnOnce() -> bool,
4553 {
4554 let prepared = prepare_backup_destination(&self.root, destination.as_ref())?;
4555 self.hot_backup_prepared(prepared, control, before_publish)
4556 }
4557
4558 fn hot_backup_prepared<F>(
4559 &self,
4560 mut prepared: PreparedBackupDestination,
4561 control: &crate::ExecutionControl,
4562 before_publish: F,
4563 ) -> Result<crate::backup::BackupReport>
4564 where
4565 F: FnOnce() -> bool,
4566 {
4567 let admin = crate::auth::Permission::Admin;
4568 self.require(&admin)?;
4569 let operation_principal = self.principal_snapshot();
4570 control.checkpoint()?;
4571 let destination = prepared.destination_path.clone();
4572 let mut before_publish = Some(before_publish);
4573
4574 let outcome = (|| {
4575 control.checkpoint()?;
4576 let barrier = self.replication_barrier.write();
4577 let ddl = self.ddl_lock.lock();
4578 let security = self.security_coordinator.gate.read();
4579 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4580 let mut handles: Vec<_> = self
4581 .tables
4582 .read()
4583 .iter()
4584 .map(|(id, handle)| (*id, handle.clone()))
4585 .collect();
4586 handles.sort_by_key(|(id, _)| *id);
4587 let table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
4588 let commit = self.commit_lock.lock();
4589 let mut wal = self.shared_wal.lock();
4590 wal.group_sync()?;
4591 let epoch = self.epoch.committed().0;
4592 let boundary_unix_nanos = current_unix_nanos();
4593
4594 let pin_nonce = std::time::SystemTime::now()
4595 .duration_since(std::time::UNIX_EPOCH)
4596 .unwrap_or_default()
4597 .as_nanos();
4598 let file_pin_root = self
4599 .root
4600 .join(META_DIR)
4601 .join("backup-pins")
4602 .join(format!("{}-{pin_nonce}", std::process::id()));
4603 std::fs::create_dir_all(&file_pin_root)?;
4604 let _file_pins = BackupFilePins {
4605 root: file_pin_root.clone(),
4606 };
4607 let mut run_files = Vec::new();
4608 for (index, (table_id, _)) in handles.iter().enumerate() {
4609 if index % 256 == 0 {
4610 control.checkpoint()?;
4611 }
4612 let table = &table_guards[index];
4613 for (run_index, run) in table.run_refs().iter().enumerate() {
4614 if run_index % 256 == 0 {
4615 control.checkpoint()?;
4616 }
4617 let source = table.run_path(run.run_id as u64);
4618 let relative = Path::new(TABLES_DIR)
4619 .join(table_id.to_string())
4620 .join(crate::engine::RUNS_DIR)
4621 .join(format!("r-{}.sr", run.run_id));
4622 let pinned = file_pin_root.join(format!("{table_id}-{}.sr", run.run_id));
4623 if std::fs::hard_link(&source, &pinned).is_err() {
4624 crate::backup::copy_file_synced(&source, &pinned)?;
4625 }
4626 run_files.push(((*table_id, run.run_id), pinned, relative));
4627 }
4628 }
4629 crate::durable_file::sync_directory(&file_pin_root)?;
4630 let run_keys: Vec<_> = run_files.iter().map(|(key, _, _)| *key).collect();
4631 {
4632 let mut pins = self.backup_pins.lock();
4633 for key in &run_keys {
4634 *pins.entry(*key).or_insert(0) += 1;
4635 }
4636 }
4637 let _run_pins = RunPins {
4638 pins: Arc::clone(&self.backup_pins),
4639 runs: run_keys,
4640 };
4641 let deferred: HashSet<_> = run_files
4642 .iter()
4643 .map(|(_, _, relative)| relative.clone())
4644 .collect();
4645 let mut copied = Vec::new();
4646 copy_backup_boundary(
4647 &self.root,
4648 prepared.stage.as_deref().ok_or_else(|| {
4649 MongrelError::Other("backup staging root was already released".into())
4650 })?,
4651 &deferred,
4652 &mut copied,
4653 Some(control),
4654 )?;
4655
4656 drop(wal);
4657 drop(commit);
4658 drop(table_guards);
4659 drop(security);
4660 drop(ddl);
4661 drop(barrier);
4662
4663 if let Some(hook) = self.backup_hook.lock().as_ref() {
4664 hook();
4665 }
4666 for (index, (_, source, relative)) in run_files.into_iter().enumerate() {
4667 if index % 256 == 0 {
4668 control.checkpoint()?;
4669 }
4670 let mut source = crate::durable_file::open_regular_nofollow(&source)?;
4671 prepared
4672 .stage
4673 .as_deref()
4674 .ok_or_else(|| {
4675 MongrelError::Other("backup staging root was already released".into())
4676 })?
4677 .copy_new_from(&relative, &mut source)?;
4678 copied.push(relative);
4679 }
4680
4681 let manifest = crate::backup::BackupManifest::create_controlled_durable(
4682 prepared.stage.as_deref().ok_or_else(|| {
4683 MongrelError::Other("backup staging root was already released".into())
4684 })?,
4685 epoch,
4686 &copied,
4687 control,
4688 )?;
4689 manifest.write_to_durable(prepared.stage.as_deref().ok_or_else(|| {
4690 MongrelError::Other("backup staging root was already released".into())
4691 })?)?;
4692 control.checkpoint()?;
4693 let publish = before_publish.take().ok_or_else(|| {
4694 MongrelError::Other("backup publication callback already consumed".into())
4695 })?;
4696 if !publish() {
4697 return Err(MongrelError::Cancelled);
4698 }
4699 let final_security = self.security_coordinator.gate.read();
4700 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4701 drop(prepared.stage.take().ok_or_else(|| {
4705 MongrelError::Other("backup staging root was already released".into())
4706 })?);
4707 let published = std::cell::Cell::new(false);
4708 if let Err(error) = prepared.parent.rename_directory_new_with_after(
4709 Path::new(&prepared.stage_name),
4710 &prepared.parent,
4711 Path::new(&prepared.destination_name),
4712 || published.set(true),
4713 ) {
4714 if published.get() {
4715 return Err(MongrelError::CommitOutcomeUnknown {
4716 epoch,
4717 message: format!("backup publication was not durable: {error}"),
4718 });
4719 }
4720 return Err(error.into());
4721 }
4722 drop(final_security);
4723 Ok(crate::backup::BackupReport {
4724 destination,
4725 epoch,
4726 boundary_unix_nanos,
4727 files: manifest.files.len(),
4728 bytes: manifest.total_bytes(),
4729 })
4730 })();
4731
4732 if outcome.is_err() {
4733 drop(prepared.stage.take());
4734 let _ = prepared
4735 .parent
4736 .remove_directory_all(Path::new(&prepared.stage_name));
4737 }
4738 outcome
4739 }
4740
4741 pub fn replication_batch_since(
4744 &self,
4745 since_epoch: u64,
4746 ) -> Result<crate::replication::ReplicationBatch> {
4747 use crate::wal::Op;
4748
4749 let admin = crate::auth::Permission::Admin;
4750 self.require(&admin)?;
4751 let operation_principal = self.principal_snapshot();
4752
4753 let mut wal = self.shared_wal.lock();
4754 wal.group_sync()?;
4755 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
4756 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
4757 drop(wal);
4758
4759 let commits: HashMap<u64, u64> = records
4760 .iter()
4761 .filter_map(|record| match &record.op {
4762 Op::TxnCommit { epoch, .. } => Some((record.txn_id, *epoch)),
4763 _ => None,
4764 })
4765 .collect();
4766 let earliest_epoch = commits.values().copied().min();
4767 let current_epoch = commits
4768 .values()
4769 .copied()
4770 .max()
4771 .unwrap_or(0)
4772 .max(self.epoch.committed().0);
4773 let selected: HashSet<u64> = commits
4774 .iter()
4775 .filter_map(|(txn_id, epoch)| (*epoch > since_epoch).then_some(*txn_id))
4776 .collect();
4777 let retention_gap = since_epoch < current_epoch
4778 && since_epoch < crate::replication::replication_wal_floor(&self.root)?;
4779 let spilled = records.iter().any(|record| {
4780 selected.contains(&record.txn_id)
4781 && matches!(
4782 &record.op,
4783 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty()
4784 )
4785 });
4786 let records = records
4787 .into_iter()
4788 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
4789 .filter(|record| selected.contains(&record.txn_id))
4790 .collect();
4791 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
4792 let batch = crate::replication::ReplicationBatch::complete_for_source(
4793 source_id,
4794 since_epoch,
4795 current_epoch,
4796 earliest_epoch,
4797 retention_gap,
4798 spilled,
4799 records,
4800 )?;
4801 if let Some(hook) = self.replication_hook.lock().as_ref() {
4802 hook();
4803 }
4804 let _security = self.security_coordinator.gate.read();
4805 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4806 Ok(batch)
4807 }
4808
4809 pub fn append_replication_batch(
4813 &self,
4814 batch: &crate::replication::ReplicationBatch,
4815 ) -> Result<u64> {
4816 use crate::wal::Op;
4817
4818 if !self.read_only {
4819 return Err(MongrelError::InvalidArgument(
4820 "replication batches may only target a marked replica".into(),
4821 ));
4822 }
4823 let current = crate::replication::replica_epoch(&self.root)?;
4824 if batch.is_source_bound() {
4825 let source_id = crate::replication::replica_source_id_durable(&self.durable_root)?;
4826 if batch.source_id != source_id {
4827 return Err(MongrelError::Conflict(
4828 "replication batch source does not match follower binding".into(),
4829 ));
4830 }
4831 }
4832 if batch.requires_snapshot {
4833 return Err(MongrelError::Conflict(
4834 "replication snapshot required for this batch".into(),
4835 ));
4836 }
4837 batch.validate_proof()?;
4838 if batch.from_epoch != current {
4839 if batch.from_epoch < current && batch.current_epoch == current {
4840 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
4841 let _wal = self.shared_wal.lock();
4842 let existing: HashSet<(u64, u64)> =
4843 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
4844 .into_iter()
4845 .filter_map(|record| match record.op {
4846 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
4847 _ => None,
4848 })
4849 .collect();
4850 let already_applied = batch.records.iter().all(|record| match &record.op {
4851 Op::TxnCommit { epoch, .. } => existing.contains(&(record.txn_id, *epoch)),
4852 _ => true,
4853 });
4854 if already_applied {
4855 return Ok(current);
4856 }
4857 }
4858 return Err(MongrelError::Conflict(format!(
4859 "replication batch starts at epoch {}, follower is at epoch {current}",
4860 batch.from_epoch
4861 )));
4862 }
4863 if batch.current_epoch < current {
4864 return Err(MongrelError::InvalidArgument(format!(
4865 "replication batch current epoch {} precedes follower epoch {current}",
4866 batch.current_epoch
4867 )));
4868 }
4869 let records = &batch.records;
4870 let mut commits = HashMap::new();
4871 let mut commit_epochs = HashSet::new();
4872 let mut commit_timestamps = HashMap::new();
4873 for record in records {
4874 match &record.op {
4875 Op::TxnCommit { epoch, added_runs } => {
4876 if !added_runs.is_empty() {
4877 return Err(MongrelError::Conflict(
4878 "replication snapshot required for spilled-run transaction".into(),
4879 ));
4880 }
4881 if commits.insert(record.txn_id, *epoch).is_some() {
4882 return Err(MongrelError::InvalidArgument(format!(
4883 "duplicate commit for replication transaction {}",
4884 record.txn_id
4885 )));
4886 }
4887 if *epoch <= current || *epoch > batch.current_epoch {
4888 return Err(MongrelError::InvalidArgument(format!(
4889 "replication commit epoch {epoch} is outside ({current}, {}]",
4890 batch.current_epoch
4891 )));
4892 }
4893 if !commit_epochs.insert(*epoch) {
4894 return Err(MongrelError::InvalidArgument(format!(
4895 "duplicate replication commit epoch {epoch}"
4896 )));
4897 }
4898 }
4899 Op::CommitTimestamp { unix_nanos } => {
4900 commit_timestamps.insert(record.txn_id, *unix_nanos);
4901 }
4902 _ => {}
4903 }
4904 }
4905 for record in records {
4906 if record.txn_id == crate::wal::SYSTEM_TXN_ID
4907 || matches!(&record.op, Op::TxnAbort | Op::Flush { .. })
4908 {
4909 return Err(MongrelError::InvalidArgument(
4910 "replication batch contains a non-committed record".into(),
4911 ));
4912 }
4913 if !commits.contains_key(&record.txn_id) {
4914 return Err(MongrelError::InvalidArgument(format!(
4915 "incomplete replication transaction {}",
4916 record.txn_id
4917 )));
4918 }
4919 }
4920 let target_epoch = commits
4921 .values()
4922 .copied()
4923 .filter(|epoch| *epoch > current)
4924 .max()
4925 .unwrap_or(current);
4926 if target_epoch != batch.current_epoch {
4927 return Err(MongrelError::InvalidArgument(format!(
4928 "replication batch ends at epoch {target_epoch}, expected {}",
4929 batch.current_epoch
4930 )));
4931 }
4932 let mut selected: HashSet<u64> = commits
4933 .iter()
4934 .filter_map(|(txn_id, epoch)| (*epoch > current).then_some(*txn_id))
4935 .collect();
4936 if selected.is_empty() {
4937 return Ok(current);
4938 }
4939 let mut wal = self.shared_wal.lock();
4940 wal.group_sync()?;
4941 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
4942 let existing: HashSet<(u64, u64)> =
4943 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
4944 .into_iter()
4945 .filter_map(|record| match record.op {
4946 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
4947 _ => None,
4948 })
4949 .collect();
4950 selected.retain(|txn_id| {
4951 commits
4952 .get(txn_id)
4953 .is_some_and(|epoch| !existing.contains(&(*txn_id, *epoch)))
4954 });
4955 for record in records {
4956 if !selected.contains(&record.txn_id) {
4957 continue;
4958 }
4959 match &record.op {
4960 Op::TxnCommit { epoch, added_runs } => {
4961 let timestamp = commit_timestamps
4962 .get(&record.txn_id)
4963 .copied()
4964 .unwrap_or_else(current_unix_nanos);
4965 wal.append_commit_at(record.txn_id, Epoch(*epoch), added_runs, timestamp)?;
4966 }
4967 Op::TxnAbort | Op::Flush { .. } | Op::CommitTimestamp { .. } => {}
4968 op => {
4969 wal.append(record.txn_id, 0, op.clone())?;
4970 }
4971 }
4972 }
4973 if !selected.is_empty() {
4974 wal.group_sync()?;
4975 }
4976 drop(wal);
4977
4978 let mut recovered_catalog = self.catalog.read().clone();
4982 if let Err(error) = recover_ddl_from_wal(
4983 &self.root,
4984 Some(&self.durable_root),
4985 &mut recovered_catalog,
4986 self.meta_dek.as_ref(),
4987 wal_dek.as_ref(),
4988 true,
4989 None,
4990 ) {
4991 return Err(MongrelError::DurableCommit {
4992 epoch: target_epoch,
4993 message: format!(
4994 "replication WAL is durable but catalog checkpoint failed: {error}"
4995 ),
4996 });
4997 }
4998 let _security = self.security_coordinator.gate.write();
4999 let old_security_version = self.catalog.read().security_version;
5000 let security_changed = old_security_version != recovered_catalog.security_version
5001 || self.catalog.read().require_auth != recovered_catalog.require_auth;
5002 let require_auth = recovered_catalog.require_auth;
5003 let principal = if security_changed {
5004 None
5005 } else {
5006 self.principal.read().as_ref().and_then(|principal| {
5007 Self::resolve_bound_principal_from_catalog(&recovered_catalog, principal)
5008 })
5009 };
5010 if require_auth {
5011 self.auth_state.set_require_auth(true);
5012 }
5013 self.auth_state.set_principal(principal.clone());
5014 *self.principal.write() = principal;
5015 let security_version = recovered_catalog.security_version;
5016 *self.catalog.write() = recovered_catalog;
5017 self.security_coordinator
5018 .version
5019 .store(security_version, Ordering::Release);
5020 if !require_auth {
5021 self.auth_state.set_require_auth(false);
5022 }
5023 if let Err(error) =
5024 crate::replication::reconcile_replica_epoch_durable(&self.durable_root, target_epoch)
5025 {
5026 return Err(MongrelError::DurableCommit {
5027 epoch: target_epoch,
5028 message: format!(
5029 "replication WAL and catalog are durable but follower watermark failed: {error}"
5030 ),
5031 });
5032 }
5033 Ok(target_epoch)
5034 }
5035
5036 pub fn table_id(&self, name: &str) -> Result<u64> {
5039 let cat = self.catalog.read();
5040 cat.live(name)
5041 .map(|e| e.table_id)
5042 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
5043 }
5044
5045 pub fn table_identity(&self, name: &str) -> Result<(u64, u64)> {
5050 let catalog = self.catalog.read();
5051 catalog
5052 .live(name)
5053 .map(|entry| (entry.table_id, entry.schema.schema_id))
5054 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
5055 }
5056
5057 pub(crate) fn building_table_id(&self, name: &str) -> Result<u64> {
5058 self.catalog
5059 .read()
5060 .building(name)
5061 .map(|entry| entry.table_id)
5062 .ok_or_else(|| MongrelError::NotFound(format!("building table {name:?} not found")))
5063 }
5064
5065 pub fn procedures(&self) -> Vec<StoredProcedure> {
5066 self.catalog
5067 .read()
5068 .procedures
5069 .iter()
5070 .map(|p| p.procedure.clone())
5071 .collect()
5072 }
5073
5074 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
5075 self.catalog
5076 .read()
5077 .procedures
5078 .iter()
5079 .find(|p| p.procedure.name == name)
5080 .map(|p| p.procedure.clone())
5081 }
5082
5083 pub fn create_procedure(&self, procedure: StoredProcedure) -> Result<StoredProcedure> {
5084 self.create_procedure_inner(procedure, None)
5085 }
5086
5087 pub fn create_procedure_controlled<F>(
5088 &self,
5089 procedure: StoredProcedure,
5090 mut before_publish: F,
5091 ) -> Result<StoredProcedure>
5092 where
5093 F: FnMut() -> Result<()>,
5094 {
5095 self.create_procedure_inner(procedure, Some(&mut before_publish))
5096 }
5097
5098 fn create_procedure_inner(
5099 &self,
5100 mut procedure: StoredProcedure,
5101 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5102 ) -> Result<StoredProcedure> {
5103 self.require(&crate::auth::Permission::Ddl)?;
5104 let _g = self.ddl_lock.lock();
5105 let _security_write = self.security_write()?;
5106 self.require(&crate::auth::Permission::Ddl)?;
5107 procedure.validate()?;
5108 self.validate_procedure_references(&procedure)?;
5109 {
5110 let cat = self.catalog.read();
5111 if cat
5112 .procedures
5113 .iter()
5114 .any(|p| p.procedure.name == procedure.name)
5115 {
5116 return Err(MongrelError::InvalidArgument(format!(
5117 "procedure {:?} already exists",
5118 procedure.name
5119 )));
5120 }
5121 }
5122 let commit_lock = Arc::clone(&self.commit_lock);
5123 let _c = commit_lock.lock();
5124 let epoch = self.epoch.bump_assigned();
5125 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5126 procedure.created_epoch = epoch.0;
5127 procedure.updated_epoch = epoch.0;
5128 let mut next_catalog = self.catalog.read().clone();
5129 next_catalog
5130 .procedures
5131 .push(ProcedureEntry::from(procedure.clone()));
5132 next_catalog.db_epoch = epoch.0;
5133 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5134 Ok(procedure)
5135 }
5136
5137 pub fn create_or_replace_procedure(
5138 &self,
5139 procedure: StoredProcedure,
5140 ) -> Result<StoredProcedure> {
5141 self.create_or_replace_procedure_inner(procedure, None)
5142 }
5143
5144 pub fn create_or_replace_procedure_controlled<F>(
5145 &self,
5146 procedure: StoredProcedure,
5147 mut before_publish: F,
5148 ) -> Result<StoredProcedure>
5149 where
5150 F: FnMut() -> Result<()>,
5151 {
5152 self.create_or_replace_procedure_inner(procedure, Some(&mut before_publish))
5153 }
5154
5155 fn create_or_replace_procedure_inner(
5156 &self,
5157 procedure: StoredProcedure,
5158 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5159 ) -> Result<StoredProcedure> {
5160 self.require(&crate::auth::Permission::Ddl)?;
5161 let _g = self.ddl_lock.lock();
5162 let _security_write = self.security_write()?;
5163 self.require(&crate::auth::Permission::Ddl)?;
5164 procedure.validate()?;
5165 self.validate_procedure_references(&procedure)?;
5166 let commit_lock = Arc::clone(&self.commit_lock);
5167 let _c = commit_lock.lock();
5168 let epoch = self.epoch.bump_assigned();
5169 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5170 let mut next_catalog = self.catalog.read().clone();
5171 let replaced = {
5172 let next = match next_catalog
5173 .procedures
5174 .iter()
5175 .position(|p| p.procedure.name == procedure.name)
5176 {
5177 Some(idx) => {
5178 let next = next_catalog.procedures[idx]
5179 .procedure
5180 .replaced(procedure.clone(), epoch.0)?;
5181 next_catalog.procedures[idx] = ProcedureEntry::from(next.clone());
5182 next
5183 }
5184 None => {
5185 let mut next = procedure;
5186 next.created_epoch = epoch.0;
5187 next.updated_epoch = epoch.0;
5188 next_catalog
5189 .procedures
5190 .push(ProcedureEntry::from(next.clone()));
5191 next
5192 }
5193 };
5194 next_catalog.db_epoch = epoch.0;
5195 next
5196 };
5197 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5198 Ok(replaced)
5199 }
5200
5201 pub fn drop_procedure(&self, name: &str) -> Result<()> {
5202 self.drop_procedure_with_epoch(name).map(|_| ())
5203 }
5204
5205 pub fn drop_procedure_with_epoch(&self, name: &str) -> Result<Epoch> {
5206 self.drop_procedure_with_epoch_inner(name, None)
5207 }
5208
5209 pub fn drop_procedure_with_epoch_controlled<F>(
5210 &self,
5211 name: &str,
5212 mut before_publish: F,
5213 ) -> Result<Epoch>
5214 where
5215 F: FnMut() -> Result<()>,
5216 {
5217 self.drop_procedure_with_epoch_inner(name, Some(&mut before_publish))
5218 }
5219
5220 fn drop_procedure_with_epoch_inner(
5221 &self,
5222 name: &str,
5223 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5224 ) -> Result<Epoch> {
5225 self.require(&crate::auth::Permission::Ddl)?;
5226 let _g = self.ddl_lock.lock();
5227 let _security_write = self.security_write()?;
5228 self.require(&crate::auth::Permission::Ddl)?;
5229 let commit_lock = Arc::clone(&self.commit_lock);
5230 let _c = commit_lock.lock();
5231 let epoch = self.epoch.bump_assigned();
5232 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5233 let mut next_catalog = self.catalog.read().clone();
5234 let before = next_catalog.procedures.len();
5235 next_catalog.procedures.retain(|p| p.procedure.name != name);
5236 if next_catalog.procedures.len() == before {
5237 return Err(MongrelError::NotFound(format!(
5238 "procedure {name:?} not found"
5239 )));
5240 }
5241 next_catalog.db_epoch = epoch.0;
5242 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5243 Ok(epoch)
5244 }
5245
5246 pub fn users(&self) -> Vec<crate::auth::UserEntry> {
5251 self.catalog.read().users.clone()
5252 }
5253
5254 pub fn user_identity(&self, username: &str) -> Option<(u64, u64)> {
5257 self.catalog
5258 .read()
5259 .users
5260 .iter()
5261 .find(|user| user.username == username)
5262 .map(|user| (user.id, user.created_epoch))
5263 }
5264
5265 pub fn security_version(&self) -> u64 {
5268 self.catalog.read().security_version
5269 }
5270
5271 pub fn roles(&self) -> Vec<crate::auth::RoleEntry> {
5273 self.catalog.read().roles.clone()
5274 }
5275
5276 pub fn create_user(&self, username: &str, password: &str) -> Result<crate::auth::UserEntry> {
5278 self.require(&crate::auth::Permission::Admin)?;
5279 let hash = crate::auth::hash_password(password).map_err(MongrelError::Other)?;
5280 self.create_user_with_password_hash(username, hash)
5281 }
5282
5283 pub fn create_user_with_password_hash(
5285 &self,
5286 username: &str,
5287 hash: String,
5288 ) -> Result<crate::auth::UserEntry> {
5289 self.create_user_with_password_hash_inner(username, hash, None)
5290 }
5291
5292 pub fn create_user_with_password_hash_controlled<F>(
5293 &self,
5294 username: &str,
5295 hash: String,
5296 mut before_publish: F,
5297 ) -> Result<crate::auth::UserEntry>
5298 where
5299 F: FnMut() -> Result<()>,
5300 {
5301 self.create_user_with_password_hash_inner(username, hash, Some(&mut before_publish))
5302 }
5303
5304 fn create_user_with_password_hash_inner(
5305 &self,
5306 username: &str,
5307 hash: String,
5308 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5309 ) -> Result<crate::auth::UserEntry> {
5310 self.require(&crate::auth::Permission::Admin)?;
5311 let _ddl = self.ddl_lock.lock();
5312 let _security_write = self.security_write()?;
5313 self.require(&crate::auth::Permission::Admin)?;
5314 let _commit = self.commit_lock.lock();
5315 let epoch = self.epoch.bump_assigned();
5316 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5317 let mut next_catalog = self.catalog.read().clone();
5318 if next_catalog.users.iter().any(|u| u.username == username) {
5319 return Err(MongrelError::InvalidArgument(format!(
5320 "user {username:?} already exists"
5321 )));
5322 }
5323 next_catalog.next_user_id = next_catalog.next_user_id.max(1);
5324 let id = next_catalog.next_user_id;
5325 next_catalog.next_user_id = id
5326 .checked_add(1)
5327 .ok_or_else(|| MongrelError::Full("user-id namespace exhausted".into()))?;
5328 let entry = crate::auth::UserEntry {
5329 id,
5330 username: username.into(),
5331 password_hash: hash,
5332 roles: Vec::new(),
5333 is_admin: false,
5334 created_epoch: epoch.0,
5335 };
5336 next_catalog.users.push(entry.clone());
5337 advance_security_version(&mut next_catalog)?;
5338 next_catalog.db_epoch = epoch.0;
5339 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5340 Ok(entry)
5341 }
5342
5343 pub fn drop_user(&self, username: &str) -> Result<()> {
5345 self.drop_user_with_epoch(username).map(|_| ())
5346 }
5347
5348 pub fn drop_user_with_epoch(&self, username: &str) -> Result<Epoch> {
5349 self.drop_user_with_epoch_inner(username, None)
5350 }
5351
5352 pub fn drop_user_with_epoch_controlled<F>(
5353 &self,
5354 username: &str,
5355 mut before_publish: F,
5356 ) -> Result<Epoch>
5357 where
5358 F: FnMut() -> Result<()>,
5359 {
5360 self.drop_user_with_epoch_inner(username, Some(&mut before_publish))
5361 }
5362
5363 fn drop_user_with_epoch_inner(
5364 &self,
5365 username: &str,
5366 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5367 ) -> Result<Epoch> {
5368 self.require(&crate::auth::Permission::Admin)?;
5369 let _ddl = self.ddl_lock.lock();
5370 let _security_write = self.security_write()?;
5371 self.require(&crate::auth::Permission::Admin)?;
5372 let _commit = self.commit_lock.lock();
5373 let epoch = self.epoch.bump_assigned();
5374 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5375 let mut next_catalog = self.catalog.read().clone();
5376 let before = next_catalog.users.len();
5377 next_catalog.users.retain(|u| u.username != username);
5378 if next_catalog.users.len() == before {
5379 return Err(MongrelError::NotFound(format!(
5380 "user {username:?} not found"
5381 )));
5382 }
5383 advance_security_version(&mut next_catalog)?;
5384 next_catalog.db_epoch = epoch.0;
5385 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5386 Ok(epoch)
5387 }
5388
5389 pub fn alter_user_password(&self, username: &str, new_password: &str) -> Result<()> {
5391 self.alter_user_password_with_epoch(username, new_password)
5392 .map(|_| ())
5393 }
5394
5395 pub fn alter_user_password_with_epoch(
5396 &self,
5397 username: &str,
5398 new_password: &str,
5399 ) -> Result<Epoch> {
5400 self.require(&crate::auth::Permission::Admin)?;
5401 let hash = crate::auth::hash_password(new_password).map_err(MongrelError::Other)?;
5402 self.alter_user_password_hash_with_epoch(username, hash)
5403 }
5404
5405 pub fn alter_user_password_hash_with_epoch(
5406 &self,
5407 username: &str,
5408 hash: String,
5409 ) -> Result<Epoch> {
5410 self.alter_user_password_hash_with_epoch_inner(username, hash, None)
5411 }
5412
5413 pub fn alter_user_password_hash_with_epoch_controlled<F>(
5414 &self,
5415 username: &str,
5416 hash: String,
5417 mut before_publish: F,
5418 ) -> Result<Epoch>
5419 where
5420 F: FnMut() -> Result<()>,
5421 {
5422 self.alter_user_password_hash_with_epoch_inner(username, hash, Some(&mut before_publish))
5423 }
5424
5425 fn alter_user_password_hash_with_epoch_inner(
5426 &self,
5427 username: &str,
5428 hash: String,
5429 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5430 ) -> Result<Epoch> {
5431 self.require(&crate::auth::Permission::Admin)?;
5432 let _ddl = self.ddl_lock.lock();
5433 let _security_write = self.security_write()?;
5434 self.require(&crate::auth::Permission::Admin)?;
5435 let _commit = self.commit_lock.lock();
5436 let epoch = self.epoch.bump_assigned();
5437 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5438 let mut next_catalog = self.catalog.read().clone();
5439 let user = next_catalog
5440 .users
5441 .iter_mut()
5442 .find(|u| u.username == username)
5443 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5444 user.password_hash = hash;
5445 advance_security_version(&mut next_catalog)?;
5446 next_catalog.db_epoch = epoch.0;
5447 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5448 Ok(epoch)
5449 }
5450
5451 pub fn verify_user(
5454 &self,
5455 username: &str,
5456 password: &str,
5457 ) -> Result<Option<crate::auth::UserEntry>> {
5458 let cat = self.catalog.read();
5459 let Some(user) = cat.users.iter().find(|u| u.username == username) else {
5460 return Ok(None);
5461 };
5462 if user.password_hash.is_empty() {
5463 return Ok(None);
5464 }
5465 let ok = crate::auth::verify_password(password, &user.password_hash)
5466 .map_err(MongrelError::Other)?;
5467 if ok {
5468 Ok(Some(user.clone()))
5469 } else {
5470 Ok(None)
5471 }
5472 }
5473
5474 pub fn authenticate_principal(
5478 &self,
5479 username: &str,
5480 password: &str,
5481 ) -> Result<Option<crate::auth::Principal>> {
5482 self.authenticate_principal_inner(username, password, || {})
5483 }
5484
5485 fn authenticate_principal_inner<F>(
5486 &self,
5487 username: &str,
5488 password: &str,
5489 after_verify: F,
5490 ) -> Result<Option<crate::auth::Principal>>
5491 where
5492 F: FnOnce(),
5493 {
5494 let catalog = self.catalog.read();
5495 let Some(user) = catalog.users.iter().find(|user| user.username == username) else {
5496 return Ok(None);
5497 };
5498 if user.password_hash.is_empty()
5499 || !crate::auth::verify_password(password, &user.password_hash)
5500 .map_err(MongrelError::Other)?
5501 {
5502 return Ok(None);
5503 }
5504 after_verify();
5505 Ok(Self::resolve_user_principal_from_catalog(&catalog, user))
5506 }
5507
5508 pub fn set_user_admin(&self, username: &str, is_admin: bool) -> Result<()> {
5510 self.set_user_admin_with_epoch(username, is_admin)
5511 .map(|_| ())
5512 }
5513
5514 pub fn set_user_admin_with_epoch(
5515 &self,
5516 username: &str,
5517 is_admin: bool,
5518 ) -> Result<Option<Epoch>> {
5519 self.set_user_admin_with_epoch_inner(username, is_admin, None)
5520 }
5521
5522 pub fn set_user_admin_with_epoch_controlled<F>(
5523 &self,
5524 username: &str,
5525 is_admin: bool,
5526 mut before_publish: F,
5527 ) -> Result<Option<Epoch>>
5528 where
5529 F: FnMut() -> Result<()>,
5530 {
5531 self.set_user_admin_with_epoch_inner(username, is_admin, Some(&mut before_publish))
5532 }
5533
5534 fn set_user_admin_with_epoch_inner(
5535 &self,
5536 username: &str,
5537 is_admin: bool,
5538 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5539 ) -> Result<Option<Epoch>> {
5540 self.require(&crate::auth::Permission::Admin)?;
5541 let _ddl = self.ddl_lock.lock();
5542 let _security_write = self.security_write()?;
5543 self.require(&crate::auth::Permission::Admin)?;
5544 let _commit = self.commit_lock.lock();
5545 let mut next_catalog = self.catalog.read().clone();
5546 let user = next_catalog
5547 .users
5548 .iter_mut()
5549 .find(|u| u.username == username)
5550 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5551 if user.is_admin == is_admin {
5552 return Ok(None);
5553 }
5554 user.is_admin = is_admin;
5555 let epoch = self.epoch.bump_assigned();
5556 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5557 advance_security_version(&mut next_catalog)?;
5558 next_catalog.db_epoch = epoch.0;
5559 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5560 Ok(Some(epoch))
5561 }
5562
5563 pub fn create_role(&self, name: &str) -> Result<crate::auth::RoleEntry> {
5565 self.create_role_inner(name, None)
5566 }
5567
5568 pub fn create_role_controlled<F>(
5569 &self,
5570 name: &str,
5571 mut before_publish: F,
5572 ) -> Result<crate::auth::RoleEntry>
5573 where
5574 F: FnMut() -> Result<()>,
5575 {
5576 self.create_role_inner(name, Some(&mut before_publish))
5577 }
5578
5579 fn create_role_inner(
5580 &self,
5581 name: &str,
5582 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5583 ) -> Result<crate::auth::RoleEntry> {
5584 self.require(&crate::auth::Permission::Admin)?;
5585 let _ddl = self.ddl_lock.lock();
5586 let _security_write = self.security_write()?;
5587 self.require(&crate::auth::Permission::Admin)?;
5588 let _commit = self.commit_lock.lock();
5589 let epoch = self.epoch.bump_assigned();
5590 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5591 let mut next_catalog = self.catalog.read().clone();
5592 if next_catalog.roles.iter().any(|r| r.name == name) {
5593 return Err(MongrelError::InvalidArgument(format!(
5594 "role {name:?} already exists"
5595 )));
5596 }
5597 let entry = crate::auth::RoleEntry {
5598 name: name.into(),
5599 permissions: Vec::new(),
5600 created_epoch: epoch.0,
5601 };
5602 next_catalog.roles.push(entry.clone());
5603 advance_security_version(&mut next_catalog)?;
5604 next_catalog.db_epoch = epoch.0;
5605 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5606 Ok(entry)
5607 }
5608
5609 pub fn drop_role(&self, name: &str) -> Result<()> {
5611 self.drop_role_with_epoch(name).map(|_| ())
5612 }
5613
5614 pub fn drop_role_with_epoch(&self, name: &str) -> Result<Epoch> {
5615 self.drop_role_with_epoch_inner(name, None)
5616 }
5617
5618 pub fn drop_role_with_epoch_controlled<F>(
5619 &self,
5620 name: &str,
5621 mut before_publish: F,
5622 ) -> Result<Epoch>
5623 where
5624 F: FnMut() -> Result<()>,
5625 {
5626 self.drop_role_with_epoch_inner(name, Some(&mut before_publish))
5627 }
5628
5629 fn drop_role_with_epoch_inner(
5630 &self,
5631 name: &str,
5632 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5633 ) -> Result<Epoch> {
5634 self.require(&crate::auth::Permission::Admin)?;
5635 let _ddl = self.ddl_lock.lock();
5636 let _security_write = self.security_write()?;
5637 self.require(&crate::auth::Permission::Admin)?;
5638 let _commit = self.commit_lock.lock();
5639 let epoch = self.epoch.bump_assigned();
5640 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5641 let mut next_catalog = self.catalog.read().clone();
5642 let before = next_catalog.roles.len();
5643 next_catalog.roles.retain(|r| r.name != name);
5644 if next_catalog.roles.len() == before {
5645 return Err(MongrelError::NotFound(format!("role {name:?} not found")));
5646 }
5647 for user in &mut next_catalog.users {
5648 user.roles.retain(|r| r != name);
5649 }
5650 advance_security_version(&mut next_catalog)?;
5651 next_catalog.db_epoch = epoch.0;
5652 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5653 Ok(epoch)
5654 }
5655
5656 pub fn grant_role(&self, username: &str, role_name: &str) -> Result<()> {
5658 self.grant_role_with_epoch(username, role_name).map(|_| ())
5659 }
5660
5661 pub fn grant_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
5662 self.grant_role_with_epoch_inner(username, role_name, None)
5663 }
5664
5665 pub fn grant_role_with_epoch_controlled<F>(
5666 &self,
5667 username: &str,
5668 role_name: &str,
5669 mut before_publish: F,
5670 ) -> Result<Option<Epoch>>
5671 where
5672 F: FnMut() -> Result<()>,
5673 {
5674 self.grant_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
5675 }
5676
5677 fn grant_role_with_epoch_inner(
5678 &self,
5679 username: &str,
5680 role_name: &str,
5681 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5682 ) -> Result<Option<Epoch>> {
5683 self.require(&crate::auth::Permission::Admin)?;
5684 let _ddl = self.ddl_lock.lock();
5685 let _security_write = self.security_write()?;
5686 self.require(&crate::auth::Permission::Admin)?;
5687 let _commit = self.commit_lock.lock();
5688 let mut next_catalog = self.catalog.read().clone();
5689 if !next_catalog.roles.iter().any(|r| r.name == role_name) {
5690 return Err(MongrelError::NotFound(format!(
5691 "role {role_name:?} not found"
5692 )));
5693 }
5694 let user = next_catalog
5695 .users
5696 .iter_mut()
5697 .find(|u| u.username == username)
5698 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5699 if user.roles.iter().any(|role| role == role_name) {
5700 return Ok(None);
5701 }
5702 user.roles.push(role_name.into());
5703 let epoch = self.epoch.bump_assigned();
5704 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5705 advance_security_version(&mut next_catalog)?;
5706 next_catalog.db_epoch = epoch.0;
5707 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5708 Ok(Some(epoch))
5709 }
5710
5711 pub fn revoke_role(&self, username: &str, role_name: &str) -> Result<()> {
5713 self.revoke_role_with_epoch(username, role_name).map(|_| ())
5714 }
5715
5716 pub fn revoke_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
5717 self.revoke_role_with_epoch_inner(username, role_name, None)
5718 }
5719
5720 pub fn revoke_role_with_epoch_controlled<F>(
5721 &self,
5722 username: &str,
5723 role_name: &str,
5724 mut before_publish: F,
5725 ) -> Result<Option<Epoch>>
5726 where
5727 F: FnMut() -> Result<()>,
5728 {
5729 self.revoke_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
5730 }
5731
5732 fn revoke_role_with_epoch_inner(
5733 &self,
5734 username: &str,
5735 role_name: &str,
5736 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5737 ) -> Result<Option<Epoch>> {
5738 self.require(&crate::auth::Permission::Admin)?;
5739 let _ddl = self.ddl_lock.lock();
5740 let _security_write = self.security_write()?;
5741 self.require(&crate::auth::Permission::Admin)?;
5742 let _commit = self.commit_lock.lock();
5743 let mut next_catalog = self.catalog.read().clone();
5744 let user = next_catalog
5745 .users
5746 .iter_mut()
5747 .find(|u| u.username == username)
5748 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5749 let before = user.roles.len();
5750 user.roles.retain(|r| r != role_name);
5751 if user.roles.len() == before {
5752 return Ok(None);
5753 }
5754 let epoch = self.epoch.bump_assigned();
5755 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5756 advance_security_version(&mut next_catalog)?;
5757 next_catalog.db_epoch = epoch.0;
5758 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5759 Ok(Some(epoch))
5760 }
5761
5762 pub fn grant_permission(
5764 &self,
5765 role_name: &str,
5766 permission: crate::auth::Permission,
5767 ) -> Result<()> {
5768 self.grant_permission_with_epoch(role_name, permission)
5769 .map(|_| ())
5770 }
5771
5772 pub fn grant_permission_with_epoch(
5773 &self,
5774 role_name: &str,
5775 permission: crate::auth::Permission,
5776 ) -> Result<Option<Epoch>> {
5777 self.grant_permission_with_epoch_inner(role_name, permission, None)
5778 }
5779
5780 pub fn grant_permission_with_epoch_controlled<F>(
5781 &self,
5782 role_name: &str,
5783 permission: crate::auth::Permission,
5784 mut before_publish: F,
5785 ) -> Result<Option<Epoch>>
5786 where
5787 F: FnMut() -> Result<()>,
5788 {
5789 self.grant_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
5790 }
5791
5792 fn grant_permission_with_epoch_inner(
5793 &self,
5794 role_name: &str,
5795 permission: crate::auth::Permission,
5796 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5797 ) -> Result<Option<Epoch>> {
5798 self.require(&crate::auth::Permission::Admin)?;
5799 let _ddl = self.ddl_lock.lock();
5800 let _security_write = self.security_write()?;
5801 self.require(&crate::auth::Permission::Admin)?;
5802 let _commit = self.commit_lock.lock();
5803 let mut next_catalog = self.catalog.read().clone();
5804 let role = next_catalog
5805 .roles
5806 .iter_mut()
5807 .find(|r| r.name == role_name)
5808 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
5809 let before = role.permissions.clone();
5810 merge_permission(&mut role.permissions, permission);
5811 if role.permissions == before {
5812 return Ok(None);
5813 }
5814 let epoch = self.epoch.bump_assigned();
5815 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5816 advance_security_version(&mut next_catalog)?;
5817 next_catalog.db_epoch = epoch.0;
5818 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5819 Ok(Some(epoch))
5820 }
5821
5822 pub fn revoke_permission(
5824 &self,
5825 role_name: &str,
5826 permission: crate::auth::Permission,
5827 ) -> Result<()> {
5828 self.revoke_permission_with_epoch(role_name, permission)
5829 .map(|_| ())
5830 }
5831
5832 pub fn revoke_permission_with_epoch(
5833 &self,
5834 role_name: &str,
5835 permission: crate::auth::Permission,
5836 ) -> Result<Option<Epoch>> {
5837 self.revoke_permission_with_epoch_inner(role_name, permission, None)
5838 }
5839
5840 pub fn revoke_permission_with_epoch_controlled<F>(
5841 &self,
5842 role_name: &str,
5843 permission: crate::auth::Permission,
5844 mut before_publish: F,
5845 ) -> Result<Option<Epoch>>
5846 where
5847 F: FnMut() -> Result<()>,
5848 {
5849 self.revoke_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
5850 }
5851
5852 fn revoke_permission_with_epoch_inner(
5853 &self,
5854 role_name: &str,
5855 permission: crate::auth::Permission,
5856 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5857 ) -> Result<Option<Epoch>> {
5858 self.require(&crate::auth::Permission::Admin)?;
5859 let _ddl = self.ddl_lock.lock();
5860 let _security_write = self.security_write()?;
5861 self.require(&crate::auth::Permission::Admin)?;
5862 let _commit = self.commit_lock.lock();
5863 let mut next_catalog = self.catalog.read().clone();
5864 let role = next_catalog
5865 .roles
5866 .iter_mut()
5867 .find(|r| r.name == role_name)
5868 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
5869 let before = role.permissions.clone();
5870 revoke_permission_from(&mut role.permissions, &permission);
5871 if role.permissions == before {
5872 return Ok(None);
5873 }
5874 let epoch = self.epoch.bump_assigned();
5875 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5876 advance_security_version(&mut next_catalog)?;
5877 next_catalog.db_epoch = epoch.0;
5878 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5879 Ok(Some(epoch))
5880 }
5881
5882 pub fn resolve_principal(&self, username: &str) -> Option<crate::auth::Principal> {
5885 let cat = self.catalog.read();
5886 Self::resolve_principal_from_catalog(&cat, username)
5887 }
5888
5889 pub fn resolve_current_principal(
5892 &self,
5893 principal: &crate::auth::Principal,
5894 ) -> Option<crate::auth::Principal> {
5895 Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
5896 }
5897
5898 fn resolve_principal_from_catalog(
5903 cat: &Catalog,
5904 username: &str,
5905 ) -> Option<crate::auth::Principal> {
5906 let user = cat.users.iter().find(|u| u.username == username)?;
5907 Self::resolve_user_principal_from_catalog(cat, user)
5908 }
5909
5910 fn resolve_bound_principal_from_catalog(
5911 cat: &Catalog,
5912 principal: &crate::auth::Principal,
5913 ) -> Option<crate::auth::Principal> {
5914 let user = cat.users.iter().find(|user| {
5915 user.id == principal.user_id
5916 && user.created_epoch == principal.created_epoch
5917 && user.username == principal.username
5918 })?;
5919 Self::resolve_user_principal_from_catalog(cat, user)
5920 }
5921
5922 fn resolve_user_principal_from_catalog(
5923 cat: &Catalog,
5924 user: &crate::auth::UserEntry,
5925 ) -> Option<crate::auth::Principal> {
5926 let mut permissions = Vec::new();
5927 for role_name in &user.roles {
5928 if let Some(role) = cat.roles.iter().find(|r| &r.name == role_name) {
5929 permissions.extend(role.permissions.iter().cloned());
5930 }
5931 }
5932 Some(crate::auth::Principal {
5933 user_id: user.id,
5934 created_epoch: user.created_epoch,
5935 username: user.username.clone(),
5936 is_admin: user.is_admin,
5937 roles: user.roles.clone(),
5938 permissions,
5939 })
5940 }
5941
5942 pub fn check_permission(&self, username: &str, permission: &crate::auth::Permission) -> bool {
5944 match self.resolve_principal(username) {
5945 Some(p) => p.has_permission(permission),
5946 None => false,
5947 }
5948 }
5949
5950 pub fn require_auth_enabled(&self) -> bool {
5954 self.catalog.read().require_auth
5955 }
5956
5957 pub fn principal(&self) -> Option<crate::auth::Principal> {
5961 self.principal.read().clone()
5962 }
5963
5964 fn table_auth_checker(&self) -> Option<Arc<dyn crate::auth_state::TableAuthChecker>> {
5970 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
5971 self.auth_state.clone(),
5972 )))
5973 }
5974
5975 pub fn refresh_principal(&self) -> Result<()> {
5989 let previous = match self.principal.read().clone() {
5990 Some(principal) => principal,
5991 None => return Ok(()),
5992 };
5993 let observed_version = self.security_coordinator.version.load(Ordering::Acquire);
5994 self.refresh_security_catalog_if_stale(observed_version)?;
5995 let cat = self.catalog.read();
5996 match Self::resolve_bound_principal_from_catalog(&cat, &previous) {
5997 Some(p) => {
5998 *self.principal.write() = Some(p.clone());
5999 self.auth_state.set_principal(Some(p));
6003 Ok(())
6004 }
6005 None => Err(MongrelError::InvalidCredentials {
6006 username: previous.username,
6007 }),
6008 }
6009 }
6010
6011 pub fn security_catalog_disk_read_count(&self) -> u64 {
6014 self.security_catalog_disk_reads.load(Ordering::Relaxed)
6015 }
6016
6017 pub fn enable_auth(&self, admin_username: &str, admin_password: &str) -> Result<()> {
6029 let password_hash =
6030 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
6031 let _ddl = self.ddl_lock.lock();
6032 let _security_write = self.security_write()?;
6033 let _commit = self.commit_lock.lock();
6034 let epoch = self.epoch.bump_assigned();
6035 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6036 let mut next_catalog = self.catalog.read().clone();
6037 if next_catalog.require_auth {
6038 return Err(MongrelError::InvalidArgument(
6039 "database already has require_auth enabled".into(),
6040 ));
6041 }
6042 if next_catalog
6043 .users
6044 .iter()
6045 .any(|u| u.username == admin_username)
6046 {
6047 return Err(MongrelError::InvalidArgument(format!(
6048 "user {admin_username:?} already exists"
6049 )));
6050 }
6051 next_catalog.next_user_id = next_catalog.next_user_id.max(1);
6052 let id = next_catalog.next_user_id;
6053 next_catalog.next_user_id = id
6054 .checked_add(1)
6055 .ok_or_else(|| MongrelError::Full("user-id namespace exhausted".into()))?;
6056 next_catalog.users.push(crate::auth::UserEntry {
6057 id,
6058 username: admin_username.to_string(),
6059 password_hash,
6060 roles: Vec::new(),
6061 is_admin: true,
6062 created_epoch: epoch.0,
6063 });
6064 next_catalog.require_auth = true;
6065 advance_security_version(&mut next_catalog)?;
6066 next_catalog.db_epoch = epoch.0;
6067 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
6068 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
6072 let principal = crate::auth::Principal {
6073 user_id: id,
6074 created_epoch: epoch.0,
6075 username: admin_username.to_string(),
6076 is_admin: true,
6077 roles: Vec::new(),
6078 permissions: Vec::new(),
6079 };
6080 *self.principal.write() = Some(principal.clone());
6081 self.auth_state.set_principal(Some(principal));
6082 }
6083 publish
6084 }
6085
6086 pub fn disable_auth(&self) -> Result<()> {
6103 let _ddl = self.ddl_lock.lock();
6104 let _security_write = self.security_write()?;
6105 let _commit = self.commit_lock.lock();
6106 let epoch = self.epoch.bump_assigned();
6107 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6108 let mut next_catalog = self.catalog.read().clone();
6109 if !next_catalog.require_auth {
6110 return Err(MongrelError::InvalidArgument(
6111 "database does not have require_auth enabled".into(),
6112 ));
6113 }
6114 next_catalog.require_auth = false;
6115 advance_security_version(&mut next_catalog)?;
6116 next_catalog.db_epoch = epoch.0;
6117 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
6118 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
6120 *self.principal.write() = None;
6121 }
6122 publish
6123 }
6124
6125 pub fn require(&self, perm: &crate::auth::Permission) -> Result<()> {
6132 if self.read_only && !matches!(perm, crate::auth::Permission::Select { .. }) {
6133 return Err(MongrelError::ReadOnlyReplica);
6134 }
6135 if self.principal.read().is_some() {
6136 self.refresh_principal().map_err(|error| match error {
6137 MongrelError::InvalidCredentials { .. } => MongrelError::AuthRequired,
6138 error => error,
6139 })?;
6140 }
6141 if !self.catalog.read().require_auth {
6142 return Ok(());
6143 }
6144 let guard = self.principal.read();
6145 let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
6146 if p.has_permission(perm) {
6147 Ok(())
6148 } else {
6149 Err(MongrelError::PermissionDenied {
6150 required: perm.clone(),
6151 principal: p.username.clone(),
6152 })
6153 }
6154 }
6155
6156 pub fn require_table(
6161 &self,
6162 table: &str,
6163 perm: crate::auth_state::RequiredPermission,
6164 ) -> Result<()> {
6165 self.require(&perm.into_permission(table))
6166 }
6167
6168 pub fn triggers(&self) -> Vec<StoredTrigger> {
6169 self.catalog
6170 .read()
6171 .triggers
6172 .iter()
6173 .map(|t| t.trigger.clone())
6174 .collect()
6175 }
6176
6177 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
6178 self.catalog
6179 .read()
6180 .triggers
6181 .iter()
6182 .find(|t| t.trigger.name == name)
6183 .map(|t| t.trigger.clone())
6184 }
6185
6186 pub fn create_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
6187 self.create_trigger_inner(trigger, None, None)
6188 }
6189
6190 pub fn create_trigger_controlled<F>(
6191 &self,
6192 trigger: StoredTrigger,
6193 mut before_publish: F,
6194 ) -> Result<StoredTrigger>
6195 where
6196 F: FnMut() -> Result<()>,
6197 {
6198 self.create_trigger_inner(trigger, None, Some(&mut before_publish))
6199 }
6200
6201 pub fn create_trigger_as_controlled<F>(
6202 &self,
6203 trigger: StoredTrigger,
6204 principal: Option<&crate::auth::Principal>,
6205 mut before_publish: F,
6206 ) -> Result<StoredTrigger>
6207 where
6208 F: FnMut() -> Result<()>,
6209 {
6210 self.create_trigger_inner(trigger, principal, Some(&mut before_publish))
6211 }
6212
6213 fn create_trigger_inner(
6214 &self,
6215 mut trigger: StoredTrigger,
6216 principal: Option<&crate::auth::Principal>,
6217 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6218 ) -> Result<StoredTrigger> {
6219 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6220 let _g = self.ddl_lock.lock();
6221 let _security_write = self.security_write()?;
6222 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6223 trigger.validate()?;
6224 self.validate_trigger_references(&trigger)
6225 .map_err(trigger_validation_error)?;
6226 {
6227 let cat = self.catalog.read();
6228 if cat.triggers.iter().any(|t| t.trigger.name == trigger.name) {
6229 return Err(MongrelError::TriggerValidation(format!(
6230 "trigger {:?} already exists",
6231 trigger.name
6232 )));
6233 }
6234 }
6235 let commit_lock = Arc::clone(&self.commit_lock);
6236 let _c = commit_lock.lock();
6237 let epoch = self.epoch.bump_assigned();
6238 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6239 trigger.created_epoch = epoch.0;
6240 trigger.updated_epoch = epoch.0;
6241 let mut next_catalog = self.catalog.read().clone();
6242 next_catalog
6243 .triggers
6244 .push(TriggerEntry::from(trigger.clone()));
6245 next_catalog.db_epoch = epoch.0;
6246 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6247 Ok(trigger)
6248 }
6249
6250 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
6251 self.create_or_replace_trigger_inner(trigger, None, None)
6252 }
6253
6254 pub fn create_or_replace_trigger_controlled<F>(
6255 &self,
6256 trigger: StoredTrigger,
6257 mut before_publish: F,
6258 ) -> Result<StoredTrigger>
6259 where
6260 F: FnMut() -> Result<()>,
6261 {
6262 self.create_or_replace_trigger_inner(trigger, None, Some(&mut before_publish))
6263 }
6264
6265 pub fn create_or_replace_trigger_as_controlled<F>(
6266 &self,
6267 trigger: StoredTrigger,
6268 principal: Option<&crate::auth::Principal>,
6269 mut before_publish: F,
6270 ) -> Result<StoredTrigger>
6271 where
6272 F: FnMut() -> Result<()>,
6273 {
6274 self.create_or_replace_trigger_inner(trigger, principal, Some(&mut before_publish))
6275 }
6276
6277 fn create_or_replace_trigger_inner(
6278 &self,
6279 trigger: StoredTrigger,
6280 principal: Option<&crate::auth::Principal>,
6281 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6282 ) -> Result<StoredTrigger> {
6283 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6284 let _g = self.ddl_lock.lock();
6285 let _security_write = self.security_write()?;
6286 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6287 trigger.validate()?;
6288 self.validate_trigger_references(&trigger)
6289 .map_err(trigger_validation_error)?;
6290 let commit_lock = Arc::clone(&self.commit_lock);
6291 let _c = commit_lock.lock();
6292 let epoch = self.epoch.bump_assigned();
6293 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6294 let mut next_catalog = self.catalog.read().clone();
6295 let replaced = {
6296 let next = match next_catalog
6297 .triggers
6298 .iter()
6299 .position(|t| t.trigger.name == trigger.name)
6300 {
6301 Some(idx) => {
6302 let next = next_catalog.triggers[idx]
6303 .trigger
6304 .replaced(trigger.clone(), epoch.0)?;
6305 next_catalog.triggers[idx] = TriggerEntry::from(next.clone());
6306 next
6307 }
6308 None => {
6309 let mut next = trigger;
6310 next.created_epoch = epoch.0;
6311 next.updated_epoch = epoch.0;
6312 next_catalog.triggers.push(TriggerEntry::from(next.clone()));
6313 next
6314 }
6315 };
6316 next_catalog.db_epoch = epoch.0;
6317 next
6318 };
6319 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6320 Ok(replaced)
6321 }
6322
6323 pub fn drop_trigger(&self, name: &str) -> Result<()> {
6324 self.drop_trigger_with_epoch(name).map(|_| ())
6325 }
6326
6327 pub fn drop_trigger_with_epoch(&self, name: &str) -> Result<Epoch> {
6329 self.drop_triggers_with_epoch(&[name.to_string()])
6330 }
6331
6332 pub fn drop_trigger_with_epoch_controlled<F>(
6333 &self,
6334 name: &str,
6335 before_publish: F,
6336 ) -> Result<Epoch>
6337 where
6338 F: FnMut() -> Result<()>,
6339 {
6340 self.drop_triggers_with_epoch_controlled(&[name.to_string()], before_publish)
6341 }
6342
6343 pub fn drop_triggers_with_epoch(&self, names: &[String]) -> Result<Epoch> {
6345 self.drop_triggers_with_epoch_inner(names, None, None)
6346 }
6347
6348 pub fn drop_triggers_with_epoch_controlled<F>(
6349 &self,
6350 names: &[String],
6351 mut before_publish: F,
6352 ) -> Result<Epoch>
6353 where
6354 F: FnMut() -> Result<()>,
6355 {
6356 self.drop_triggers_with_epoch_inner(names, None, Some(&mut before_publish))
6357 }
6358
6359 pub fn drop_triggers_with_epoch_as_controlled<F>(
6360 &self,
6361 names: &[String],
6362 principal: Option<&crate::auth::Principal>,
6363 mut before_publish: F,
6364 ) -> Result<Epoch>
6365 where
6366 F: FnMut() -> Result<()>,
6367 {
6368 self.drop_triggers_with_epoch_inner(names, principal, Some(&mut before_publish))
6369 }
6370
6371 fn drop_triggers_with_epoch_inner(
6372 &self,
6373 names: &[String],
6374 principal: Option<&crate::auth::Principal>,
6375 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6376 ) -> Result<Epoch> {
6377 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6378 if names.is_empty() {
6379 return Err(MongrelError::InvalidArgument(
6380 "at least one trigger name is required".into(),
6381 ));
6382 }
6383 let _g = self.ddl_lock.lock();
6384 let _security_write = self.security_write()?;
6385 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6386 {
6387 let cat = self.catalog.read();
6388 for name in names {
6389 if !cat.triggers.iter().any(|t| t.trigger.name == *name) {
6390 return Err(MongrelError::NotFound(format!(
6391 "trigger {name:?} not found"
6392 )));
6393 }
6394 }
6395 }
6396 let commit_lock = Arc::clone(&self.commit_lock);
6397 let _c = commit_lock.lock();
6398 let epoch = self.epoch.bump_assigned();
6399 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6400 let mut next_catalog = self.catalog.read().clone();
6401 next_catalog
6402 .triggers
6403 .retain(|trigger| !names.contains(&trigger.trigger.name));
6404 next_catalog.db_epoch = epoch.0;
6405 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6406 Ok(epoch)
6407 }
6408
6409 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
6410 self.catalog.read().external_tables.clone()
6411 }
6412
6413 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
6414 self.catalog
6415 .read()
6416 .external_tables
6417 .iter()
6418 .find(|t| t.name == name)
6419 .cloned()
6420 }
6421
6422 pub fn create_external_table(&self, entry: ExternalTableEntry) -> Result<ExternalTableEntry> {
6423 self.create_external_table_inner(entry, None)
6424 }
6425
6426 pub fn create_external_table_controlled<F>(
6427 &self,
6428 entry: ExternalTableEntry,
6429 mut before_publish: F,
6430 ) -> Result<ExternalTableEntry>
6431 where
6432 F: FnMut() -> Result<()>,
6433 {
6434 self.create_external_table_inner(entry, Some(&mut before_publish))
6435 }
6436
6437 fn create_external_table_inner(
6438 &self,
6439 mut entry: ExternalTableEntry,
6440 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6441 ) -> Result<ExternalTableEntry> {
6442 self.require(&crate::auth::Permission::Ddl)?;
6443 let _g = self.ddl_lock.lock();
6444 let _security_write = self.security_write()?;
6445 self.require(&crate::auth::Permission::Ddl)?;
6446 entry.validate()?;
6447 {
6448 let cat = self.catalog.read();
6449 if cat.live(&entry.name).is_some()
6450 || cat.external_tables.iter().any(|t| t.name == entry.name)
6451 {
6452 return Err(MongrelError::InvalidArgument(format!(
6453 "table {:?} already exists",
6454 entry.name
6455 )));
6456 }
6457 }
6458 let commit_lock = Arc::clone(&self.commit_lock);
6459 let _c = commit_lock.lock();
6460 crate::durable_file::create_directory(&self.root.join(VTAB_DIR))?;
6464 crate::durable_file::remove_directory_all(&self.root.join(VTAB_DIR).join(&entry.name))?;
6465 let epoch = self.epoch.bump_assigned();
6466 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6467 entry.created_epoch = epoch.0;
6468 let mut next_catalog = self.catalog.read().clone();
6469 next_catalog.external_tables.push(entry.clone());
6470 next_catalog.db_epoch = epoch.0;
6471 self.publish_catalog_candidate_with_prelude(
6472 next_catalog,
6473 epoch,
6474 &mut _epoch_guard,
6475 before_publish,
6476 vec![(
6477 EXTERNAL_TABLE_ID,
6478 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
6479 name: entry.name.clone(),
6480 generation_epoch: epoch.0,
6481 }),
6482 )],
6483 )?;
6484 Ok(entry)
6485 }
6486
6487 pub fn drop_external_table(&self, name: &str) -> Result<()> {
6488 self.drop_external_table_with_epoch(name).map(|_| ())
6489 }
6490
6491 pub fn drop_external_table_with_epoch(&self, name: &str) -> Result<Epoch> {
6493 self.drop_external_table_with_epoch_inner(name, None)
6494 }
6495
6496 pub fn drop_external_table_with_epoch_controlled<F>(
6497 &self,
6498 name: &str,
6499 mut before_publish: F,
6500 ) -> Result<Epoch>
6501 where
6502 F: FnMut() -> Result<()>,
6503 {
6504 self.drop_external_table_with_epoch_inner(name, Some(&mut before_publish))
6505 }
6506
6507 fn drop_external_table_with_epoch_inner(
6508 &self,
6509 name: &str,
6510 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6511 ) -> Result<Epoch> {
6512 self.require(&crate::auth::Permission::Ddl)?;
6513 let _g = self.ddl_lock.lock();
6514 let _security_write = self.security_write()?;
6515 self.require(&crate::auth::Permission::Ddl)?;
6516 let commit_lock = Arc::clone(&self.commit_lock);
6517 let _c = commit_lock.lock();
6518 let epoch = self.epoch.bump_assigned();
6519 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6520 let mut next_catalog = self.catalog.read().clone();
6521 let before = next_catalog.external_tables.len();
6522 next_catalog.external_tables.retain(|t| t.name != name);
6523 if next_catalog.external_tables.len() == before {
6524 return Err(MongrelError::NotFound(format!(
6525 "external table {name:?} not found"
6526 )));
6527 }
6528 next_catalog.db_epoch = epoch.0;
6529 self.publish_catalog_candidate_with_prelude(
6530 next_catalog,
6531 epoch,
6532 &mut _epoch_guard,
6533 before_publish,
6534 vec![(
6535 EXTERNAL_TABLE_ID,
6536 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
6537 name: name.to_string(),
6538 generation_epoch: epoch.0,
6539 }),
6540 )],
6541 )?;
6542 let state_dir = self.root.join(VTAB_DIR).join(name);
6543 if let Err(error) = crate::durable_file::remove_directory_all(&state_dir) {
6544 return Err(MongrelError::DurableCommit {
6545 epoch: epoch.0,
6546 message: format!(
6547 "external table was dropped but connector-state cleanup failed: {error}"
6548 ),
6549 });
6550 }
6551 Ok(epoch)
6552 }
6553
6554 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
6555 let txn_id = self.alloc_txn_id()?;
6556 let (principal, catalog_bound) = self.transaction_principal_snapshot();
6557 self.commit_transaction_with_external_states(
6558 txn_id,
6559 self.epoch.visible(),
6560 Vec::new(),
6561 vec![(name.to_string(), state.to_vec())],
6562 Vec::new(),
6563 principal,
6564 catalog_bound,
6565 None,
6566 )
6567 .map(|(epoch, _)| epoch)
6568 }
6569
6570 pub fn trigger_config(&self) -> TriggerConfig {
6571 use std::sync::atomic::Ordering;
6572 TriggerConfig {
6573 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
6574 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
6575 max_loop_iterations: self.trigger_max_loop_iterations.load(Ordering::Relaxed),
6576 }
6577 }
6578
6579 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
6580 use std::sync::atomic::Ordering;
6581 if config.max_depth == 0 {
6582 return Err(MongrelError::InvalidArgument(
6583 "trigger max_depth must be greater than 0".into(),
6584 ));
6585 }
6586 self.trigger_recursive
6587 .store(config.recursive_triggers, Ordering::Relaxed);
6588 self.trigger_max_depth
6589 .store(config.max_depth, Ordering::Relaxed);
6590 self.trigger_max_loop_iterations
6591 .store(config.max_loop_iterations, Ordering::Relaxed);
6592 Ok(())
6593 }
6594
6595 pub fn set_recursive_triggers(&self, recursive: bool) {
6596 use std::sync::atomic::Ordering;
6597 self.trigger_recursive.store(recursive, Ordering::Relaxed);
6598 }
6599
6600 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent> {
6604 self.notify.subscribe()
6605 }
6606
6607 pub fn subscribe_change_commits(&self) -> tokio::sync::broadcast::Receiver<()> {
6608 self.change_wake.subscribe()
6609 }
6610
6611 pub fn change_events_since(&self, last_event_id: Option<&str>) -> Result<CdcBatch> {
6616 let control = crate::ExecutionControl::new(None);
6617 self.change_events_since_controlled(last_event_id, &control)
6618 }
6619
6620 pub fn change_events_since_controlled(
6622 &self,
6623 last_event_id: Option<&str>,
6624 control: &crate::ExecutionControl,
6625 ) -> Result<CdcBatch> {
6626 use crate::wal::Op;
6627
6628 control.checkpoint()?;
6629 let resume = match last_event_id {
6630 Some(id) => {
6631 let (epoch, index) = id.split_once(':').ok_or_else(|| {
6632 MongrelError::InvalidArgument(format!(
6633 "invalid CDC event id {id:?}; expected <epoch>:<index>"
6634 ))
6635 })?;
6636 Some((
6637 epoch.parse::<u64>().map_err(|error| {
6638 MongrelError::InvalidArgument(format!("invalid CDC epoch: {error}"))
6639 })?,
6640 index.parse::<u32>().map_err(|error| {
6641 MongrelError::InvalidArgument(format!("invalid CDC index: {error}"))
6642 })?,
6643 ))
6644 }
6645 None => None,
6646 };
6647
6648 let mut wal = self.shared_wal.lock();
6649 wal.group_sync()?;
6650 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
6651 let records = crate::wal::SharedWal::replay_with_dek_controlled(
6652 &self.root,
6653 wal_dek.as_ref(),
6654 control,
6655 CDC_MAX_WAL_RECORDS,
6656 CDC_MAX_WAL_REPLAY_BYTES,
6657 )?;
6658 drop(wal);
6659 control.checkpoint()?;
6660
6661 let mut commits: HashMap<u64, (u64, Vec<crate::wal::AddedRun>)> = HashMap::new();
6662 let mut spilled_payloads: HashMap<(u64, u64), Vec<&[u8]>> = HashMap::new();
6663 for (index, record) in records.iter().enumerate() {
6664 if index % 256 == 0 {
6665 control.checkpoint()?;
6666 }
6667 if let Op::TxnCommit { epoch, added_runs } = &record.op {
6668 commits.insert(record.txn_id, (*epoch, added_runs.clone()));
6669 }
6670 if let Op::SpilledRows { table_id, rows } = &record.op {
6671 spilled_payloads
6672 .entry((record.txn_id, *table_id))
6673 .or_default()
6674 .push(rows);
6675 }
6676 }
6677 let earliest_epoch = commits.values().map(|(epoch, _)| *epoch).min();
6678 let current_epoch = self.epoch.committed().0;
6679 let retention_floor = crate::replication::replication_wal_floor(&self.root)?;
6680 let gap = resume.is_some_and(|(epoch, _)| {
6681 retention_floor != 0 && epoch <= retention_floor && epoch <= current_epoch
6682 });
6683 if gap {
6684 return Ok(CdcBatch {
6685 events: Vec::new(),
6686 current_epoch,
6687 earliest_epoch,
6688 gap: true,
6689 });
6690 }
6691
6692 let table_names: HashMap<u64, String> = self
6693 .catalog
6694 .read()
6695 .tables
6696 .iter()
6697 .map(|entry| (entry.table_id, entry.name.clone()))
6698 .collect();
6699 let mut before_images: HashMap<(u64, u64, u64), crate::memtable::Row> = HashMap::new();
6700 let mut retained_bytes = 0_usize;
6701 for (index, record) in records.iter().enumerate() {
6702 if index % 256 == 0 {
6703 control.checkpoint()?;
6704 }
6705 if !commits.contains_key(&record.txn_id) {
6706 continue;
6707 }
6708 let Op::BeforeImage {
6709 table_id,
6710 row_id,
6711 row,
6712 } = &record.op
6713 else {
6714 continue;
6715 };
6716 if row.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
6717 return Err(MongrelError::ResourceLimitExceeded {
6718 resource: "CDC before-image bytes",
6719 requested: row.len(),
6720 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
6721 });
6722 }
6723 let before: crate::memtable::Row = bincode::deserialize(row)?;
6724 if before_images.len() >= CDC_MAX_ROWS {
6725 return Err(MongrelError::ResourceLimitExceeded {
6726 resource: "CDC before-image rows",
6727 requested: before_images.len().saturating_add(1),
6728 limit: CDC_MAX_ROWS,
6729 });
6730 }
6731 charge_cdc_bytes(
6732 &mut retained_bytes,
6733 cdc_row_storage_bytes(&before),
6734 "CDC retained bytes",
6735 )?;
6736 before_images.insert((record.txn_id, *table_id, row_id.0), before);
6737 }
6738 let mut operation_indices: HashMap<u64, u32> = HashMap::new();
6739 let mut events = Vec::new();
6740 let mut decoded_rows = before_images.len();
6741 for (record_index, record) in records.iter().enumerate() {
6742 if record_index % 256 == 0 {
6743 control.checkpoint()?;
6744 }
6745 let Some((commit_epoch, _)) = commits.get(&record.txn_id) else {
6746 continue;
6747 };
6748 let event = match &record.op {
6749 Op::Put { table_id, rows } => {
6750 if rows.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
6751 return Err(MongrelError::ResourceLimitExceeded {
6752 resource: "CDC inline row bytes",
6753 requested: rows.len(),
6754 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
6755 });
6756 }
6757 let rows: Vec<crate::memtable::Row> = bincode::deserialize(rows)?;
6758 decoded_rows = decoded_rows.saturating_add(rows.len());
6759 if decoded_rows > CDC_MAX_ROWS {
6760 return Err(MongrelError::ResourceLimitExceeded {
6761 resource: "CDC decoded rows",
6762 requested: decoded_rows,
6763 limit: CDC_MAX_ROWS,
6764 });
6765 }
6766 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(512);
6767 let mut peak_bytes = retained_bytes;
6768 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
6769 let data = serde_json::to_value(rows)
6770 .map_err(|error| MongrelError::Other(format!("CDC JSON: {error}")))?;
6771 Some((*table_id, "put", data, event_bytes))
6772 }
6773 Op::Delete { table_id, row_ids } => {
6774 let before = row_ids
6775 .iter()
6776 .filter_map(|row_id| {
6777 before_images
6778 .get(&(record.txn_id, *table_id, row_id.0))
6779 .cloned()
6780 })
6781 .collect::<Vec<_>>();
6782 let event_bytes = cdc_rows_json_bytes(&before)
6783 .saturating_add(
6784 row_ids
6785 .len()
6786 .saturating_mul(std::mem::size_of::<serde_json::Value>()),
6787 )
6788 .saturating_add(512);
6789 let mut peak_bytes = retained_bytes;
6790 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
6791 Some((
6792 *table_id,
6793 "delete",
6794 serde_json::json!({
6795 "row_ids": row_ids.iter().map(|row_id| row_id.0).collect::<Vec<_>>(),
6796 "before": before,
6797 }),
6798 event_bytes,
6799 ))
6800 }
6801 Op::TruncateTable { table_id } => {
6802 Some((*table_id, "truncate", serde_json::Value::Null, 512))
6803 }
6804 _ => None,
6805 };
6806 if let Some((table_id, op, data, event_bytes)) = event {
6807 let index = operation_indices.entry(record.txn_id).or_insert(0);
6808 let event_position = (*commit_epoch, *index);
6809 *index = index.saturating_add(1);
6810 if resume.is_some_and(|position| event_position <= position) {
6811 continue;
6812 }
6813 if events.len() >= CDC_MAX_EVENTS {
6814 return Err(MongrelError::ResourceLimitExceeded {
6815 resource: "CDC events",
6816 requested: events.len().saturating_add(1),
6817 limit: CDC_MAX_EVENTS,
6818 });
6819 }
6820 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
6821 events.push(ChangeEvent {
6822 id: Some(format!("{}:{}", event_position.0, event_position.1)),
6823 channel: "changes".into(),
6824 table_id: Some(table_id),
6825 table: table_names.get(&table_id).cloned().unwrap_or_default(),
6826 op: op.into(),
6827 epoch: *commit_epoch,
6828 txn_id: Some(record.txn_id),
6829 message: None,
6830 data: Some(data),
6831 });
6832 }
6833 if let Op::TxnCommit { added_runs, .. } = &record.op {
6834 for run in added_runs {
6835 control.checkpoint()?;
6836 let index = operation_indices.entry(record.txn_id).or_insert(0);
6837 let event_position = (*commit_epoch, *index);
6838 *index = index.saturating_add(1);
6839 if resume.is_some_and(|position| event_position <= position) {
6840 continue;
6841 }
6842 let mut rows = if let Some(payloads) =
6843 spilled_payloads.get(&(record.txn_id, run.table_id))
6844 {
6845 let mut rows = Vec::new();
6846 for payload in payloads {
6847 control.checkpoint()?;
6848 if payload.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
6849 return Err(MongrelError::ResourceLimitExceeded {
6850 resource: "CDC spilled row bytes",
6851 requested: payload.len(),
6852 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
6853 });
6854 }
6855 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(payload)?;
6856 if decoded_rows
6857 .saturating_add(rows.len())
6858 .saturating_add(chunk.len())
6859 > CDC_MAX_ROWS
6860 {
6861 return Err(MongrelError::ResourceLimitExceeded {
6862 resource: "CDC decoded rows",
6863 requested: decoded_rows
6864 .saturating_add(rows.len())
6865 .saturating_add(chunk.len()),
6866 limit: CDC_MAX_ROWS,
6867 });
6868 }
6869 rows.extend(chunk);
6870 }
6871 rows
6872 } else {
6873 let Some(handle) = self.tables.read().get(&run.table_id).cloned() else {
6874 return Ok(CdcBatch {
6875 events: Vec::new(),
6876 current_epoch,
6877 earliest_epoch,
6878 gap: true,
6879 });
6880 };
6881 let table = handle.lock();
6882 let mut reader = match table.open_reader(run.run_id) {
6883 Ok(reader) => reader,
6884 Err(_) => {
6885 return Ok(CdcBatch {
6886 events: Vec::new(),
6887 current_epoch,
6888 earliest_epoch,
6889 gap: true,
6890 })
6891 }
6892 };
6893 let remaining = CDC_MAX_ROWS.saturating_sub(decoded_rows);
6894 let rows = reader.all_rows_controlled(control, remaining)?;
6895 drop(reader);
6896 drop(table);
6897 rows
6898 };
6899 for row in &mut rows {
6900 row.committed_epoch = Epoch(*commit_epoch);
6901 }
6902 decoded_rows = decoded_rows.saturating_add(rows.len());
6903 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(768);
6904 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
6905 if events.len() >= CDC_MAX_EVENTS {
6906 return Err(MongrelError::ResourceLimitExceeded {
6907 resource: "CDC events",
6908 requested: events.len().saturating_add(1),
6909 limit: CDC_MAX_EVENTS,
6910 });
6911 }
6912 events.push(ChangeEvent {
6913 id: Some(format!("{}:{}", event_position.0, event_position.1)),
6914 channel: "changes".into(),
6915 table_id: Some(run.table_id),
6916 table: table_names.get(&run.table_id).cloned().unwrap_or_default(),
6917 op: "put_run".into(),
6918 epoch: *commit_epoch,
6919 txn_id: Some(record.txn_id),
6920 message: None,
6921 data: Some(serde_json::json!({
6922 "run_id": run.run_id.to_string(),
6923 "row_count": run.row_count,
6924 "min_row_id": run.min_row_id,
6925 "max_row_id": run.max_row_id,
6926 "rows": rows,
6927 })),
6928 });
6929 }
6930 }
6931 }
6932 control.checkpoint()?;
6933 Ok(CdcBatch {
6934 events,
6935 current_epoch,
6936 earliest_epoch,
6937 gap: false,
6938 })
6939 }
6940
6941 pub fn notify(&self, channel: &str, message: Option<String>) {
6944 let _ = self.notify.send(ChangeEvent {
6945 id: None,
6946 channel: channel.to_string(),
6947 table_id: None,
6948 table: String::new(),
6949 op: "notify".into(),
6950 epoch: self.epoch.visible().0,
6951 txn_id: None,
6952 message,
6953 data: None,
6954 });
6955 }
6956
6957 pub fn call_procedure(
6958 &self,
6959 name: &str,
6960 args: HashMap<String, crate::Value>,
6961 ) -> Result<ProcedureCallResult> {
6962 self.call_procedure_as(name, args, None)
6963 }
6964
6965 pub fn call_procedure_as(
6966 &self,
6967 name: &str,
6968 args: HashMap<String, crate::Value>,
6969 principal: Option<&crate::auth::Principal>,
6970 ) -> Result<ProcedureCallResult> {
6971 let control = crate::ExecutionControl::new(None);
6972 self.call_procedure_as_controlled(name, args, principal, &control, || true)
6973 }
6974
6975 #[doc(hidden)]
6978 pub fn call_procedure_as_bound(
6979 &self,
6980 expected: &StoredProcedure,
6981 args: HashMap<String, crate::Value>,
6982 principal: Option<&crate::auth::Principal>,
6983 ) -> Result<ProcedureCallResult> {
6984 self.require_for(principal, &crate::auth::Permission::All)?;
6985 let procedure = self.procedure(&expected.name).ok_or_else(|| {
6986 MongrelError::NotFound(format!("procedure {:?} not found", expected.name))
6987 })?;
6988 if &procedure != expected {
6989 return Err(MongrelError::Conflict(format!(
6990 "procedure {:?} changed after request authorization",
6991 expected.name
6992 )));
6993 }
6994 let control = crate::ExecutionControl::new(None);
6995 self.execute_procedure_as_controlled(procedure, args, principal, &control, || true)
6996 }
6997
6998 #[doc(hidden)]
7003 pub fn call_procedure_as_controlled<F>(
7004 &self,
7005 name: &str,
7006 args: HashMap<String, crate::Value>,
7007 principal: Option<&crate::auth::Principal>,
7008 control: &crate::ExecutionControl,
7009 before_commit: F,
7010 ) -> Result<ProcedureCallResult>
7011 where
7012 F: FnOnce() -> bool,
7013 {
7014 self.require_for(principal, &crate::auth::Permission::All)?;
7018 let procedure = self
7019 .procedure(name)
7020 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
7021 self.execute_procedure_as_controlled(procedure, args, principal, control, before_commit)
7022 }
7023
7024 fn execute_procedure_as_controlled<F>(
7025 &self,
7026 procedure: StoredProcedure,
7027 args: HashMap<String, crate::Value>,
7028 principal: Option<&crate::auth::Principal>,
7029 control: &crate::ExecutionControl,
7030 before_commit: F,
7031 ) -> Result<ProcedureCallResult>
7032 where
7033 F: FnOnce() -> bool,
7034 {
7035 let args = bind_procedure_args(&procedure, args)?;
7036 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
7037 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
7038 if has_writes {
7039 let mut tx = self.begin_as(principal.cloned());
7040 let run = (|| {
7041 for (step_index, step) in procedure.body.steps.iter().enumerate() {
7042 if step_index % 256 == 0 {
7043 control.checkpoint()?;
7044 }
7045 let output = self.execute_procedure_step(
7046 step,
7047 &args,
7048 &outputs,
7049 Some(&mut tx),
7050 principal,
7051 Some(control),
7052 )?;
7053 outputs.insert(step.id().to_string(), output);
7054 }
7055 control.checkpoint()?;
7056 eval_return_output(&procedure.body.return_value, &args, &outputs)
7057 })();
7058 match run {
7059 Ok(output) => {
7060 control.checkpoint()?;
7061 if !before_commit() {
7062 tx.rollback();
7063 return Err(MongrelError::Cancelled);
7064 }
7065 let epoch = tx.commit()?.0;
7066 Ok(ProcedureCallResult {
7067 epoch: Some(epoch),
7068 output,
7069 })
7070 }
7071 Err(e) => {
7072 tx.rollback();
7073 Err(e)
7074 }
7075 }
7076 } else {
7077 for (step_index, step) in procedure.body.steps.iter().enumerate() {
7078 if step_index % 256 == 0 {
7079 control.checkpoint()?;
7080 }
7081 let output = self.execute_procedure_step(
7082 step,
7083 &args,
7084 &outputs,
7085 None,
7086 principal,
7087 Some(control),
7088 )?;
7089 outputs.insert(step.id().to_string(), output);
7090 }
7091 control.checkpoint()?;
7092 Ok(ProcedureCallResult {
7093 epoch: None,
7094 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
7095 })
7096 }
7097 }
7098
7099 fn execute_procedure_step(
7100 &self,
7101 step: &ProcedureStep,
7102 args: &HashMap<String, crate::Value>,
7103 outputs: &HashMap<String, ProcedureCallOutput>,
7104 tx: Option<&mut crate::txn::Transaction<'_>>,
7105 principal: Option<&crate::auth::Principal>,
7106 control: Option<&crate::ExecutionControl>,
7107 ) -> Result<ProcedureCallOutput> {
7108 if let Some(control) = control {
7109 control.checkpoint()?;
7110 }
7111 match step {
7112 ProcedureStep::NativeQuery {
7113 table,
7114 conditions,
7115 projection,
7116 limit,
7117 ..
7118 } => {
7119 let mut q = crate::Query::new();
7120 for condition in conditions {
7121 q = q.and(eval_condition(condition, args, outputs)?);
7122 }
7123 let fallback_control = crate::ExecutionControl::new(None);
7124 let query_control = control.unwrap_or(&fallback_control);
7125 let mut rows = self.query_for_principal_controlled(
7126 table,
7127 &q,
7128 projection.as_deref(),
7129 principal,
7130 false,
7131 query_control,
7132 )?;
7133 if let Some(limit) = limit {
7134 rows.truncate(*limit);
7135 }
7136 let mut output = Vec::with_capacity(rows.len());
7137 for (row_index, row) in rows.into_iter().enumerate() {
7138 if row_index % 256 == 0 {
7139 if let Some(control) = control {
7140 control.checkpoint()?;
7141 }
7142 }
7143 output.push(ProcedureCallRow {
7144 row_id: Some(row.row_id),
7145 columns: row.columns,
7146 });
7147 }
7148 Ok(ProcedureCallOutput::Rows(output))
7149 }
7150 ProcedureStep::Put {
7151 table,
7152 cells,
7153 returning,
7154 ..
7155 } => {
7156 let tx = tx.ok_or_else(|| {
7157 MongrelError::InvalidArgument(
7158 "write procedure step requires a transaction".into(),
7159 )
7160 })?;
7161 let cells = eval_cells(cells, args, outputs)?;
7162 if *returning {
7163 let out = tx.put_returning(table, cells)?;
7164 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
7165 row_id: None,
7166 columns: out.row.columns.into_iter().collect(),
7167 }))
7168 } else {
7169 tx.put(table, cells)?;
7170 Ok(ProcedureCallOutput::Null)
7171 }
7172 }
7173 ProcedureStep::Upsert {
7174 table,
7175 cells,
7176 update_cells,
7177 returning,
7178 ..
7179 } => {
7180 let tx = tx.ok_or_else(|| {
7181 MongrelError::InvalidArgument(
7182 "write procedure step requires a transaction".into(),
7183 )
7184 })?;
7185 let cells = eval_cells(cells, args, outputs)?;
7186 let action = match update_cells {
7187 Some(update_cells) => {
7188 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
7189 }
7190 None => crate::UpsertAction::DoNothing,
7191 };
7192 let out = tx.upsert(table, cells, action)?;
7193 if *returning {
7194 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
7195 row_id: None,
7196 columns: out.row.columns.into_iter().collect(),
7197 }))
7198 } else {
7199 Ok(ProcedureCallOutput::Null)
7200 }
7201 }
7202 ProcedureStep::DeleteByPk { table, pk, .. } => {
7203 let tx = tx.ok_or_else(|| {
7204 MongrelError::InvalidArgument(
7205 "write procedure step requires a transaction".into(),
7206 )
7207 })?;
7208 let pk = eval_value(pk, args, outputs)?;
7209 let handle = self.table(table)?;
7210 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
7211 MongrelError::NotFound("procedure delete_by_pk target not found".into())
7212 })?;
7213 tx.delete(table, row_id)?;
7214 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
7215 }
7216 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
7217 "DeleteRows procedure step is not supported by the core executor yet".into(),
7218 )),
7219 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
7220 "SqlQuery procedure step must be executed by mongreldb-query".into(),
7221 )),
7222 }
7223 }
7224
7225 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
7226 let cat = self.catalog.read();
7227 for step in &procedure.body.steps {
7228 let Some(table_name) = step.table() else {
7229 continue;
7230 };
7231 let schema = &cat
7232 .live(table_name)
7233 .ok_or_else(|| {
7234 MongrelError::InvalidArgument(format!(
7235 "procedure {:?} references unknown table {table_name:?}",
7236 procedure.name
7237 ))
7238 })?
7239 .schema;
7240 match step {
7241 ProcedureStep::NativeQuery {
7242 conditions,
7243 projection,
7244 ..
7245 } => {
7246 for condition in conditions {
7247 validate_condition_columns(condition, schema)?;
7248 }
7249 if let Some(projection) = projection {
7250 for id in projection {
7251 validate_column_id(*id, schema)?;
7252 }
7253 }
7254 }
7255 ProcedureStep::Put { cells, .. } => {
7256 for cell in cells {
7257 validate_column_id(cell.column_id, schema)?;
7258 }
7259 }
7260 ProcedureStep::Upsert {
7261 cells,
7262 update_cells,
7263 ..
7264 } => {
7265 for cell in cells {
7266 validate_column_id(cell.column_id, schema)?;
7267 }
7268 if let Some(update_cells) = update_cells {
7269 for cell in update_cells {
7270 validate_column_id(cell.column_id, schema)?;
7271 }
7272 }
7273 }
7274 ProcedureStep::DeleteByPk { .. } => {
7275 if schema.primary_key().is_none() {
7276 return Err(MongrelError::InvalidArgument(format!(
7277 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
7278 procedure.name
7279 )));
7280 }
7281 }
7282 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
7283 }
7284 }
7285 Ok(())
7286 }
7287
7288 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
7289 let cat = self.catalog.read();
7290 let target_schema = match &trigger.target {
7291 TriggerTarget::Table(target_name) => cat
7292 .live(target_name)
7293 .ok_or_else(|| {
7294 MongrelError::InvalidArgument(format!(
7295 "trigger {:?} references unknown target table {target_name:?}",
7296 trigger.name
7297 ))
7298 })?
7299 .schema
7300 .clone(),
7301 TriggerTarget::View(_) => Schema {
7302 columns: trigger.target_columns.clone(),
7303 ..Schema::default()
7304 },
7305 };
7306 for col in &trigger.update_of {
7307 if target_schema.column(col).is_none() {
7308 return Err(MongrelError::InvalidArgument(format!(
7309 "trigger {:?} UPDATE OF references unknown column {col:?}",
7310 trigger.name
7311 )));
7312 }
7313 }
7314 if let Some(expr) = &trigger.when {
7315 validate_trigger_expr(expr, &target_schema, trigger.event)?;
7316 }
7317 let mut select_schemas: HashMap<String, &Schema> = HashMap::new();
7318 for step in &trigger.program.steps {
7319 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
7320 {
7321 return Err(MongrelError::InvalidArgument(
7322 "SetNew trigger steps are only valid in BEFORE triggers".into(),
7323 ));
7324 }
7325 validate_trigger_step(
7326 step,
7327 &cat,
7328 &target_schema,
7329 trigger.event,
7330 &mut select_schemas,
7331 )?;
7332 }
7333 Ok(())
7334 }
7335
7336 pub fn begin(&self) -> crate::txn::Transaction<'_> {
7338 self.begin_with_isolation(crate::txn::IsolationLevel::default())
7339 }
7340
7341 fn transaction_principal_snapshot(&self) -> (Option<crate::auth::Principal>, bool) {
7342 let principal = self.principal.read().clone();
7343 let catalog_bound = principal.as_ref().is_some_and(|principal| {
7344 let catalog = self.catalog.read();
7345 catalog.require_auth || principal.user_id != 0
7346 });
7347 (principal, catalog_bound)
7348 }
7349
7350 pub fn begin_as(
7351 &self,
7352 principal: Option<crate::auth::Principal>,
7353 ) -> crate::txn::Transaction<'_> {
7354 let catalog_bound = principal.as_ref().is_some_and(|principal| {
7355 let catalog = self.catalog.read();
7356 catalog.require_auth || principal.user_id != 0
7357 });
7358 let txn_id = self.alloc_txn_id();
7359 let read = Snapshot::at(self.epoch.visible());
7360 crate::txn::Transaction::new(self, txn_id, read).with_principal(principal, catalog_bound)
7361 }
7362
7363 pub fn begin_with_isolation(
7365 &self,
7366 level: crate::txn::IsolationLevel,
7367 ) -> crate::txn::Transaction<'_> {
7368 let txn_id = self.alloc_txn_id();
7369 let epoch = match level {
7370 crate::txn::IsolationLevel::ReadCommitted => self.epoch.visible(),
7371 _ => self.epoch.visible(),
7372 };
7373 let read = Snapshot::at(epoch);
7374 let (principal, catalog_bound) = self.transaction_principal_snapshot();
7375 crate::txn::Transaction::new(self, txn_id, read).with_principal(principal, catalog_bound)
7376 }
7377
7378 pub fn begin_with_external_trigger_bridge<'a>(
7381 &'a self,
7382 bridge: &'a dyn ExternalTriggerBridge,
7383 ) -> crate::txn::Transaction<'a> {
7384 let txn_id = self.alloc_txn_id();
7385 let read = Snapshot::at(self.epoch.visible());
7386 let (principal, catalog_bound) = self.transaction_principal_snapshot();
7387 crate::txn::Transaction::new(self, txn_id, read)
7388 .with_external_trigger_bridge(bridge)
7389 .with_principal(principal, catalog_bound)
7390 }
7391
7392 pub fn begin_with_external_trigger_bridge_as<'a>(
7393 &'a self,
7394 bridge: &'a dyn ExternalTriggerBridge,
7395 principal: Option<crate::auth::Principal>,
7396 ) -> crate::txn::Transaction<'a> {
7397 let catalog_bound = principal.as_ref().is_some_and(|principal| {
7398 let catalog = self.catalog.read();
7399 catalog.require_auth || principal.user_id != 0
7400 });
7401 let txn_id = self.alloc_txn_id();
7402 let read = Snapshot::at(self.epoch.visible());
7403 crate::txn::Transaction::new(self, txn_id, read)
7404 .with_external_trigger_bridge(bridge)
7405 .with_principal(principal, catalog_bound)
7406 }
7407
7408 pub fn transaction<T>(
7410 &self,
7411 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7412 ) -> Result<T> {
7413 let mut tx = self.begin();
7414 match f(&mut tx) {
7415 Ok(out) => {
7416 tx.commit()?;
7417 Ok(out)
7418 }
7419 Err(e) => {
7420 tx.rollback();
7421 Err(e)
7422 }
7423 }
7424 }
7425
7426 pub fn transaction_with_row_ids<T>(
7427 &self,
7428 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7429 ) -> Result<(T, Vec<RowId>)> {
7430 let mut tx = self.begin();
7431 match f(&mut tx) {
7432 Ok(output) => {
7433 let (_, row_ids) = tx.commit_with_row_ids()?;
7434 Ok((output, row_ids))
7435 }
7436 Err(error) => {
7437 tx.rollback();
7438 Err(error)
7439 }
7440 }
7441 }
7442
7443 pub fn transaction_for_current_principal<T>(
7444 &self,
7445 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7446 ) -> Result<T> {
7447 if self.principal.read().is_some() {
7448 self.refresh_principal()?;
7449 }
7450 let mut transaction = self.begin_as(self.principal.read().clone());
7451 match f(&mut transaction) {
7452 Ok(output) => {
7453 transaction.commit()?;
7454 Ok(output)
7455 }
7456 Err(error) => {
7457 transaction.rollback();
7458 Err(error)
7459 }
7460 }
7461 }
7462
7463 pub fn transaction_for_current_principal_with_epoch<T>(
7464 &self,
7465 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7466 ) -> Result<(Epoch, T)> {
7467 if self.principal.read().is_some() {
7468 self.refresh_principal()?;
7469 }
7470 let mut transaction = self.begin_as(self.principal.read().clone());
7471 match f(&mut transaction) {
7472 Ok(output) => {
7473 let epoch = transaction.commit()?;
7474 Ok((epoch, output))
7475 }
7476 Err(error) => {
7477 transaction.rollback();
7478 Err(error)
7479 }
7480 }
7481 }
7482
7483 pub fn transaction_with_row_ids_for_current_principal<T>(
7484 &self,
7485 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7486 ) -> Result<(T, Vec<RowId>)> {
7487 if self.principal.read().is_some() {
7488 self.refresh_principal()?;
7489 }
7490 let mut transaction = self.begin_as(self.principal.read().clone());
7491 match f(&mut transaction) {
7492 Ok(output) => {
7493 let (_, row_ids) = transaction.commit_with_row_ids()?;
7494 Ok((output, row_ids))
7495 }
7496 Err(error) => {
7497 transaction.rollback();
7498 Err(error)
7499 }
7500 }
7501 }
7502
7503 pub fn transaction_with_external_trigger_bridge<'a, T>(
7506 &'a self,
7507 bridge: &'a dyn ExternalTriggerBridge,
7508 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7509 ) -> Result<T> {
7510 let mut tx = self.begin_with_external_trigger_bridge(bridge);
7511 match f(&mut tx) {
7512 Ok(out) => {
7513 tx.commit()?;
7514 Ok(out)
7515 }
7516 Err(e) => {
7517 tx.rollback();
7518 Err(e)
7519 }
7520 }
7521 }
7522
7523 pub fn transaction_with_external_trigger_bridge_as<'a, T>(
7524 &'a self,
7525 bridge: &'a dyn ExternalTriggerBridge,
7526 principal: Option<crate::auth::Principal>,
7527 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7528 ) -> Result<T> {
7529 let mut tx = self.begin_with_external_trigger_bridge_as(bridge, principal);
7530 match f(&mut tx) {
7531 Ok(output) => {
7532 tx.commit()?;
7533 Ok(output)
7534 }
7535 Err(error) => {
7536 tx.rollback();
7537 Err(error)
7538 }
7539 }
7540 }
7541
7542 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
7545 self.active_txns.register(epoch)
7546 }
7547
7548 fn fill_auto_increment_for_staging(
7549 &self,
7550 staging: &mut [(u64, crate::txn::Staged)],
7551 control: Option<&crate::ExecutionControl>,
7552 ) -> Result<()> {
7553 let mut puts_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
7554 for (index, (table_id, staged)) in staging.iter().enumerate() {
7555 commit_prepare_checkpoint(control, index)?;
7556 if matches!(staged, crate::txn::Staged::Put(_)) {
7557 puts_by_table.entry(*table_id).or_default().push(index);
7558 }
7559 }
7560
7561 let tables = self.tables.read();
7562 for (table_index, (table_id, indexes)) in puts_by_table.into_iter().enumerate() {
7563 commit_prepare_checkpoint(control, table_index)?;
7564 if let Some(handle) = tables.get(&table_id) {
7565 #[cfg(test)]
7566 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
7567 let mut t = handle.lock();
7568 for (fill_index, index) in indexes.into_iter().enumerate() {
7569 commit_prepare_checkpoint(control, fill_index)?;
7570 if let crate::txn::Staged::Put(cells) = &mut staging[index].1 {
7571 t.fill_auto_inc(cells)?;
7572 }
7573 }
7574 }
7575 }
7576 Ok(())
7577 }
7578
7579 fn expand_table_triggers(
7580 &self,
7581 staging: &mut Vec<(u64, crate::txn::Staged)>,
7582 read_epoch: Epoch,
7583 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
7584 external_states: &mut Vec<(String, Vec<u8>)>,
7585 control: Option<&crate::ExecutionControl>,
7586 ) -> Result<()> {
7587 commit_prepare_checkpoint(control, 0)?;
7588 let mut external_writes = Vec::new();
7589 let config = self.trigger_config();
7590 if config.recursive_triggers {
7591 let chunk = std::mem::take(staging);
7592 let stacks = vec![Vec::new(); chunk.len()];
7593 *staging = self.expand_trigger_chunk(
7594 chunk,
7595 stacks,
7596 read_epoch,
7597 0,
7598 config.max_depth,
7599 &mut external_writes,
7600 &config,
7601 control,
7602 )?;
7603 self.apply_external_trigger_writes(
7604 external_writes,
7605 external_trigger_bridge,
7606 external_states,
7607 staging,
7608 control,
7609 )?;
7610 return Ok(());
7611 }
7612
7613 let mut expansion =
7614 self.expand_table_triggers_once(staging, read_epoch, None, &config, control)?;
7615 if !expansion.before.is_empty() {
7616 let mut final_staging = expansion.before;
7617 final_staging.extend(filter_ignored_staging(
7618 std::mem::take(staging),
7619 &expansion.ignored_indices,
7620 ));
7621 *staging = final_staging;
7622 } else if !expansion.ignored_indices.is_empty() {
7623 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
7624 }
7625 staging.append(&mut expansion.after);
7626 external_writes.append(&mut expansion.before_external);
7627 external_writes.append(&mut expansion.after_external);
7628 self.apply_external_trigger_writes(
7629 external_writes,
7630 external_trigger_bridge,
7631 external_states,
7632 staging,
7633 control,
7634 )?;
7635 Ok(())
7636 }
7637
7638 #[allow(clippy::too_many_arguments)]
7639 fn expand_trigger_chunk(
7640 &self,
7641 mut chunk: Vec<(u64, crate::txn::Staged)>,
7642 stacks: Vec<Vec<String>>,
7643 read_epoch: Epoch,
7644 depth: u32,
7645 max_depth: u32,
7646 external_writes: &mut Vec<ExternalTriggerWrite>,
7647 config: &TriggerConfig,
7648 control: Option<&crate::ExecutionControl>,
7649 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
7650 if chunk.is_empty() {
7651 return Ok(Vec::new());
7652 }
7653 commit_prepare_checkpoint(control, 0)?;
7654 self.fill_auto_increment_for_staging(&mut chunk, control)?;
7655 let expansion = self.expand_table_triggers_once(
7656 &mut chunk,
7657 read_epoch,
7658 Some(&stacks),
7659 config,
7660 control,
7661 )?;
7662 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
7663 let stack = expansion
7664 .before_stacks
7665 .first()
7666 .or_else(|| expansion.after_stacks.first())
7667 .cloned()
7668 .unwrap_or_default();
7669 return Err(MongrelError::TriggerValidation(format!(
7670 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
7671 Self::format_trigger_stack(&stack)
7672 )));
7673 }
7674
7675 let mut out = Vec::new();
7676 external_writes.extend(expansion.before_external);
7677 out.extend(self.expand_trigger_chunk(
7678 expansion.before,
7679 expansion.before_stacks,
7680 read_epoch,
7681 depth + 1,
7682 max_depth,
7683 external_writes,
7684 config,
7685 control,
7686 )?);
7687 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
7688 external_writes.extend(expansion.after_external);
7689 out.extend(self.expand_trigger_chunk(
7690 expansion.after,
7691 expansion.after_stacks,
7692 read_epoch,
7693 depth + 1,
7694 max_depth,
7695 external_writes,
7696 config,
7697 control,
7698 )?);
7699 Ok(out)
7700 }
7701
7702 fn apply_external_trigger_writes(
7703 &self,
7704 writes: Vec<ExternalTriggerWrite>,
7705 bridge: Option<&dyn ExternalTriggerBridge>,
7706 external_states: &mut Vec<(String, Vec<u8>)>,
7707 staging: &mut Vec<(u64, crate::txn::Staged)>,
7708 control: Option<&crate::ExecutionControl>,
7709 ) -> Result<()> {
7710 if writes.is_empty() {
7711 return Ok(());
7712 }
7713 let bridge = bridge.ok_or_else(|| {
7714 MongrelError::TriggerValidation(
7715 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
7716 )
7717 })?;
7718 for (write_index, write) in writes.into_iter().enumerate() {
7719 commit_prepare_checkpoint(control, write_index)?;
7720 let table = write.table().to_string();
7721 let entry = self.external_table(&table).ok_or_else(|| {
7722 MongrelError::NotFound(format!("external table {table:?} not found"))
7723 })?;
7724 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
7725 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
7726 external_states.push((table, result.state));
7727 for (base_index, base_write) in result.base_writes.into_iter().enumerate() {
7728 commit_prepare_checkpoint(control, base_index)?;
7729 match base_write {
7730 ExternalTriggerBaseWrite::Put { table, cells } => {
7731 let table_id = self.table_id(&table)?;
7732 staging.push((table_id, crate::txn::Staged::Put(cells)));
7733 }
7734 ExternalTriggerBaseWrite::Delete { table, row_id } => {
7735 let table_id = self.table_id(&table)?;
7736 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
7737 }
7738 }
7739 }
7740 }
7741 dedup_external_states_in_place(external_states);
7742 Ok(())
7743 }
7744
7745 fn expand_table_triggers_once(
7746 &self,
7747 staging: &mut Vec<(u64, crate::txn::Staged)>,
7748 read_epoch: Epoch,
7749 trigger_stacks: Option<&[Vec<String>]>,
7750 config: &TriggerConfig,
7751 control: Option<&crate::ExecutionControl>,
7752 ) -> Result<TriggerExpansion> {
7753 commit_prepare_checkpoint(control, 0)?;
7754 let triggers: Vec<StoredTrigger> = self
7755 .catalog
7756 .read()
7757 .triggers
7758 .iter()
7759 .filter(|entry| {
7760 entry.trigger.enabled
7761 && matches!(
7762 entry.trigger.timing,
7763 TriggerTiming::Before | TriggerTiming::After
7764 )
7765 && matches!(entry.trigger.target, TriggerTarget::Table(_))
7766 })
7767 .map(|entry| entry.trigger.clone())
7768 .collect();
7769 if triggers.is_empty() || staging.is_empty() {
7770 return Ok(TriggerExpansion::default());
7771 }
7772
7773 let before_triggers = triggers
7774 .iter()
7775 .filter(|trigger| trigger.timing == TriggerTiming::Before)
7776 .cloned()
7777 .collect::<Vec<_>>();
7778 let after_triggers = triggers
7779 .iter()
7780 .filter(|trigger| trigger.timing == TriggerTiming::After)
7781 .cloned()
7782 .collect::<Vec<_>>();
7783
7784 let mut before_added = Vec::new();
7785 let mut before_stacks = Vec::new();
7786 let mut before_external = Vec::new();
7787 let mut ignored_indices = std::collections::BTreeSet::new();
7788 if !before_triggers.is_empty() {
7789 let before_events =
7790 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?;
7791 let mut out = TriggerProgramOutput {
7792 added: &mut before_added,
7793 added_stacks: &mut before_stacks,
7794 added_external: &mut before_external,
7795 ignored_indices: &mut ignored_indices,
7796 };
7797 self.execute_triggers_for_events(
7798 &before_triggers,
7799 &before_events,
7800 Some(staging),
7801 &mut out,
7802 config,
7803 read_epoch,
7804 control,
7805 )?;
7806 }
7807
7808 let after_events = if after_triggers.is_empty() {
7809 Vec::new()
7810 } else {
7811 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?
7812 .into_iter()
7813 .filter(|event| {
7814 !event
7815 .op_indices
7816 .iter()
7817 .any(|idx| ignored_indices.contains(idx))
7818 })
7819 .collect()
7820 };
7821
7822 let mut after_added = Vec::new();
7823 let mut after_stacks = Vec::new();
7824 let mut after_external = Vec::new();
7825 let mut out = TriggerProgramOutput {
7826 added: &mut after_added,
7827 added_stacks: &mut after_stacks,
7828 added_external: &mut after_external,
7829 ignored_indices: &mut ignored_indices,
7830 };
7831 self.execute_triggers_for_events(
7832 &after_triggers,
7833 &after_events,
7834 None,
7835 &mut out,
7836 config,
7837 read_epoch,
7838 control,
7839 )?;
7840 Ok(TriggerExpansion {
7841 before: before_added,
7842 before_stacks,
7843 before_external,
7844 after: after_added,
7845 after_stacks,
7846 after_external,
7847 ignored_indices,
7848 })
7849 }
7850
7851 #[allow(clippy::too_many_arguments)]
7852 fn execute_triggers_for_events(
7853 &self,
7854 triggers: &[StoredTrigger],
7855 events: &[WriteEvent],
7856 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
7857 out: &mut TriggerProgramOutput<'_>,
7858 config: &TriggerConfig,
7859 read_epoch: Epoch,
7860 control: Option<&crate::ExecutionControl>,
7861 ) -> Result<()> {
7862 let mut checkpoint_index = 0_usize;
7863 for event in events {
7864 for trigger in triggers {
7865 commit_prepare_checkpoint(control, checkpoint_index)?;
7866 checkpoint_index += 1;
7867 if event
7868 .op_indices
7869 .iter()
7870 .any(|idx| out.ignored_indices.contains(idx))
7871 {
7872 break;
7873 }
7874 let matches = {
7875 let cat = self.catalog.read();
7876 trigger_matches_event(trigger, event, &cat)?
7877 };
7878 if !matches {
7879 continue;
7880 }
7881 if let Some(when) = &trigger.when {
7882 if !eval_trigger_expr(when, event)? {
7883 continue;
7884 }
7885 }
7886 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
7887 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
7888 return Err(MongrelError::TriggerValidation(format!(
7889 "trigger recursion cycle detected; trigger stack: {}",
7890 Self::format_trigger_stack(&trigger_stack)
7891 )));
7892 }
7893 let outcome = match staging.as_mut() {
7894 Some(staging) => self.execute_trigger_program(
7895 trigger,
7896 event,
7897 Some(&mut **staging),
7898 out,
7899 &trigger_stack,
7900 config,
7901 read_epoch,
7902 control,
7903 )?,
7904 None => self.execute_trigger_program(
7905 trigger,
7906 event,
7907 None,
7908 out,
7909 &trigger_stack,
7910 config,
7911 read_epoch,
7912 control,
7913 )?,
7914 };
7915 if outcome == TriggerProgramOutcome::Ignore {
7916 out.ignored_indices.extend(event.op_indices.iter().copied());
7917 break;
7918 }
7919 }
7920 }
7921 Ok(())
7922 }
7923
7924 fn trigger_events_for_staging(
7925 &self,
7926 staging: &[(u64, crate::txn::Staged)],
7927 read_epoch: Epoch,
7928 trigger_stacks: Option<&[Vec<String>]>,
7929 control: Option<&crate::ExecutionControl>,
7930 ) -> Result<Vec<WriteEvent>> {
7931 use crate::txn::Staged;
7932 use std::collections::{HashMap, VecDeque};
7933
7934 let snapshot = Snapshot::at(read_epoch);
7935 let cat = self.catalog.read();
7936 let mut table_names = HashMap::new();
7937 let mut table_schemas = HashMap::new();
7938 for entry in cat
7939 .tables
7940 .iter()
7941 .filter(|entry| matches!(entry.state, TableState::Live))
7942 {
7943 table_names.insert(entry.table_id, entry.name.clone());
7944 table_schemas.insert(entry.table_id, entry.schema.clone());
7945 }
7946 drop(cat);
7947
7948 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
7949 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
7950 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
7951
7952 for (idx, (table_id, staged)) in staging.iter().enumerate() {
7953 commit_prepare_checkpoint(control, idx)?;
7954 let Some(schema) = table_schemas.get(table_id) else {
7955 continue;
7956 };
7957 let Some(pk) = schema.primary_key() else {
7958 continue;
7959 };
7960 match staged {
7961 Staged::Delete(row_id) => {
7962 let handle = self.table_by_id(*table_id)?;
7963 let Some(row) = handle.lock().get(*row_id, snapshot) else {
7964 continue;
7965 };
7966 let Some(pk_value) = row.columns.get(&pk.id) else {
7967 continue;
7968 };
7969 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
7970 delete_by_key
7971 .entry((*table_id, pk_value.encode_key()))
7972 .or_default()
7973 .push_back(idx);
7974 }
7975 Staged::Put(cells) => {
7976 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
7977 put_by_key
7978 .entry((*table_id, value.encode_key()))
7979 .or_default()
7980 .push_back(idx);
7981 }
7982 }
7983 Staged::Update { row_id, .. } => {
7984 let handle = self.table_by_id(*table_id)?;
7985 let row = handle.lock().get(*row_id, snapshot);
7986 if let Some(row) = row {
7987 old_rows.insert(idx, TriggerRowImage::from_row(row));
7988 }
7989 }
7990 Staged::Truncate => {}
7991 }
7992 }
7993
7994 let mut paired_delete = std::collections::HashSet::new();
7995 let mut paired_put = std::collections::HashSet::new();
7996 let mut events = Vec::new();
7997
7998 for (pair_index, (key, deletes)) in delete_by_key.iter_mut().enumerate() {
7999 commit_prepare_checkpoint(control, pair_index)?;
8000 let Some(puts) = put_by_key.get_mut(key) else {
8001 continue;
8002 };
8003 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
8004 paired_delete.insert(delete_idx);
8005 paired_put.insert(put_idx);
8006 let (table_id, _) = &staging[put_idx];
8007 let Some(table_name) = table_names.get(table_id).cloned() else {
8008 continue;
8009 };
8010 let old = old_rows.get(&delete_idx).cloned();
8011 let new = match &staging[put_idx].1 {
8012 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
8013 _ => None,
8014 };
8015 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
8016 events.push(WriteEvent {
8017 table: table_name,
8018 kind: TriggerEvent::Update,
8019 old,
8020 new,
8021 changed_columns,
8022 op_indices: vec![delete_idx, put_idx],
8023 put_idx: Some(put_idx),
8024 trigger_stack: Self::trigger_stack_for_indices(
8025 trigger_stacks,
8026 &[delete_idx, put_idx],
8027 ),
8028 });
8029 }
8030 }
8031
8032 for (idx, (table_id, staged)) in staging.iter().enumerate() {
8033 commit_prepare_checkpoint(control, idx)?;
8034 let Some(table_name) = table_names.get(table_id).cloned() else {
8035 continue;
8036 };
8037 match staged {
8038 Staged::Put(cells) if !paired_put.contains(&idx) => {
8039 let new = Some(TriggerRowImage::from_cells(cells));
8040 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
8041 events.push(WriteEvent {
8042 table: table_name,
8043 kind: TriggerEvent::Insert,
8044 old: None,
8045 new,
8046 changed_columns,
8047 op_indices: vec![idx],
8048 put_idx: Some(idx),
8049 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
8050 });
8051 }
8052 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
8053 let old = match old_rows.get(&idx).cloned() {
8054 Some(old) => Some(old),
8055 None => {
8056 let handle = self.table_by_id(*table_id)?;
8057 let row = handle.lock().get(*row_id, snapshot);
8058 row.map(TriggerRowImage::from_row)
8059 }
8060 };
8061 let Some(old) = old else {
8062 continue;
8063 };
8064 let changed_columns = old.columns.keys().copied().collect();
8065 events.push(WriteEvent {
8066 table: table_name,
8067 kind: TriggerEvent::Delete,
8068 old: Some(old),
8069 new: None,
8070 changed_columns,
8071 op_indices: vec![idx],
8072 put_idx: None,
8073 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
8074 });
8075 }
8076 Staged::Update { new_row: cells, .. } => {
8077 let old = old_rows.get(&idx).cloned();
8078 let new = Some(TriggerRowImage::from_cells(cells));
8079 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
8080 events.push(WriteEvent {
8081 table: table_name,
8082 kind: TriggerEvent::Update,
8083 old,
8084 new,
8085 changed_columns,
8086 op_indices: vec![idx],
8087 put_idx: Some(idx),
8088 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
8089 });
8090 }
8091 Staged::Truncate => {}
8092 _ => {}
8093 }
8094 }
8095
8096 Ok(events)
8097 }
8098
8099 #[allow(clippy::too_many_arguments)]
8100 fn execute_trigger_program(
8101 &self,
8102 trigger: &StoredTrigger,
8103 event: &WriteEvent,
8104 staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
8105 out: &mut TriggerProgramOutput<'_>,
8106 trigger_stack: &[String],
8107 config: &TriggerConfig,
8108 read_epoch: Epoch,
8109 control: Option<&crate::ExecutionControl>,
8110 ) -> Result<TriggerProgramOutcome> {
8111 let mut event = event.clone();
8112 let mut select_results: HashMap<String, Vec<TriggerRowImage>> = HashMap::new();
8113 self.execute_trigger_steps(
8114 trigger,
8115 &trigger.program.steps,
8116 &mut event,
8117 staging,
8118 out,
8119 trigger_stack,
8120 config,
8121 &mut select_results,
8122 0,
8123 None,
8124 read_epoch,
8125 control,
8126 )
8127 }
8128
8129 #[allow(clippy::too_many_arguments)]
8130 fn execute_trigger_steps(
8131 &self,
8132 trigger: &StoredTrigger,
8133 steps: &[TriggerStep],
8134 event: &mut WriteEvent,
8135 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
8136 out: &mut TriggerProgramOutput<'_>,
8137 trigger_stack: &[String],
8138 config: &TriggerConfig,
8139 select_results: &mut HashMap<String, Vec<TriggerRowImage>>,
8140 depth: u32,
8141 selected: Option<&TriggerRowImage>,
8142 read_epoch: Epoch,
8143 control: Option<&crate::ExecutionControl>,
8144 ) -> Result<TriggerProgramOutcome> {
8145 let _ = depth;
8146 for (step_index, step) in steps.iter().enumerate() {
8147 commit_prepare_checkpoint(control, step_index)?;
8148 match step {
8149 TriggerStep::SetNew { cells } => {
8150 if trigger.timing != TriggerTiming::Before {
8151 return Err(MongrelError::InvalidArgument(
8152 "SetNew trigger step is only valid in BEFORE triggers".into(),
8153 ));
8154 }
8155 let put_idx = event.put_idx.ok_or_else(|| {
8156 MongrelError::InvalidArgument(
8157 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
8158 )
8159 })?;
8160 let staging = staging.as_deref_mut().ok_or_else(|| {
8161 MongrelError::InvalidArgument(
8162 "SetNew trigger step requires mutable trigger staging".into(),
8163 )
8164 })?;
8165 let mut update_changed_columns = None;
8166 let row_cells = match staging.get_mut(put_idx).map(|(_, op)| op) {
8167 Some(crate::txn::Staged::Put(cells)) => cells,
8168 Some(crate::txn::Staged::Update {
8169 new_row,
8170 changed_columns,
8171 ..
8172 }) => {
8173 update_changed_columns = Some(changed_columns);
8174 new_row
8175 }
8176 _ => {
8177 return Err(MongrelError::InvalidArgument(
8178 "SetNew trigger step target row is not mutable".into(),
8179 ))
8180 }
8181 };
8182 for (column_id, value) in eval_trigger_cells(cells, event, selected)? {
8183 row_cells.retain(|(id, _)| *id != column_id);
8184 row_cells.push((column_id, value.clone()));
8185 if let Some(changed_columns) = &mut update_changed_columns {
8186 changed_columns.push(column_id);
8187 }
8188 if let Some(new) = &mut event.new {
8189 new.columns.insert(column_id, value);
8190 }
8191 }
8192 row_cells.sort_by_key(|(id, _)| *id);
8193 if let Some(changed_columns) = update_changed_columns {
8194 changed_columns.sort_unstable();
8195 changed_columns.dedup();
8196 }
8197 }
8198 TriggerStep::Insert { table, cells } => {
8199 let cells = eval_trigger_cells(cells, event, selected)?;
8200 if let Ok(table_id) = self.table_id(table) {
8201 out.added.push((table_id, crate::txn::Staged::Put(cells)));
8202 out.added_stacks.push(trigger_stack.to_vec());
8203 } else if self.external_table(table).is_some() {
8204 out.added_external.push(ExternalTriggerWrite::Insert {
8205 table: table.clone(),
8206 cells,
8207 });
8208 } else {
8209 return Err(MongrelError::NotFound(format!(
8210 "trigger {:?} insert target {table:?} not found",
8211 trigger.name
8212 )));
8213 }
8214 }
8215 TriggerStep::UpdateByPk { table, pk, cells } => {
8216 let pk = eval_trigger_value(pk, event, selected)?;
8217 let cells = eval_trigger_cells(cells, event, selected)?;
8218 if self.external_table(table).is_some() {
8219 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
8220 table: table.clone(),
8221 pk,
8222 cells,
8223 });
8224 } else {
8225 let row_id = self
8226 .table(table)?
8227 .lock()
8228 .lookup_pk(&pk.encode_key())
8229 .ok_or_else(|| {
8230 MongrelError::NotFound(format!(
8231 "trigger {:?} update target not found",
8232 trigger.name
8233 ))
8234 })?;
8235 let handle = self.table(table)?;
8236 let snapshot = Snapshot::at(self.epoch.visible());
8237 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
8238 MongrelError::NotFound(format!(
8239 "trigger {:?} update target not visible",
8240 trigger.name
8241 ))
8242 })?;
8243 let mut changed_columns = cells
8244 .iter()
8245 .map(|(column_id, _)| *column_id)
8246 .collect::<Vec<_>>();
8247 changed_columns.sort_unstable();
8248 changed_columns.dedup();
8249 let mut merged = old.columns;
8250 for (column_id, value) in cells {
8251 merged.insert(column_id, value);
8252 }
8253 out.added.push((
8254 self.table_id(table)?,
8255 crate::txn::Staged::Update {
8256 row_id,
8257 new_row: merged.into_iter().collect(),
8258 changed_columns,
8259 },
8260 ));
8261 out.added_stacks.push(trigger_stack.to_vec());
8262 }
8263 }
8264 TriggerStep::DeleteByPk { table, pk } => {
8265 let pk = eval_trigger_value(pk, event, selected)?;
8266 if self.external_table(table).is_some() {
8267 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
8268 table: table.clone(),
8269 pk,
8270 });
8271 } else {
8272 let row_id = self
8273 .table(table)?
8274 .lock()
8275 .lookup_pk(&pk.encode_key())
8276 .ok_or_else(|| {
8277 MongrelError::NotFound(format!(
8278 "trigger {:?} delete target not found",
8279 trigger.name
8280 ))
8281 })?;
8282 out.added
8283 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
8284 out.added_stacks.push(trigger_stack.to_vec());
8285 }
8286 }
8287 TriggerStep::Select {
8288 id,
8289 table,
8290 conditions,
8291 } => {
8292 let schema = self.table(table)?.lock().schema().clone();
8293 let snapshot = Snapshot::at(read_epoch);
8294 let handle = self.table(table)?;
8295 let rows = match control {
8296 Some(control) => {
8297 handle.lock().visible_rows_controlled(snapshot, control)?
8298 }
8299 None => handle.lock().visible_rows(snapshot)?,
8300 };
8301 let mut matched = Vec::new();
8302 for (row_index, row) in rows.into_iter().enumerate() {
8303 commit_prepare_checkpoint(control, row_index)?;
8304 let image = TriggerRowImage::from_row(row);
8305 let passes = conditions
8306 .iter()
8307 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
8308 .collect::<Result<Vec<_>>>()?
8309 .into_iter()
8310 .all(|b| b);
8311 if passes {
8312 matched.push(image);
8313 }
8314 }
8315 if let Some(pk) = schema.primary_key() {
8316 matched.sort_by(|a, b| {
8317 let av = a.columns.get(&pk.id).unwrap_or(&Value::Null);
8318 let bv = b.columns.get(&pk.id).unwrap_or(&Value::Null);
8319 value_order(av, bv).unwrap_or(std::cmp::Ordering::Equal)
8320 });
8321 }
8322 select_results.insert(id.clone(), matched);
8323 }
8324 TriggerStep::Foreach { id, steps } => {
8325 let rows = select_results.get(id).ok_or_else(|| {
8326 MongrelError::InvalidArgument(format!(
8327 "trigger {:?} foreach references unknown select id {id:?}",
8328 trigger.name
8329 ))
8330 })?;
8331 if rows.len() > config.max_loop_iterations as usize {
8332 return Err(MongrelError::InvalidArgument(format!(
8333 "trigger {:?} foreach exceeded max_loop_iterations ({})",
8334 trigger.name, config.max_loop_iterations
8335 )));
8336 }
8337 for (row_index, row) in rows.clone().into_iter().enumerate() {
8338 commit_prepare_checkpoint(control, row_index)?;
8339 let result = self.execute_trigger_steps(
8340 trigger,
8341 steps,
8342 event,
8343 staging.as_deref_mut(),
8344 out,
8345 trigger_stack,
8346 config,
8347 select_results,
8348 depth + 1,
8349 Some(&row),
8350 read_epoch,
8351 control,
8352 )?;
8353 if result == TriggerProgramOutcome::Ignore {
8354 return Ok(TriggerProgramOutcome::Ignore);
8355 }
8356 }
8357 }
8358 TriggerStep::DeleteWhere { table, conditions } => {
8359 let schema = self.table(table)?.lock().schema().clone();
8360 let snapshot = Snapshot::at(read_epoch);
8361 let handle = self.table(table)?;
8362 let rows = match control {
8363 Some(control) => {
8364 handle.lock().visible_rows_controlled(snapshot, control)?
8365 }
8366 None => handle.lock().visible_rows(snapshot)?,
8367 };
8368 let table_id = self.table_id(table)?;
8369 let mut to_delete = Vec::new();
8370 for (row_index, row) in rows.into_iter().enumerate() {
8371 commit_prepare_checkpoint(control, row_index)?;
8372 let image = TriggerRowImage::from_row(row.clone());
8373 let passes = conditions
8374 .iter()
8375 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
8376 .collect::<Result<Vec<_>>>()?
8377 .into_iter()
8378 .all(|b| b);
8379 if passes {
8380 to_delete.push((table_id, row.row_id));
8381 }
8382 }
8383 for (row_index, (table_id, row_id)) in to_delete.into_iter().enumerate() {
8384 commit_prepare_checkpoint(control, row_index)?;
8385 out.added
8386 .push((table_id, crate::txn::Staged::Delete(row_id)));
8387 out.added_stacks.push(trigger_stack.to_vec());
8388 }
8389 }
8390 TriggerStep::UpdateWhere {
8391 table,
8392 conditions,
8393 cells,
8394 } => {
8395 let schema = self.table(table)?.lock().schema().clone();
8396 let snapshot = Snapshot::at(read_epoch);
8397 let handle = self.table(table)?;
8398 let rows = match control {
8399 Some(control) => {
8400 handle.lock().visible_rows_controlled(snapshot, control)?
8401 }
8402 None => handle.lock().visible_rows(snapshot)?,
8403 };
8404 let table_id = self.table_id(table)?;
8405 let mut changed_columns =
8406 cells.iter().map(|cell| cell.column_id).collect::<Vec<_>>();
8407 changed_columns.sort_unstable();
8408 changed_columns.dedup();
8409 let mut to_update = Vec::new();
8410 for (row_index, row) in rows.into_iter().enumerate() {
8411 commit_prepare_checkpoint(control, row_index)?;
8412 let image = TriggerRowImage::from_row(row.clone());
8413 let passes = conditions
8414 .iter()
8415 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
8416 .collect::<Result<Vec<_>>>()?
8417 .into_iter()
8418 .all(|b| b);
8419 if passes {
8420 let new_cells = cells
8421 .iter()
8422 .map(|cell| {
8423 Ok((
8424 cell.column_id,
8425 eval_trigger_value(&cell.value, event, Some(&image))?,
8426 ))
8427 })
8428 .collect::<Result<Vec<_>>>()?;
8429 let mut merged = row.columns.clone();
8430 for (column_id, value) in new_cells {
8431 merged.insert(column_id, value);
8432 }
8433 to_update.push((table_id, row.row_id, merged));
8434 }
8435 }
8436 for (row_index, (table_id, row_id, merged)) in to_update.into_iter().enumerate()
8437 {
8438 commit_prepare_checkpoint(control, row_index)?;
8439 out.added.push((
8440 table_id,
8441 crate::txn::Staged::Update {
8442 row_id,
8443 new_row: merged.into_iter().collect(),
8444 changed_columns: changed_columns.clone(),
8445 },
8446 ));
8447 out.added_stacks.push(trigger_stack.to_vec());
8448 }
8449 }
8450 TriggerStep::Raise { action, message } => match action {
8451 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
8452 TriggerRaiseAction::Abort
8453 | TriggerRaiseAction::Fail
8454 | TriggerRaiseAction::Rollback => {
8455 let message = eval_trigger_value(message, event, selected)?;
8456 return Err(MongrelError::TriggerValidation(format!(
8457 "trigger {:?} raised: {}; trigger stack: {}",
8458 trigger.name,
8459 trigger_message(message),
8460 Self::format_trigger_stack(trigger_stack)
8461 )));
8462 }
8463 },
8464 }
8465 }
8466 Ok(TriggerProgramOutcome::Continue)
8467 }
8468
8469 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
8470 let Some(stacks) = stacks else {
8471 return Vec::new();
8472 };
8473 let mut out = Vec::new();
8474 for idx in indices {
8475 let Some(stack) = stacks.get(*idx) else {
8476 continue;
8477 };
8478 for name in stack {
8479 if !out.iter().any(|existing| existing == name) {
8480 out.push(name.clone());
8481 }
8482 }
8483 }
8484 out
8485 }
8486
8487 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
8488 let mut out = stack.to_vec();
8489 out.push(trigger_name.to_string());
8490 out
8491 }
8492
8493 fn format_trigger_stack(stack: &[String]) -> String {
8494 if stack.is_empty() {
8495 "<root>".into()
8496 } else {
8497 stack.join(" -> ")
8498 }
8499 }
8500
8501 fn validate_constraints(
8516 &self,
8517 staging: &mut Vec<(u64, crate::txn::Staged)>,
8518 read_epoch: Epoch,
8519 control: Option<&crate::ExecutionControl>,
8520 ) -> Result<()> {
8521 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
8522 use crate::memtable::Row;
8523 use crate::txn::Staged;
8524 use std::collections::HashSet;
8525
8526 commit_prepare_checkpoint(control, 0)?;
8527 let snapshot = Snapshot::at(read_epoch);
8528 let cat = self.catalog.read();
8529
8530 let live: Vec<(u64, &str, &crate::schema::Schema)> = cat
8532 .tables
8533 .iter()
8534 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
8535 .map(|e| (e.table_id, e.name.as_str(), &e.schema))
8536 .collect();
8537
8538 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
8540 if !any_constraints {
8541 return Ok(());
8542 }
8543
8544 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
8546 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
8547 if let Some(r) = rows_cache.get(&table_id) {
8548 return Ok(r.clone());
8549 }
8550 let handle = self.table_by_id(table_id)?;
8551 let rows = match control {
8552 Some(control) => handle.lock().visible_rows_controlled(snapshot, control)?,
8553 None => handle.lock().visible_rows(snapshot)?,
8554 };
8555 rows_cache.insert(table_id, rows.clone());
8556 Ok(rows)
8557 };
8558
8559 let mut processed_updates = HashSet::new();
8564 type PendingUpdate = (usize, u64, crate::rowid::RowId, Vec<(u16, Value)>);
8565 let mut update_pass = 0_usize;
8566 loop {
8567 commit_prepare_checkpoint(control, update_pass)?;
8568 update_pass += 1;
8569 let updates: Vec<PendingUpdate> = staging
8570 .iter()
8571 .enumerate()
8572 .filter_map(|(index, (table_id, op))| match op {
8573 Staged::Update {
8574 row_id,
8575 new_row: cells,
8576 ..
8577 } if !processed_updates.contains(&index) => {
8578 Some((index, *table_id, *row_id, cells.clone()))
8579 }
8580 _ => None,
8581 })
8582 .collect();
8583 if updates.is_empty() {
8584 break;
8585 }
8586 let mut new_ops = Vec::new();
8587 for (update_index, (index, table_id, row_id, new_cells)) in
8588 updates.into_iter().enumerate()
8589 {
8590 commit_prepare_checkpoint(control, update_index)?;
8591 processed_updates.insert(index);
8592 let Some(tname) = live
8593 .iter()
8594 .find(|(id, _, _)| *id == table_id)
8595 .map(|(_, name, _)| *name)
8596 else {
8597 continue;
8598 };
8599 let Some(old_row) = self.table_by_id(table_id)?.lock().get(row_id, snapshot) else {
8600 continue;
8601 };
8602 let new_map: HashMap<u16, Value> = new_cells.iter().cloned().collect();
8603 for (child_id, _child_name, child_schema) in &live {
8604 for fk in &child_schema.constraints.foreign_keys {
8605 if fk.ref_table != tname {
8606 continue;
8607 }
8608 let Some(old_key) = encode_composite_key(&fk.ref_columns, &old_row.columns)
8609 else {
8610 continue;
8611 };
8612 if encode_composite_key(&fk.ref_columns, &new_map).as_deref()
8613 == Some(old_key.as_slice())
8614 {
8615 continue;
8616 }
8617 if fk.on_update == FkAction::Restrict {
8618 continue;
8619 }
8620 let child_rows = load_rows(*child_id)?;
8621 for (child_index, child) in child_rows.into_iter().enumerate() {
8622 commit_prepare_checkpoint(control, child_index)?;
8623 if encode_composite_key(&fk.columns, &child.columns).as_deref()
8624 != Some(old_key.as_slice())
8625 {
8626 continue;
8627 }
8628 if staging.iter().any(|(id, op)| {
8629 *id == *child_id
8630 && matches!(op, Staged::Delete(id) if *id == child.row_id)
8631 }) {
8632 continue;
8633 }
8634 let mut cells: Vec<(u16, Value)> = child
8635 .columns
8636 .iter()
8637 .map(|(column_id, value)| (*column_id, value.clone()))
8638 .collect();
8639 for (child_column, parent_column) in
8640 fk.columns.iter().zip(&fk.ref_columns)
8641 {
8642 cells.retain(|(column_id, _)| column_id != child_column);
8643 let value = match fk.on_update {
8644 FkAction::Cascade => {
8645 new_map.get(parent_column).cloned().unwrap_or(Value::Null)
8646 }
8647 FkAction::SetNull => Value::Null,
8648 FkAction::Restrict => {
8649 return Err(MongrelError::Other(
8650 "restricted foreign-key update reached cascade preparation"
8651 .into(),
8652 ));
8653 }
8654 };
8655 cells.push((*child_column, value));
8656 }
8657 cells.sort_by_key(|(column_id, _)| *column_id);
8658 if let Some(existing_index) = staging.iter().position(|(id, op)| {
8659 *id == *child_id
8660 && matches!(op, Staged::Update { row_id, .. } if *row_id == child.row_id)
8661 }) {
8662 if let Staged::Update {
8663 new_row: existing,
8664 changed_columns,
8665 ..
8666 } = &mut staging[existing_index].1 {
8667 changed_columns.extend(fk.columns.iter().copied());
8668 changed_columns.sort_unstable();
8669 changed_columns.dedup();
8670 if *existing != cells {
8671 *existing = cells;
8672 processed_updates.remove(&existing_index);
8673 }
8674 }
8675 } else {
8676 new_ops.push((
8677 *child_id,
8678 Staged::Update {
8679 row_id: child.row_id,
8680 new_row: cells,
8681 changed_columns: fk.columns.clone(),
8682 },
8683 ));
8684 }
8685 }
8686 }
8687 }
8688 }
8689 staging.extend(new_ops);
8690 }
8691
8692 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
8697 let mut cascade_pass = 0_usize;
8698 loop {
8699 commit_prepare_checkpoint(control, cascade_pass)?;
8700 cascade_pass += 1;
8701 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
8702 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
8703 .iter()
8704 .filter_map(|(t, op)| match op {
8705 Staged::Delete(rid) => Some((*t, *rid)),
8706 _ => None,
8707 })
8708 .collect();
8709 for (delete_index, (table_id, rid)) in deletes.into_iter().enumerate() {
8710 commit_prepare_checkpoint(control, delete_index)?;
8711 if !cascaded.insert((table_id, rid.0)) {
8712 continue;
8713 }
8714 let Some(tname) = live
8715 .iter()
8716 .find(|(t, _, _)| *t == table_id)
8717 .map(|(_, n, _)| *n)
8718 else {
8719 continue;
8720 };
8721 let parent_handle = self.table_by_id(table_id)?;
8722 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
8723 continue;
8724 };
8725 for (child_id, _child_name, child_schema) in &live {
8726 for fk in &child_schema.constraints.foreign_keys {
8727 if fk.ref_table != tname {
8728 continue;
8729 }
8730 let Some(parent_key) =
8731 encode_composite_key(&fk.ref_columns, &parent_row.columns)
8732 else {
8733 continue;
8734 };
8735 let key_preserved = staging.iter().any(|(t, op)| {
8744 if *t != table_id {
8745 return false;
8746 }
8747 let Staged::Put(cells) = op else {
8748 return false;
8749 };
8750 let map: HashMap<u16, crate::memtable::Value> =
8751 cells.iter().cloned().collect();
8752 encode_composite_key(&fk.ref_columns, &map).as_deref()
8753 == Some(parent_key.as_slice())
8754 });
8755 if key_preserved {
8756 continue;
8757 }
8758 match fk.on_delete {
8759 FkAction::Restrict => continue,
8760 FkAction::Cascade => {
8761 let child_rows = load_rows(*child_id)?;
8762 for (child_index, cr) in child_rows.iter().enumerate() {
8763 commit_prepare_checkpoint(control, child_index)?;
8764 if !cascaded.contains(&(*child_id, cr.row_id.0))
8765 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
8766 == Some(parent_key.as_slice())
8767 {
8768 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
8769 }
8770 }
8771 }
8772 FkAction::SetNull => {
8773 let child_rows = load_rows(*child_id)?;
8774 for (child_index, cr) in child_rows.iter().enumerate() {
8775 commit_prepare_checkpoint(control, child_index)?;
8776 if !cascaded.contains(&(*child_id, cr.row_id.0))
8777 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
8778 == Some(parent_key.as_slice())
8779 {
8780 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
8783 .columns
8784 .iter()
8785 .map(|(k, v)| (*k, v.clone()))
8786 .collect();
8787 for cid in &fk.columns {
8788 cells.retain(|(k, _)| k != cid);
8789 cells.push((*cid, crate::memtable::Value::Null));
8790 }
8791 new_ops.push((
8792 *child_id,
8793 Staged::Update {
8794 row_id: cr.row_id,
8795 new_row: cells,
8796 changed_columns: fk.columns.clone(),
8797 },
8798 ));
8799 }
8800 }
8801 }
8802 }
8803 }
8804 }
8805 }
8806 if new_ops.is_empty() {
8807 break;
8808 }
8809 staging.extend(new_ops);
8810 }
8811
8812 let staged_deletes: HashSet<(u64, u64)> = staging
8816 .iter()
8817 .filter_map(|(t, op)| match op {
8818 Staged::Delete(rid) | Staged::Update { row_id: rid, .. } => Some((*t, rid.0)),
8819 _ => None,
8820 })
8821 .collect();
8822
8823 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
8825
8826 for (operation_index, (table_id, op)) in staging.iter().enumerate() {
8828 commit_prepare_checkpoint(control, operation_index)?;
8829 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id).copied()
8830 else {
8831 continue;
8832 };
8833 let cells_map: HashMap<u16, crate::memtable::Value>;
8834 match op {
8835 Staged::Put(cells) | Staged::Update { new_row: cells, .. } => {
8836 cells_map = cells.iter().cloned().collect();
8837
8838 if !schema.constraints.checks.is_empty() {
8840 validate_checks(&schema.constraints.checks, &cells_map)?;
8841 }
8842
8843 for uc in &schema.constraints.uniques {
8845 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
8846 continue; };
8848 let marker = (*table_id, uc.id, key.clone());
8849 if !seen_unique.insert(marker) {
8850 return Err(MongrelError::Conflict(format!(
8851 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
8852 uc.name
8853 )));
8854 }
8855 let rows = load_rows(*table_id)?;
8856 for (row_index, r) in rows.iter().enumerate() {
8857 commit_prepare_checkpoint(control, row_index)?;
8858 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
8861 continue;
8862 }
8863 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
8864 if theirs == key {
8865 return Err(MongrelError::Conflict(format!(
8866 "UNIQUE constraint '{}' on table '{tname}' violated",
8867 uc.name
8868 )));
8869 }
8870 }
8871 }
8872 }
8873
8874 for fk in &schema.constraints.foreign_keys {
8876 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
8877 continue; };
8879 let Some(parent_id) = cat
8880 .tables
8881 .iter()
8882 .find(|t| t.name == fk.ref_table)
8883 .map(|t| t.table_id)
8884 else {
8885 return Err(MongrelError::InvalidArgument(format!(
8886 "FOREIGN KEY '{}' references unknown table '{}'",
8887 fk.name, fk.ref_table
8888 )));
8889 };
8890 let parent_rows = load_rows(parent_id)?;
8891 let mut found = false;
8892 for (row_index, r) in parent_rows.iter().enumerate() {
8893 commit_prepare_checkpoint(control, row_index)?;
8894 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
8895 continue;
8896 }
8897 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
8898 if pkey == child_key {
8899 found = true;
8900 break;
8901 }
8902 }
8903 }
8904 if !found {
8911 for (staged_index, (st_table, st_op)) in staging.iter().enumerate() {
8912 commit_prepare_checkpoint(control, staged_index)?;
8913 if *st_table != parent_id {
8914 continue;
8915 }
8916 if let Staged::Put(pcells)
8917 | Staged::Update {
8918 new_row: pcells, ..
8919 } = st_op
8920 {
8921 let pmap: HashMap<u16, crate::memtable::Value> =
8922 pcells.iter().cloned().collect();
8923 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &pmap)
8924 {
8925 if pkey == child_key {
8926 found = true;
8927 break;
8928 }
8929 }
8930 }
8931 }
8932 }
8933 if !found {
8934 return Err(MongrelError::Conflict(format!(
8935 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
8936 fk.name, fk.ref_table
8937 )));
8938 }
8939 }
8940
8941 if let Staged::Update { row_id, .. } = op {
8946 let parent_handle = self.table_by_id(*table_id)?;
8947 let Some(old_parent) = parent_handle.lock().get(*row_id, snapshot) else {
8948 continue;
8949 };
8950 for (child_id, child_name, child_schema) in &live {
8951 for fk in &child_schema.constraints.foreign_keys {
8952 if fk.ref_table != tname || fk.on_update != FkAction::Restrict {
8953 continue;
8954 }
8955 let Some(old_key) =
8956 encode_composite_key(&fk.ref_columns, &old_parent.columns)
8957 else {
8958 continue;
8959 };
8960 if encode_composite_key(&fk.ref_columns, &cells_map).as_deref()
8961 == Some(old_key.as_slice())
8962 {
8963 continue;
8964 }
8965 for (child_index, child) in
8966 load_rows(*child_id)?.into_iter().enumerate()
8967 {
8968 commit_prepare_checkpoint(control, child_index)?;
8969 if encode_composite_key(&fk.columns, &child.columns).as_deref()
8970 != Some(old_key.as_slice())
8971 {
8972 continue;
8973 }
8974 let replacement = staging.iter().find_map(|(id, op)| {
8975 if *id != *child_id {
8976 return None;
8977 }
8978 match op {
8979 Staged::Delete(id) if *id == child.row_id => Some(None),
8980 Staged::Update {
8981 row_id,
8982 new_row: cells,
8983 ..
8984 } if *row_id == child.row_id => {
8985 let map: HashMap<u16, Value> =
8986 cells.iter().cloned().collect();
8987 Some(encode_composite_key(&fk.columns, &map))
8988 }
8989 _ => None,
8990 }
8991 });
8992 if replacement.is_some_and(|key| {
8993 key.as_deref() != Some(old_key.as_slice())
8994 }) {
8995 continue;
8996 }
8997 return Err(MongrelError::Conflict(format!(
8998 "FOREIGN KEY '{}' on table '{child_name}' restricts update (parent key referenced)",
8999 fk.name
9000 )));
9001 }
9002 }
9003 }
9004 }
9005 }
9006 Staged::Delete(rid) => {
9007 let parent_handle = self.table_by_id(*table_id)?;
9011 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
9012 continue;
9013 };
9014 for (child_id, child_name, child_schema) in &live {
9015 for fk in &child_schema.constraints.foreign_keys {
9016 if fk.ref_table != tname || fk.on_delete != FkAction::Restrict {
9017 continue;
9018 }
9019 let Some(parent_key) =
9020 encode_composite_key(&fk.ref_columns, &parent_row.columns)
9021 else {
9022 continue;
9023 };
9024 let child_rows = load_rows(*child_id)?;
9025 for (row_index, r) in child_rows.iter().enumerate() {
9026 commit_prepare_checkpoint(control, row_index)?;
9027 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
9030 continue;
9031 }
9032 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
9033 if ck == parent_key {
9034 return Err(MongrelError::Conflict(format!(
9035 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
9036 fk.name
9037 )));
9038 }
9039 }
9040 }
9041 }
9042 }
9043 }
9044 Staged::Truncate => {
9045 for (child_id, child_name, child_schema) in &live {
9049 for fk in &child_schema.constraints.foreign_keys {
9050 if fk.ref_table != tname {
9051 continue;
9052 }
9053 let child_rows = load_rows(*child_id)?;
9054 if child_rows
9055 .iter()
9056 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
9057 {
9058 return Err(MongrelError::Conflict(format!(
9059 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
9060 fk.name
9061 )));
9062 }
9063 }
9064 }
9065 }
9066 }
9067 }
9068 Ok(())
9069 }
9070
9071 fn validate_write_permissions(
9072 &self,
9073 staging: &[(u64, crate::txn::Staged)],
9074 principal: Option<&crate::auth::Principal>,
9075 control: Option<&crate::ExecutionControl>,
9076 ) -> Result<()> {
9077 commit_prepare_checkpoint(control, 0)?;
9078 if principal.is_none() && !self.auth_state.require_auth() {
9079 return Ok(());
9080 }
9081 let principal = principal.ok_or(MongrelError::AuthRequired)?;
9082 let needs = summarize_write_permissions(staging);
9083 let catalog = self.catalog.read();
9084
9085 if needs.values().any(|need| need.truncate) {
9086 self.require_for(Some(principal), &crate::auth::Permission::Admin)?;
9087 }
9088 for (need_index, (table_id, need)) in needs.into_iter().enumerate() {
9089 commit_prepare_checkpoint(control, need_index)?;
9090 let entry = catalog
9091 .tables
9092 .iter()
9093 .find(|entry| {
9094 entry.table_id == table_id
9095 && matches!(entry.state, TableState::Live | TableState::Building { .. })
9096 })
9097 .ok_or_else(|| {
9098 MongrelError::NotFound(format!(
9099 "live table {table_id} not found during write validation"
9100 ))
9101 })?;
9102 if matches!(entry.state, TableState::Building { .. }) {
9103 self.require_for(Some(principal), &crate::auth::Permission::Ddl)?;
9104 continue;
9105 }
9106 if need.insert {
9107 Self::require_columns_for_principal(
9108 &entry.name,
9109 &entry.schema,
9110 crate::auth::ColumnOperation::Insert,
9111 &need.insert_columns,
9112 principal,
9113 )?;
9114 }
9115 if need.update {
9116 Self::require_columns_for_principal(
9117 &entry.name,
9118 &entry.schema,
9119 crate::auth::ColumnOperation::Update,
9120 &need.update_columns,
9121 principal,
9122 )?;
9123 }
9124 if need.delete {
9125 self.require_for(
9126 Some(principal),
9127 &crate::auth::Permission::Delete {
9128 table: entry.name.clone(),
9129 },
9130 )?;
9131 }
9132 }
9133 Ok(())
9134 }
9135
9136 fn validate_security_writes(
9137 &self,
9138 staging: &[(u64, crate::txn::Staged)],
9139 read_epoch: Epoch,
9140 explicit_principal: Option<&crate::auth::Principal>,
9141 control: Option<&crate::ExecutionControl>,
9142 ) -> Result<()> {
9143 commit_prepare_checkpoint(control, 0)?;
9144 use crate::security::PolicyCommand;
9145 use crate::txn::Staged;
9146
9147 let catalog = self.catalog.read();
9148 if catalog.security.rls_tables.is_empty() {
9149 return Ok(());
9150 }
9151 let security = catalog.security.clone();
9152 let table_names = catalog
9153 .tables
9154 .iter()
9155 .filter(|entry| matches!(entry.state, TableState::Live))
9156 .map(|entry| (entry.table_id, entry.name.clone()))
9157 .collect::<HashMap<_, _>>();
9158 drop(catalog);
9159 if !staging.iter().any(|(table_id, _)| {
9160 table_names
9161 .get(table_id)
9162 .is_some_and(|table| security.rls_enabled(table))
9163 }) {
9164 return Ok(());
9165 }
9166 let principal = explicit_principal.ok_or(MongrelError::AuthRequired)?;
9167
9168 for (operation_index, (table_id, operation)) in staging.iter().enumerate() {
9169 commit_prepare_checkpoint(control, operation_index)?;
9170 let Some(table) = table_names.get(table_id) else {
9171 continue;
9172 };
9173 if !security.rls_enabled(table) || principal.is_admin {
9174 continue;
9175 }
9176 let denied = |command| MongrelError::PermissionDenied {
9177 required: match command {
9178 PolicyCommand::Insert => crate::auth::Permission::Insert {
9179 table: table.clone(),
9180 },
9181 PolicyCommand::Update => crate::auth::Permission::Update {
9182 table: table.clone(),
9183 },
9184 PolicyCommand::Delete | PolicyCommand::All | PolicyCommand::Select => {
9185 crate::auth::Permission::Delete {
9186 table: table.clone(),
9187 }
9188 }
9189 },
9190 principal: principal.username.clone(),
9191 };
9192 match operation {
9193 Staged::Put(cells) => {
9194 let mut row = crate::memtable::Row::new(RowId(0), Epoch(read_epoch.0));
9195 row.columns.extend(cells.iter().cloned());
9196 if !security.row_allowed(table, PolicyCommand::Insert, &row, principal, true) {
9197 return Err(denied(PolicyCommand::Insert));
9198 }
9199 }
9200 Staged::Update {
9201 row_id,
9202 new_row: cells,
9203 ..
9204 } => {
9205 let old = self
9206 .table_by_id(*table_id)?
9207 .lock()
9208 .get(*row_id, Snapshot::at(read_epoch))
9209 .ok_or_else(|| {
9210 MongrelError::NotFound(format!("row {} not found", row_id.0))
9211 })?;
9212 if !security.row_allowed(table, PolicyCommand::Update, &old, principal, false) {
9213 return Err(denied(PolicyCommand::Update));
9214 }
9215 let mut new = crate::memtable::Row::new(*row_id, Epoch(read_epoch.0));
9216 new.columns.extend(cells.iter().cloned());
9217 if !security.row_allowed(table, PolicyCommand::Update, &new, principal, true) {
9218 return Err(denied(PolicyCommand::Update));
9219 }
9220 }
9221 Staged::Delete(row_id) => {
9222 let old = self
9223 .table_by_id(*table_id)?
9224 .lock()
9225 .get(*row_id, Snapshot::at(read_epoch))
9226 .ok_or_else(|| {
9227 MongrelError::NotFound(format!("row {} not found", row_id.0))
9228 })?;
9229 if !security.row_allowed(table, PolicyCommand::Delete, &old, principal, false) {
9230 return Err(denied(PolicyCommand::Delete));
9231 }
9232 }
9233 Staged::Truncate => return Err(denied(PolicyCommand::Delete)),
9234 }
9235 }
9236 Ok(())
9237 }
9238
9239 #[allow(clippy::too_many_arguments)]
9246 pub(crate) fn commit_transaction_with_external_states(
9247 &self,
9248 txn_id: u64,
9249 read_epoch: Epoch,
9250 staging: Vec<(u64, crate::txn::Staged)>,
9251 external_states: Vec<(String, Vec<u8>)>,
9252 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
9253 security_principal: Option<crate::auth::Principal>,
9254 principal_catalog_bound: bool,
9255 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9256 ) -> Result<(Epoch, Vec<RowId>)> {
9257 self.commit_transaction_with_external_states_inner(
9258 txn_id,
9259 read_epoch,
9260 staging,
9261 external_states,
9262 materialized_view_updates,
9263 security_principal,
9264 principal_catalog_bound,
9265 external_trigger_bridge,
9266 None,
9267 None,
9268 )
9269 }
9270
9271 #[allow(clippy::too_many_arguments)]
9272 pub(crate) fn commit_transaction_with_external_states_controlled(
9273 &self,
9274 txn_id: u64,
9275 read_epoch: Epoch,
9276 staging: Vec<(u64, crate::txn::Staged)>,
9277 external_states: Vec<(String, Vec<u8>)>,
9278 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
9279 security_principal: Option<crate::auth::Principal>,
9280 principal_catalog_bound: bool,
9281 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9282 control: &crate::ExecutionControl,
9283 before_commit: &mut dyn FnMut() -> Result<()>,
9284 ) -> Result<(Epoch, Vec<RowId>)> {
9285 self.commit_transaction_with_external_states_inner(
9286 txn_id,
9287 read_epoch,
9288 staging,
9289 external_states,
9290 materialized_view_updates,
9291 security_principal,
9292 principal_catalog_bound,
9293 external_trigger_bridge,
9294 Some(control),
9295 Some(before_commit),
9296 )
9297 }
9298
9299 #[allow(clippy::too_many_arguments)]
9300 fn commit_transaction_with_external_states_inner(
9301 &self,
9302 txn_id: u64,
9303 read_epoch: Epoch,
9304 mut staging: Vec<(u64, crate::txn::Staged)>,
9305 external_states: Vec<(String, Vec<u8>)>,
9306 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
9307 mut security_principal: Option<crate::auth::Principal>,
9308 principal_catalog_bound: bool,
9309 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9310 control: Option<&crate::ExecutionControl>,
9311 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
9312 ) -> Result<(Epoch, Vec<RowId>)> {
9313 use crate::memtable::Row;
9314 use crate::txn::{Staged, StagedOp, WriteKey};
9315 use crate::wal::Op;
9316 use std::collections::hash_map::DefaultHasher;
9317 use std::hash::{Hash, Hasher};
9318 use std::sync::atomic::Ordering;
9319
9320 if txn_id == crate::wal::SYSTEM_TXN_ID {
9321 return Err(MongrelError::Full(
9322 "per-open transaction id namespace exhausted; reopen the database".into(),
9323 ));
9324 }
9325 if self.read_only {
9326 return Err(MongrelError::ReadOnlyReplica);
9327 }
9328 commit_prepare_checkpoint(control, 0)?;
9329 let observed_security_version = self.security_coordinator.version.load(Ordering::Acquire);
9330 self.refresh_security_catalog_if_stale(observed_security_version)?;
9331 let trigger_binding = trigger_catalog_binding(&self.catalog.read());
9332 if self.auth_state.require_auth() && security_principal.is_none() {
9333 return Err(MongrelError::AuthRequired);
9334 }
9335 {
9336 let catalog = self.catalog.read();
9337 if catalog.require_auth
9338 || principal_catalog_bound
9339 || security_principal
9340 .as_ref()
9341 .is_some_and(|principal| principal.user_id != 0)
9342 {
9343 let principal = security_principal
9344 .as_ref()
9345 .ok_or(MongrelError::AuthRequired)?;
9346 security_principal =
9347 Self::resolve_bound_principal_from_catalog(&catalog, principal);
9348 if security_principal.is_none() {
9349 return Err(MongrelError::AuthRequired);
9350 }
9351 }
9352 }
9353 let _replication_guard = self.replication_barrier.read();
9354 if self.poisoned.load(Ordering::Relaxed) {
9355 return Err(MongrelError::Other(
9356 "database poisoned by fsync error".into(),
9357 ));
9358 }
9359 let mut external_states = dedup_external_states(external_states);
9360 if !external_states.is_empty() {
9361 let cat = self.catalog.read();
9362 for (name, _) in &external_states {
9363 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
9364 return Err(MongrelError::NotFound(format!(
9365 "external table {name:?} not found"
9366 )));
9367 }
9368 }
9369 }
9370 let prepared_materialized_views = {
9371 let mut deduplicated = HashMap::new();
9372 for (definition_index, definition) in materialized_view_updates.into_iter().enumerate()
9373 {
9374 commit_prepare_checkpoint(control, definition_index)?;
9375 if definition.name.is_empty() || definition.query.trim().is_empty() {
9376 return Err(MongrelError::InvalidArgument(
9377 "materialized view name and query must not be empty".into(),
9378 ));
9379 }
9380 deduplicated.insert(definition.name.clone(), definition);
9381 }
9382 let catalog = self.catalog.read();
9383 let mut prepared = Vec::with_capacity(deduplicated.len());
9384 for (definition_index, definition) in deduplicated.into_values().enumerate() {
9385 commit_prepare_checkpoint(control, definition_index)?;
9386 let table_id = catalog
9387 .live(&definition.name)
9388 .ok_or_else(|| {
9389 MongrelError::NotFound(format!(
9390 "materialized view table {:?} not found",
9391 definition.name
9392 ))
9393 })?
9394 .table_id;
9395 prepared.push((table_id, definition));
9396 }
9397 prepared.sort_by(|left, right| left.1.name.cmp(&right.1.name));
9398 prepared
9399 };
9400
9401 self.fill_auto_increment_for_staging(&mut staging, control)?;
9404 self.expand_table_triggers(
9405 &mut staging,
9406 read_epoch,
9407 external_trigger_bridge,
9408 &mut external_states,
9409 control,
9410 )?;
9411 self.fill_auto_increment_for_staging(&mut staging, control)?;
9412 external_states = dedup_external_states(external_states);
9413 let expected_external_generations = {
9414 let catalog = self.catalog.read();
9415 let mut generations = HashMap::with_capacity(external_states.len());
9416 for (name, _) in &external_states {
9417 let entry = catalog
9418 .external_tables
9419 .iter()
9420 .find(|entry| entry.name == *name)
9421 .ok_or_else(|| {
9422 MongrelError::NotFound(format!("external table {name:?} not found"))
9423 })?;
9424 generations.insert(name.clone(), entry.created_epoch);
9425 }
9426 generations
9427 };
9428
9429 self.validate_constraints(&mut staging, read_epoch, control)?;
9434 self.validate_write_permissions(&staging, security_principal.as_ref(), control)?;
9435 self.validate_security_writes(&staging, read_epoch, security_principal.as_ref(), control)?;
9436 let mut normalized = Vec::with_capacity(staging.len() * 2);
9437 for (staged_index, (table_id, op)) in staging.into_iter().enumerate() {
9438 commit_prepare_checkpoint(control, staged_index)?;
9439 match op {
9440 crate::txn::Staged::Update {
9441 row_id,
9442 new_row: cells,
9443 ..
9444 } => {
9445 normalized.push((table_id, crate::txn::Staged::Delete(row_id)));
9446 normalized.push((table_id, crate::txn::Staged::Put(cells)));
9447 }
9448 op => normalized.push((table_id, op)),
9449 }
9450 }
9451 staging = normalized;
9452 let has_changes = !staging.is_empty()
9453 || !external_states.is_empty()
9454 || !prepared_materialized_views.is_empty();
9455 let truncated_tables: HashSet<u64> = staging
9456 .iter()
9457 .filter_map(|(table_id, op)| matches!(op, Staged::Truncate).then_some(*table_id))
9458 .collect();
9459
9460 let write_keys = {
9461 let cat = self.catalog.read();
9462 let mut keys: Vec<WriteKey> = Vec::new();
9463 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
9464 commit_prepare_checkpoint(control, staged_index)?;
9465 match staged {
9466 Staged::Put(cells) => {
9467 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
9468 for col in &entry.schema.columns {
9469 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
9470 if let Some((_, val)) =
9471 cells.iter().find(|(id, _)| *id == col.id)
9472 {
9473 let mut h = DefaultHasher::new();
9474 val.encode_key().hash(&mut h);
9475 keys.push(WriteKey::Unique {
9476 table_id: *table_id,
9477 index_id: 0,
9478 key_hash: h.finish(),
9479 });
9480 }
9481 }
9482 }
9483 for uc in &entry.schema.constraints.uniques {
9490 if let Some(key_bytes) = crate::constraint::encode_composite_key(
9491 &uc.columns,
9492 &cells.iter().cloned().collect(),
9493 ) {
9494 let mut h = DefaultHasher::new();
9495 key_bytes.hash(&mut h);
9496 keys.push(WriteKey::Unique {
9497 table_id: *table_id,
9498 index_id: uc.id | 0x8000,
9499 key_hash: h.finish(),
9500 });
9501 }
9502 }
9503 }
9504 }
9505 Staged::Delete(rid) => keys.push(WriteKey::Row {
9506 table_id: *table_id,
9507 row_id: rid.0,
9508 }),
9509 Staged::Truncate => keys.push(WriteKey::Table {
9510 table_id: *table_id,
9511 }),
9512 Staged::Update { .. } => {
9513 return Err(MongrelError::Other(
9514 "transaction contains an unnormalized update during preparation".into(),
9515 ));
9516 }
9517 }
9518 }
9519 for (external_index, (name, _)) in external_states.iter().enumerate() {
9520 commit_prepare_checkpoint(control, external_index)?;
9521 let mut h = DefaultHasher::new();
9522 name.hash(&mut h);
9523 keys.push(WriteKey::Unique {
9524 table_id: EXTERNAL_TABLE_ID,
9525 index_id: 0,
9526 key_hash: h.finish(),
9527 });
9528 }
9529 keys
9530 };
9531
9532 let min_active = self.active_txns.min_read_epoch();
9534 if min_active < u64::MAX {
9535 self.conflicts.prune_below(Epoch(min_active));
9536 }
9537
9538 if self.conflicts.conflicts(&write_keys, read_epoch) {
9542 return Err(MongrelError::Conflict(
9543 "write-write conflict (pre-validate, first-committer-wins)".into(),
9544 ));
9545 }
9546 let pre_validate_version = self.conflicts.version();
9547
9548 let mut spilled: Vec<SpilledRun> = Vec::new();
9552 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
9553 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
9557 {
9558 let mut table_bytes: HashMap<u64, u64> = HashMap::new();
9559 let mut put_indexes: HashMap<u64, Vec<usize>> = HashMap::new();
9560 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
9561 commit_prepare_checkpoint(control, staged_index)?;
9562 if let Staged::Put(cells) = staged {
9563 let bytes = cells.iter().fold(32_u64, |bytes, (_, value)| {
9564 bytes.saturating_add(value.estimated_bytes())
9565 });
9566 let table_bytes = table_bytes.entry(*table_id).or_default();
9567 *table_bytes = table_bytes.saturating_add(bytes);
9568 put_indexes.entry(*table_id).or_default().push(staged_index);
9569 }
9570 }
9571 let tables = self.tables.read();
9572 for (table_index, (&table_id, &bytes)) in table_bytes.iter().enumerate() {
9573 commit_prepare_checkpoint(control, table_index)?;
9574 if bytes
9575 <= self
9576 .spill_threshold
9577 .load(std::sync::atomic::Ordering::Relaxed)
9578 {
9579 continue;
9580 }
9581 let Some(handle) = tables.get(&table_id) else {
9582 continue;
9583 };
9584 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
9585 let mut t = handle.lock();
9586 let tdir = t.table_dir().to_path_buf();
9587 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
9588 std::fs::create_dir_all(&txn_dir)?;
9589 let run_id = t.alloc_run_id()? as u128;
9590 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
9591 let final_path = t.run_path(run_id as u64);
9592
9593 let mut rows: Vec<Row> = Vec::new();
9594 for (put_index, staged_index) in put_indexes[&table_id].iter().enumerate() {
9595 commit_prepare_checkpoint(control, put_index)?;
9596 let Staged::Put(cells) = &mut staging[*staged_index].1 else {
9597 return Err(MongrelError::Other(
9598 "transaction put index no longer references a put".into(),
9599 ));
9600 };
9601 t.validate_cells_not_null(cells)?;
9602 let row_id = t.alloc_row_id()?;
9603 let mut row = Row::new(row_id, Epoch(0));
9604 row.columns.extend(std::mem::take(cells));
9605 rows.push(row);
9606 }
9607 let schema = t.schema_ref().clone();
9608 let kek = t.kek_ref().cloned();
9609 let specs = t.indexable_column_specs();
9610 drop(t);
9611
9612 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
9613 .uniform_epoch(true);
9614 if let Some(ref kek) = kek {
9615 writer = writer.with_encryption(kek.as_ref(), specs);
9616 }
9617 commit_prepare_checkpoint(control, 0)?;
9618 let header = writer.write(&pending_path, &rows)?;
9619 commit_prepare_checkpoint(control, 0)?;
9620 let row_count = header.row_count;
9621 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
9622 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
9623
9624 spilled.push(SpilledRun {
9625 table_id,
9626 run_id,
9627 pending_path,
9628 final_path,
9629 rows,
9630 row_count,
9631 min_rid,
9632 max_rid,
9633 content_hash: header.content_hash,
9634 });
9635 spilled_tables.insert(table_id);
9636 }
9637 }
9638
9639 if spill_guard.is_some() {
9641 if let Some(hook) = self.spill_hook.lock().as_ref() {
9642 hook();
9643 }
9644 }
9645
9646 let mut prebuilt: Vec<Option<Row>> = std::iter::repeat_with(|| None)
9657 .take(staging.len())
9658 .collect();
9659 let mut delete_images: Vec<Option<Row>> = std::iter::repeat_with(|| None)
9660 .take(staging.len())
9661 .collect();
9662 {
9663 let mut indexes_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
9664 for (index, (table_id, staged)) in staging.iter().enumerate() {
9665 commit_prepare_checkpoint(control, index)?;
9666 if matches!(staged, Staged::Delete(_))
9667 || matches!(staged, Staged::Put(_) if !spilled_tables.contains(table_id))
9668 {
9669 indexes_by_table.entry(*table_id).or_default().push(index);
9670 }
9671 }
9672 let tables = self.tables.read();
9673 for (table_index, (table_id, indexes)) in indexes_by_table.into_iter().enumerate() {
9674 commit_prepare_checkpoint(control, table_index)?;
9675 let handle = tables.get(&table_id).ok_or_else(|| {
9676 MongrelError::NotFound(format!("table {table_id} not mounted"))
9677 })?;
9678 #[cfg(test)]
9679 PREBUILD_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
9680 let mut t = handle.lock();
9681 for (prepare_index, index) in indexes.into_iter().enumerate() {
9682 commit_prepare_checkpoint(control, prepare_index)?;
9683 match &staging[index].1 {
9684 Staged::Put(cells) if !spilled_tables.contains(&table_id) => {
9685 t.validate_cells_not_null(cells)?;
9686 let mut row = Row::new(t.alloc_row_id()?, Epoch(0));
9687 for (column, value) in cells {
9688 row.columns.insert(*column, value.clone());
9689 }
9690 prebuilt[index] = Some(row);
9691 }
9692 Staged::Delete(row_id) => {
9693 delete_images[index] = t.get(*row_id, Snapshot::at(read_epoch));
9694 }
9695 Staged::Put(_) | Staged::Truncate => {}
9696 Staged::Update { .. } => {
9697 return Err(MongrelError::Other(
9698 "transaction contains an unnormalized update during row preparation"
9699 .into(),
9700 ));
9701 }
9702 }
9703 }
9704 }
9705 }
9706
9707 let prepared_table_handles = {
9711 let table_ids: HashSet<u64> = staging.iter().map(|(table_id, _)| *table_id).collect();
9712 let put_table_ids: HashSet<u64> = staging
9713 .iter()
9714 .filter_map(|(table_id, staged)| {
9715 matches!(staged, Staged::Put(_)).then_some(*table_id)
9716 })
9717 .collect();
9718 let tables = self.tables.read();
9719 let mut handles = HashMap::with_capacity(table_ids.len());
9720 for (table_index, table_id) in table_ids.into_iter().enumerate() {
9721 commit_prepare_checkpoint(control, table_index)?;
9722 let handle = tables.get(&table_id).ok_or_else(|| {
9723 MongrelError::NotFound(format!("table {table_id} not mounted"))
9724 })?;
9725 if put_table_ids.contains(&table_id) {
9726 match control {
9727 Some(control) => {
9728 handle.lock().prepare_durable_publish_controlled(control)?
9729 }
9730 None => handle.lock().prepare_durable_publish()?,
9731 }
9732 }
9733 handles.insert(table_id, handle.clone());
9734 }
9735 handles
9736 };
9737
9738 let mut prepared_run_links = PreparedRunLinks::prepare(&spilled)?;
9742
9743 let mut spilled_row_ids: HashMap<u64, VecDeque<RowId>> = spilled
9744 .iter()
9745 .map(|run| {
9746 (
9747 run.table_id,
9748 run.rows.iter().map(|row| row.row_id).collect(),
9749 )
9750 })
9751 .collect();
9752 let committed_row_ids = staging
9753 .iter()
9754 .enumerate()
9755 .filter_map(|(index, (table_id, staged))| {
9756 if !matches!(staged, Staged::Put(_)) {
9757 return None;
9758 }
9759 prebuilt[index].as_ref().map(|row| row.row_id).or_else(|| {
9760 spilled_row_ids
9761 .get_mut(table_id)
9762 .and_then(VecDeque::pop_front)
9763 })
9764 })
9765 .collect();
9766
9767 let mut prepared_external = Vec::with_capacity(external_states.len());
9768 for (external_index, (name, state)) in external_states.iter().enumerate() {
9769 commit_prepare_checkpoint(control, external_index)?;
9770 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
9771 prepared_external.push((name.clone(), state.clone(), pending));
9772 }
9773
9774 let added_runs: Vec<crate::wal::AddedRun> = spilled
9776 .iter()
9777 .map(|s| crate::wal::AddedRun {
9778 table_id: s.table_id,
9779 run_id: s.run_id,
9780 row_count: s.row_count,
9781 level: 0,
9782 min_row_id: s.min_rid,
9783 max_row_id: s.max_rid,
9784 content_hash: s.content_hash,
9785 })
9786 .collect();
9787 if let Some(hook) = self.catalog_commit_hook.lock().as_ref() {
9788 hook();
9789 }
9790 let security_guard = self.security_coordinator.gate.read();
9794 if self.security_coordinator.version.load(Ordering::Acquire) != observed_security_version {
9795 return Err(MongrelError::Conflict(
9796 "security policy changed during write".into(),
9797 ));
9798 }
9799 if spill_guard.is_some() {
9800 if let Some(hook) = self.security_commit_hook.lock().as_ref() {
9801 hook();
9802 }
9803 }
9804 let commit_guard = self.commit_lock.lock();
9805 let catalog_generation_result = (|| {
9806 {
9807 let catalog = self.catalog.read();
9808 for table_id in prepared_table_handles.keys() {
9809 let is_current = catalog.tables.iter().any(|entry| {
9810 entry.table_id == *table_id
9811 && matches!(entry.state, TableState::Live | TableState::Building { .. })
9812 });
9813 if !is_current {
9814 return Err(MongrelError::Conflict(format!(
9815 "table {table_id} changed during transaction preparation"
9816 )));
9817 }
9818 }
9819 for (name, created_epoch) in &expected_external_generations {
9820 let current = catalog
9821 .external_tables
9822 .iter()
9823 .find(|entry| entry.name == *name)
9824 .map(|entry| entry.created_epoch);
9825 if current != Some(*created_epoch) {
9826 return Err(MongrelError::Conflict(format!(
9827 "external table {name:?} changed during transaction preparation"
9828 )));
9829 }
9830 }
9831 for (table_id, definition) in &prepared_materialized_views {
9832 let current = catalog.live(&definition.name).map(|entry| entry.table_id);
9833 if current != Some(*table_id) {
9834 return Err(MongrelError::Conflict(format!(
9835 "materialized view {:?} changed during transaction preparation",
9836 definition.name
9837 )));
9838 }
9839 }
9840 if trigger_catalog_binding(&catalog) != trigger_binding {
9841 return Err(MongrelError::Conflict(
9842 "trigger or referenced table generation changed during transaction preparation"
9843 .into(),
9844 ));
9845 }
9846 }
9847 let tables = self.tables.read();
9848 for (table_id, prepared) in &prepared_table_handles {
9849 if !tables
9850 .get(table_id)
9851 .is_some_and(|current| current.ptr_eq(prepared))
9852 {
9853 return Err(MongrelError::Conflict(format!(
9854 "table {table_id} mount changed during transaction preparation"
9855 )));
9856 }
9857 }
9858 Ok(())
9859 })();
9860 if let Err(error) = catalog_generation_result {
9861 drop(commit_guard);
9862 for (_, _, pending) in &prepared_external {
9863 let _ = std::fs::remove_file(pending);
9864 }
9865 return Err(error);
9866 }
9867 let new_epoch = self.epoch.assigned().next();
9871 let mut spilled_wal_bytes = 0;
9872 let mut spilled_wal_records = Vec::<(u64, Op)>::new();
9873 let spill_prepare = (|| {
9874 for run in &mut spilled {
9875 for row in &mut run.rows {
9876 row.committed_epoch = new_epoch;
9877 }
9878 for rows in encode_spilled_row_chunks(
9879 &run.rows,
9880 &mut spilled_wal_bytes,
9881 SPILLED_WAL_TOTAL_MAX_BYTES,
9882 control,
9883 )? {
9884 spilled_wal_records.push((
9885 run.table_id,
9886 Op::SpilledRows {
9887 table_id: run.table_id,
9888 rows,
9889 },
9890 ));
9891 }
9892 }
9893 Result::<()>::Ok(())
9894 })();
9895 if let Err(error) = spill_prepare {
9896 for (_, _, pending) in &prepared_external {
9897 let _ = std::fs::remove_file(pending);
9898 }
9899 return Err(error);
9900 }
9901 let (new_epoch, mut _epoch_guard, applies, committed_materialized_views, commit_seq) = {
9902 let mut wal = self.shared_wal.lock();
9903
9904 if self.conflicts.version() != pre_validate_version
9909 && self.conflicts.conflicts(&write_keys, read_epoch)
9910 {
9911 drop(wal);
9914 for (_, _, pending) in &prepared_external {
9915 let _ = std::fs::remove_file(pending);
9916 }
9917 return Err(MongrelError::Conflict(
9918 "write-write conflict (sequencer delta re-check)".into(),
9919 ));
9920 }
9921
9922 if let Some(control) = control {
9923 if let Err(error) = control.checkpoint() {
9924 drop(wal);
9925 for (_, _, pending) in &prepared_external {
9926 let _ = std::fs::remove_file(pending);
9927 }
9928 return Err(error);
9929 }
9930 }
9931 let mut applies = Vec::<TableApplyBatch>::new();
9932 let mut apply_indexes = HashMap::<u64, usize>::new();
9933 let mut committed_materialized_views = Vec::new();
9934 let mut wal_records = spilled_wal_records;
9935
9936 let mut index = 0;
9937 while index < staging.len() {
9938 let table_id = staging[index].0;
9939 let handle = prepared_table_handles
9940 .get(&table_id)
9941 .cloned()
9942 .ok_or_else(|| {
9943 MongrelError::NotFound(format!("table {table_id} not prepared"))
9944 })?;
9945 let batch_index = *apply_indexes.entry(table_id).or_insert_with(|| {
9946 let index = applies.len();
9947 applies.push(TableApplyBatch {
9948 table_id,
9949 handle,
9950 ops: Vec::new(),
9951 });
9952 index
9953 });
9954
9955 if spilled_tables.contains(&table_id) && matches!(&staging[index].1, Staged::Put(_))
9958 {
9959 index += 1;
9960 continue;
9961 }
9962
9963 match &staging[index].1 {
9964 Staged::Put(_) => {
9965 let mut rows = Vec::new();
9966 while index < staging.len()
9967 && staging[index].0 == table_id
9968 && matches!(&staging[index].1, Staged::Put(_))
9969 {
9970 let mut row = prebuilt[index].take().ok_or_else(|| {
9971 MongrelError::Other(
9972 "transaction prepare lost a prebuilt put row".into(),
9973 )
9974 })?;
9975 row.committed_epoch = new_epoch;
9976 rows.push(row);
9977 index += 1;
9978 }
9979 let payload = bincode::serialize(&rows)
9980 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
9981 wal_records.push((
9982 table_id,
9983 Op::Put {
9984 table_id,
9985 rows: payload,
9986 },
9987 ));
9988 applies[batch_index].ops.push(StagedOp::Put(rows));
9989 }
9990 Staged::Delete(_) => {
9991 let mut row_ids = Vec::new();
9992 while index < staging.len()
9993 && staging[index].0 == table_id
9994 && matches!(&staging[index].1, Staged::Delete(_))
9995 {
9996 let Staged::Delete(row_id) = &staging[index].1 else {
9997 return Err(MongrelError::Other(
9998 "transaction delete batch changed during WAL preparation"
9999 .into(),
10000 ));
10001 };
10002 if let Some(before) = &delete_images[index] {
10003 wal_records.push((
10004 table_id,
10005 Op::BeforeImage {
10006 table_id,
10007 row_id: *row_id,
10008 row: bincode::serialize(before).map_err(|error| {
10009 MongrelError::Other(format!(
10010 "before-image serialize: {error}"
10011 ))
10012 })?,
10013 },
10014 ));
10015 }
10016 row_ids.push(*row_id);
10017 index += 1;
10018 }
10019 wal_records.push((
10020 table_id,
10021 Op::Delete {
10022 table_id,
10023 row_ids: row_ids.clone(),
10024 },
10025 ));
10026 applies[batch_index].ops.push(StagedOp::Delete(row_ids));
10027 }
10028 Staged::Truncate => {
10029 wal_records.push((table_id, Op::TruncateTable { table_id }));
10030 applies[batch_index].ops.push(StagedOp::Truncate);
10031 index += 1;
10032 }
10033 Staged::Update { .. } => {
10034 return Err(MongrelError::Other(
10035 "transaction contains an unnormalized update at the sequencer".into(),
10036 ));
10037 }
10038 }
10039 }
10040
10041 for (name, state, _) in &prepared_external {
10042 wal_records.push((
10043 EXTERNAL_TABLE_ID,
10044 Op::ExternalTableState {
10045 name: name.clone(),
10046 state: state.clone(),
10047 },
10048 ));
10049 }
10050
10051 for (table_id, definition) in &prepared_materialized_views {
10052 let mut definition = definition.clone();
10053 definition.last_refresh_epoch = new_epoch.0;
10054 wal_records.push((
10055 *table_id,
10056 Op::Ddl(crate::wal::DdlOp::SetMaterializedView {
10057 name: definition.name.clone(),
10058 definition_json: crate::wal::DdlOp::encode_materialized_view(&definition)?,
10059 }),
10060 ));
10061 committed_materialized_views.push(definition);
10062 }
10063 if !committed_materialized_views.is_empty() {
10064 let mut next_catalog = self.catalog.read().clone();
10065 for definition in &committed_materialized_views {
10066 if let Some(existing) = next_catalog
10067 .materialized_views
10068 .iter_mut()
10069 .find(|existing| existing.name == definition.name)
10070 {
10071 *existing = definition.clone();
10072 } else {
10073 next_catalog.materialized_views.push(definition.clone());
10074 }
10075 }
10076 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
10077 wal_records.push((
10078 WAL_TABLE_ID,
10079 Op::Ddl(crate::wal::DdlOp::CatalogSnapshot {
10080 catalog_json: crate::wal::DdlOp::encode_catalog(&next_catalog)?,
10081 }),
10082 ));
10083 }
10084
10085 if let Some(control) = control {
10086 if let Err(error) = control.checkpoint() {
10087 drop(wal);
10088 for (_, _, pending) in &prepared_external {
10089 let _ = std::fs::remove_file(pending);
10090 }
10091 return Err(error);
10092 }
10093 }
10094 if let Some(before_commit) = before_commit.as_mut() {
10095 if let Err(error) = before_commit() {
10096 drop(wal);
10097 for (_, _, pending) in &prepared_external {
10098 let _ = std::fs::remove_file(pending);
10099 }
10100 return Err(error);
10101 }
10102 }
10103
10104 let assigned_epoch = self.epoch.bump_assigned();
10105 let _epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
10106 if assigned_epoch != new_epoch {
10107 for (_, _, pending) in &prepared_external {
10108 let _ = std::fs::remove_file(pending);
10109 }
10110 return Err(MongrelError::Conflict(
10111 "commit epoch changed while sequencer lock was held".into(),
10112 ));
10113 }
10114
10115 prepared_run_links.disarm();
10119
10120 let append: Result<u64> = (|| {
10121 for (table_id, op) in wal_records {
10122 wal.append(txn_id, table_id, op)?;
10123 }
10124 wal.append_commit(txn_id, new_epoch, &added_runs)
10125 })();
10126 let commit_seq =
10127 append.map_err(|error| self.commit_outcome_unknown(new_epoch, error))?;
10128
10129 self.conflicts.record(&write_keys, new_epoch);
10134 (
10135 new_epoch,
10136 _epoch_guard,
10137 applies,
10138 committed_materialized_views,
10139 commit_seq,
10140 )
10141 };
10142 drop(commit_guard);
10143
10144 self.await_durable_commit(commit_seq, new_epoch)?;
10146 drop(security_guard);
10147
10148 let publish_result: Result<()> = {
10150 let mut first_error = None;
10151 let mut spilled_by_table: HashMap<u64, Vec<&SpilledRun>> = HashMap::new();
10152 for run in &spilled {
10153 spilled_by_table.entry(run.table_id).or_default().push(run);
10154 }
10155 let mut modified_tables = Vec::with_capacity(applies.len());
10156 for batch in applies {
10159 #[cfg(test)]
10160 PUBLISH_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
10161 let mut t = batch.handle.lock();
10162 for op in batch.ops {
10163 match op {
10164 StagedOp::Put(rows) => t.apply_put_rows_prepared(rows),
10165 StagedOp::Delete(row_ids) => {
10166 for row_id in row_ids {
10167 t.apply_delete(row_id, new_epoch);
10168 }
10169 }
10170 StagedOp::Truncate => t.apply_truncate(new_epoch),
10171 }
10172 }
10173 if let Some(runs) = spilled_by_table.remove(&batch.table_id) {
10174 for run in runs {
10175 t.link_run(crate::manifest::RunRef {
10176 run_id: run.run_id,
10177 level: 0,
10178 epoch_created: new_epoch.0,
10179 row_count: run.row_count,
10180 });
10181 t.apply_run_metadata_prepared(&run.rows)?;
10182 if truncated_tables.contains(&batch.table_id) {
10183 t.set_flushed_epoch(new_epoch);
10188 }
10189 }
10190 }
10191 t.invalidate_pending_cache();
10192 drop(t);
10193 modified_tables.push(batch.handle);
10194 }
10195
10196 for handle in modified_tables {
10200 #[cfg(test)]
10201 COMMIT_MANIFEST_WRITES.with(|count| count.set(count.get() + 1));
10202 if let Err(error) = handle.lock().persist_manifest(new_epoch) {
10203 first_error.get_or_insert(error);
10204 }
10205 }
10206 for (name, _, pending) in &prepared_external {
10207 if let Err(error) = publish_external_state_file(&self.root, name, pending) {
10208 first_error.get_or_insert(error);
10209 }
10210 }
10211 if !committed_materialized_views.is_empty() {
10212 let mut next_catalog = self.catalog.read().clone();
10213 for definition in committed_materialized_views {
10214 if let Some(existing) = next_catalog
10215 .materialized_views
10216 .iter_mut()
10217 .find(|existing| existing.name == definition.name)
10218 {
10219 *existing = definition;
10220 } else {
10221 next_catalog.materialized_views.push(definition);
10222 }
10223 }
10224 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
10225 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
10226 first_error.get_or_insert(error);
10227 }
10228 }
10229 match first_error {
10230 Some(error) => Err(error),
10231 None => Ok(()),
10232 }
10233 };
10234
10235 if has_changes {
10236 let _ = self.change_wake.send(());
10237 }
10238 self.finish_durable_publish(new_epoch, &mut _epoch_guard, publish_result)?;
10239 Ok((new_epoch, committed_row_ids))
10240 }
10241
10242 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
10245 let e = self.epoch.visible();
10246 let g = self.snapshots.register(e);
10247 (Snapshot::at(e), g)
10248 }
10249
10250 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
10253 let e = self.epoch.visible();
10254 let g = self.snapshots.register_owned(e);
10255 (Snapshot::at(e), g)
10256 }
10257
10258 pub fn set_history_retention_epochs(&self, epochs: u64) -> Result<()> {
10263 let _guard = self.ddl_lock.lock();
10264 let current = self.epoch.visible();
10265 let (old_epochs, old_start) = self.snapshots.history_config();
10266 let earliest_already_guaranteed = if old_epochs == 0 {
10267 current
10268 } else {
10269 Epoch(old_start.0.max(current.0.saturating_sub(old_epochs)))
10270 };
10271 let start = if epochs == 0 {
10272 current
10273 } else {
10274 earliest_already_guaranteed
10275 };
10276 let published = std::cell::Cell::new(false);
10277 let result = write_history_retention(&self.root, epochs, start, || {
10278 self.snapshots.configure_history(epochs, start);
10279 published.set(true);
10280 });
10281 match result {
10282 Err(error) if published.get() => Err(MongrelError::CommitOutcomeUnknown {
10283 epoch: current.0,
10284 message: format!("history-retention publication was not durable: {error}"),
10285 }),
10286 result => result,
10287 }
10288 }
10289
10290 pub fn history_retention_epochs(&self) -> u64 {
10291 self.snapshots.history_config().0
10292 }
10293
10294 pub fn earliest_retained_epoch(&self) -> Epoch {
10295 let current = self.epoch.visible();
10296 self.snapshots.history_floor(current).unwrap_or(current)
10297 }
10298
10299 pub fn snapshot_at_owned(&self, epoch: Epoch) -> Result<(Snapshot, OwnedSnapshotGuard)> {
10302 let current = self.epoch.visible();
10303 if epoch > current {
10304 return Err(MongrelError::InvalidArgument(format!(
10305 "epoch {} is in the future; current epoch is {}",
10306 epoch.0, current.0
10307 )));
10308 }
10309 let earliest = self.earliest_retained_epoch();
10310 if epoch < earliest {
10311 return Err(MongrelError::InvalidArgument(format!(
10312 "epoch {} is no longer retained; earliest available epoch is {}",
10313 epoch.0, earliest.0
10314 )));
10315 }
10316 let guard = self.snapshots.register_owned(epoch);
10317 Ok((Snapshot::at(epoch), guard))
10318 }
10319
10320 pub fn table_names(&self) -> Vec<String> {
10322 self.catalog
10323 .read()
10324 .tables
10325 .iter()
10326 .filter(|t| matches!(t.state, TableState::Live))
10327 .map(|t| t.name.clone())
10328 .collect()
10329 }
10330
10331 pub fn close(&self) -> Result<()> {
10337 for name in self.table_names() {
10338 if let Ok(handle) = self.table(&name) {
10339 if let Err(e) = handle.lock().close() {
10340 eprintln!("[close] flush failed for {name}: {e}");
10341 }
10342 }
10343 }
10344 Ok(())
10345 }
10346
10347 pub fn compact(&self) -> Result<(usize, usize)> {
10354 self.require(&crate::auth::Permission::Ddl)?;
10355 let mut compacted = 0;
10356 let mut skipped = 0;
10357 for name in self.table_names() {
10358 let Ok(handle) = self.table(&name) else {
10359 continue;
10360 };
10361 {
10362 let mut t = handle.lock();
10363 let before = t.run_count();
10364 if before < 2 && !t.should_compact() {
10365 skipped += 1;
10366 continue;
10367 }
10368 match t.compact() {
10369 Ok(()) => {
10370 let after = t.run_count();
10371 compacted += 1;
10372 eprintln!("[compact] {name}: {before} -> {after} runs");
10373 }
10374 Err(e) => {
10375 eprintln!("[compact] {name}: compaction failed: {e}");
10376 skipped += 1;
10377 }
10378 }
10379 }
10380 }
10381 Ok((compacted, skipped))
10382 }
10383
10384 pub fn compact_table(&self, name: &str) -> Result<bool> {
10387 self.require(&crate::auth::Permission::Ddl)?;
10388 let handle = self.table(name)?;
10389 let mut t = handle.lock();
10390 let before = t.run_count();
10391 if before < 2 {
10392 return Ok(false);
10393 }
10394 t.compact()?;
10395 Ok(t.run_count() < before)
10396 }
10397
10398 pub fn table(&self, name: &str) -> Result<TableHandle> {
10400 let cat = self.catalog.read();
10401 let entry = cat
10402 .live(name)
10403 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
10404 let id = entry.table_id;
10405 drop(cat);
10406 self.tables
10407 .read()
10408 .get(&id)
10409 .cloned()
10410 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
10411 }
10412
10413 pub fn has_ttl_tables(&self) -> bool {
10416 self.tables
10417 .read()
10418 .values()
10419 .any(|table| table.lock().ttl().is_some())
10420 }
10421
10422 pub(crate) fn table_by_id(&self, id: u64) -> Result<TableHandle> {
10425 self.tables
10426 .read()
10427 .get(&id)
10428 .cloned()
10429 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
10430 }
10431
10432 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
10438 if name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
10439 return Err(MongrelError::InvalidArgument(format!(
10440 "table names beginning with {CTAS_BUILD_TABLE_PREFIX:?} are reserved"
10441 )));
10442 }
10443 self.create_table_with_state(name, schema, TableState::Live)
10444 }
10445
10446 #[doc(hidden)]
10448 pub fn create_building_table(
10449 &self,
10450 build_name: &str,
10451 intended_name: &str,
10452 query_id: &str,
10453 schema: Schema,
10454 ) -> Result<u64> {
10455 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10456 || intended_name.is_empty()
10457 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10458 || query_id.is_empty()
10459 {
10460 return Err(MongrelError::InvalidArgument(
10461 "invalid CTAS building-table identity".into(),
10462 ));
10463 }
10464 self.create_table_with_state(
10465 build_name,
10466 schema,
10467 TableState::Building {
10468 intended_name: intended_name.to_string(),
10469 query_id: query_id.to_string(),
10470 created_at_unix_nanos: current_unix_nanos(),
10471 replaces_table_id: None,
10472 },
10473 )
10474 }
10475
10476 #[doc(hidden)]
10479 pub fn create_rebuilding_table(
10480 &self,
10481 build_name: &str,
10482 intended_name: &str,
10483 query_id: &str,
10484 schema: Schema,
10485 ) -> Result<u64> {
10486 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10487 || intended_name.is_empty()
10488 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10489 || query_id.is_empty()
10490 {
10491 return Err(MongrelError::InvalidArgument(
10492 "invalid rebuilding-table identity".into(),
10493 ));
10494 }
10495 let replaces_table_id = self
10496 .catalog
10497 .read()
10498 .live(intended_name)
10499 .ok_or_else(|| MongrelError::NotFound(format!("table {intended_name:?} not found")))?
10500 .table_id;
10501 self.create_table_with_state(
10502 build_name,
10503 schema,
10504 TableState::Building {
10505 intended_name: intended_name.to_string(),
10506 query_id: query_id.to_string(),
10507 created_at_unix_nanos: current_unix_nanos(),
10508 replaces_table_id: Some(replaces_table_id),
10509 },
10510 )
10511 }
10512
10513 fn create_table_with_state(
10514 &self,
10515 name: &str,
10516 schema: Schema,
10517 state: TableState,
10518 ) -> Result<u64> {
10519 use crate::wal::DdlOp;
10520 use std::sync::atomic::Ordering;
10521
10522 self.require(&crate::auth::Permission::Ddl)?;
10523 if self.poisoned.load(Ordering::Relaxed) {
10524 return Err(MongrelError::Other(
10525 "database poisoned by fsync error".into(),
10526 ));
10527 }
10528
10529 let _g = self.ddl_lock.lock();
10530 let _security_write = self.security_write()?;
10531 self.require(&crate::auth::Permission::Ddl)?;
10532 {
10533 let cat = self.catalog.read();
10534 match &state {
10535 TableState::Live => {
10536 if cat.live(name).is_some() || cat.building_for(name).is_some() {
10537 return Err(MongrelError::InvalidArgument(format!(
10538 "table {name:?} already exists or is being built"
10539 )));
10540 }
10541 }
10542 TableState::Building {
10543 intended_name,
10544 replaces_table_id,
10545 ..
10546 } => {
10547 let target_matches = match replaces_table_id {
10548 Some(table_id) => cat
10549 .live(intended_name)
10550 .is_some_and(|entry| entry.table_id == *table_id),
10551 None => cat.live(intended_name).is_none(),
10552 };
10553 if !target_matches || cat.building_for(intended_name).is_some() {
10554 return Err(MongrelError::InvalidArgument(format!(
10555 "table {intended_name:?} changed or is already being built"
10556 )));
10557 }
10558 if cat.building(name).is_some() {
10559 return Err(MongrelError::InvalidArgument(format!(
10560 "building table {name:?} already exists"
10561 )));
10562 }
10563 }
10564 TableState::Dropped { .. } => {
10565 return Err(MongrelError::InvalidArgument(
10566 "cannot create a dropped table".into(),
10567 ));
10568 }
10569 }
10570 }
10571
10572 let commit_lock = Arc::clone(&self.commit_lock);
10575 let _c = commit_lock.lock();
10576 let table_id = {
10577 let mut cat = self.catalog.write();
10578 let id = cat.next_table_id;
10579 cat.next_table_id = id
10580 .checked_add(1)
10581 .ok_or_else(|| MongrelError::InvalidArgument("table id space exhausted".into()))?;
10582 Result::<u64>::Ok(id)
10583 }?;
10584 let epoch = self.epoch.bump_assigned();
10585 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
10586 let txn_id = self.alloc_txn_id()?;
10587
10588 let mut schema = schema;
10592 schema.schema_id = table_id;
10593 schema.validate_auto_increment()?;
10600 schema.validate_defaults()?;
10601 schema.validate_ai()?;
10602 for index in &schema.indexes {
10603 index.validate_options()?;
10604 }
10605 for constraint in &schema.constraints.checks {
10606 constraint.expr.validate()?;
10607 }
10608
10609 let table_relative = Path::new(TABLES_DIR).join(table_id.to_string());
10612 let canonical_tdir = self.root.join(&table_relative);
10613 let table_root = Arc::new(
10614 self.durable_root
10615 .create_directory_all_pinned(&table_relative)?,
10616 );
10617 let tdir = table_root.io_path()?;
10618 let mut pending_table_dir = PendingTableDir::new(canonical_tdir);
10619 let ctx = SharedCtx {
10620 root_guard: Some(table_root),
10621 epoch: Arc::clone(&self.epoch),
10622 page_cache: Arc::clone(&self.page_cache),
10623 decoded_cache: Arc::clone(&self.decoded_cache),
10624 snapshots: Arc::clone(&self.snapshots),
10625 kek: self.kek.clone(),
10626 commit_lock: Arc::clone(&self.commit_lock),
10627 shared: Some(crate::engine::SharedWalCtx {
10628 wal: Arc::clone(&self.shared_wal),
10629 group: Arc::clone(&self.group),
10630 poisoned: Arc::clone(&self.poisoned),
10631 txn_ids: Arc::clone(&self.next_txn_id),
10632 change_wake: self.change_wake.clone(),
10633 }),
10634 table_name: Some(name.to_string()),
10635 auth: self.table_auth_checker(),
10636 read_only: self.read_only,
10637 };
10638 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
10639
10640 let schema_json = DdlOp::encode_schema(&schema)?;
10643 let ddl = match &state {
10644 TableState::Live => DdlOp::CreateTable {
10645 table_id,
10646 name: name.to_string(),
10647 schema_json,
10648 },
10649 TableState::Building {
10650 intended_name,
10651 query_id,
10652 created_at_unix_nanos,
10653 replaces_table_id,
10654 } => match replaces_table_id {
10655 Some(replaces_table_id) => DdlOp::CreateRebuildingTable {
10656 table_id,
10657 build_name: name.to_string(),
10658 intended_name: intended_name.clone(),
10659 query_id: query_id.clone(),
10660 created_at_unix_nanos: *created_at_unix_nanos,
10661 replaces_table_id: *replaces_table_id,
10662 schema_json,
10663 },
10664 None => DdlOp::CreateBuildingTable {
10665 table_id,
10666 build_name: name.to_string(),
10667 intended_name: intended_name.clone(),
10668 query_id: query_id.clone(),
10669 created_at_unix_nanos: *created_at_unix_nanos,
10670 schema_json,
10671 },
10672 },
10673 TableState::Dropped { .. } => {
10674 return Err(MongrelError::InvalidArgument(
10675 "cannot create a table in dropped state".into(),
10676 ));
10677 }
10678 };
10679 let mut next_catalog = self.catalog.read().clone();
10680 next_catalog.tables.push(CatalogEntry {
10681 table_id,
10682 name: name.to_string(),
10683 schema: schema.clone(),
10684 state: state.clone(),
10685 created_epoch: epoch.0,
10686 });
10687 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
10688 let commit_seq = {
10689 let mut wal = self.shared_wal.lock();
10690 let append: Result<u64> = (|| {
10691 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
10692 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
10693 wal.append_commit(txn_id, epoch, &[])
10694 })();
10695 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
10696 };
10697 self.await_durable_commit(commit_seq, epoch)?;
10698 pending_table_dir.disarm();
10699
10700 self.tables
10703 .write()
10704 .insert(table_id, TableHandle::new(table));
10705 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
10706 self.finish_durable_publish(epoch, &mut _epoch_guard, checkpoint)?;
10707 Ok(table_id)
10708 }
10709
10710 pub fn drop_table(&self, name: &str) -> Result<()> {
10712 self.drop_table_with_epoch(name).map(|_| ())
10713 }
10714
10715 pub fn drop_table_with_epoch(&self, name: &str) -> Result<Epoch> {
10717 self.drop_table_with_state(name, false, None)
10718 }
10719
10720 pub fn drop_table_with_epoch_controlled<F>(
10721 &self,
10722 name: &str,
10723 mut before_commit: F,
10724 ) -> Result<Epoch>
10725 where
10726 F: FnMut() -> Result<()>,
10727 {
10728 self.drop_table_with_state(name, false, Some(&mut before_commit))
10729 }
10730
10731 #[doc(hidden)]
10733 pub fn discard_building_table(&self, name: &str) -> Result<()> {
10734 if !name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
10735 return Err(MongrelError::InvalidArgument(
10736 "not a CTAS building table".into(),
10737 ));
10738 }
10739 self.drop_table_with_state(name, true, None).map(|_| ())
10740 }
10741
10742 fn drop_table_with_state(
10743 &self,
10744 name: &str,
10745 building: bool,
10746 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10747 ) -> Result<Epoch> {
10748 use crate::wal::DdlOp;
10749 use std::sync::atomic::Ordering;
10750
10751 self.require(&crate::auth::Permission::Ddl)?;
10752 if self.poisoned.load(Ordering::Relaxed) {
10753 return Err(MongrelError::Other(
10754 "database poisoned by fsync error".into(),
10755 ));
10756 }
10757
10758 let _g = self.ddl_lock.lock();
10759 let _security_write = self.security_write()?;
10760 self.require(&crate::auth::Permission::Ddl)?;
10761 let table_id = {
10762 let cat = self.catalog.read();
10763 if building {
10764 cat.building(name)
10765 } else {
10766 cat.live(name)
10767 }
10768 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
10769 .table_id
10770 };
10771
10772 let commit_lock = Arc::clone(&self.commit_lock);
10773 let _c = commit_lock.lock();
10774 let epoch = self.epoch.bump_assigned();
10775 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
10776 let txn_id = self.alloc_txn_id()?;
10777 let mut next_catalog = self.catalog.read().clone();
10778 let entry = next_catalog
10779 .tables
10780 .iter_mut()
10781 .find(|t| t.table_id == table_id)
10782 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
10783 entry.state = TableState::Dropped { at_epoch: epoch.0 };
10784 next_catalog.triggers.retain(|trigger| {
10785 !matches!(
10786 &trigger.trigger.target,
10787 TriggerTarget::Table(target) if target == name
10788 )
10789 });
10790 next_catalog
10791 .materialized_views
10792 .retain(|definition| definition.name != name);
10793 next_catalog
10794 .security
10795 .rls_tables
10796 .retain(|table| table != name);
10797 next_catalog
10798 .security
10799 .policies
10800 .retain(|policy| policy.table != name);
10801 next_catalog
10802 .security
10803 .masks
10804 .retain(|mask| mask.table != name);
10805 for role in &mut next_catalog.roles {
10806 role.permissions
10807 .retain(|permission| permission_table(permission) != Some(name));
10808 }
10809 advance_security_version(&mut next_catalog)?;
10810 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
10811 let commit_seq = {
10812 let mut wal = self.shared_wal.lock();
10813 if let Some(before_commit) = before_commit {
10814 before_commit()?;
10815 }
10816 let append: Result<u64> = (|| {
10817 wal.append(
10818 txn_id,
10819 table_id,
10820 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
10821 )?;
10822 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
10823 wal.append_commit(txn_id, epoch, &[])
10824 })();
10825 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
10826 };
10827 self.await_durable_commit(commit_seq, epoch)?;
10828
10829 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
10830 self.tables.write().remove(&table_id);
10831 self.finish_durable_publish(epoch, &mut _epoch_guard, checkpoint)?;
10832 Ok(epoch)
10833 }
10834
10835 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
10844 self.rename_table_with_epoch(name, new_name).map(|_| ())
10845 }
10846
10847 pub fn rename_table_with_epoch(&self, name: &str, new_name: &str) -> Result<Epoch> {
10849 self.rename_table_with_epoch_inner(name, new_name, None)
10850 }
10851
10852 pub fn rename_table_with_epoch_controlled<F>(
10853 &self,
10854 name: &str,
10855 new_name: &str,
10856 mut before_commit: F,
10857 ) -> Result<Epoch>
10858 where
10859 F: FnMut() -> Result<()>,
10860 {
10861 self.rename_table_with_epoch_inner(name, new_name, Some(&mut before_commit))
10862 }
10863
10864 fn rename_table_with_epoch_inner(
10865 &self,
10866 name: &str,
10867 new_name: &str,
10868 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10869 ) -> Result<Epoch> {
10870 if name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10871 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10872 {
10873 return Err(MongrelError::InvalidArgument(
10874 "the CTAS building-table namespace is reserved".into(),
10875 ));
10876 }
10877 self.rename_table_with_state(name, new_name, false, None, before_commit)
10878 }
10879
10880 #[doc(hidden)]
10882 pub fn publish_building_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
10883 self.publish_building_table_inner(build_name, new_name, None)
10884 }
10885
10886 #[doc(hidden)]
10887 pub fn publish_building_table_controlled<F>(
10888 &self,
10889 build_name: &str,
10890 new_name: &str,
10891 mut before_commit: F,
10892 ) -> Result<Epoch>
10893 where
10894 F: FnMut() -> Result<()>,
10895 {
10896 self.publish_building_table_inner(build_name, new_name, Some(&mut before_commit))
10897 }
10898
10899 fn publish_building_table_inner(
10900 &self,
10901 build_name: &str,
10902 new_name: &str,
10903 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10904 ) -> Result<Epoch> {
10905 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10906 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10907 {
10908 return Err(MongrelError::InvalidArgument(
10909 "invalid CTAS publish identity".into(),
10910 ));
10911 }
10912 self.rename_table_with_state(build_name, new_name, true, None, before_commit)
10913 }
10914
10915 #[doc(hidden)]
10917 pub fn publish_materialized_building_table(
10918 &self,
10919 build_name: &str,
10920 new_name: &str,
10921 definition: crate::catalog::MaterializedViewEntry,
10922 ) -> Result<Epoch> {
10923 self.publish_materialized_building_table_inner(build_name, new_name, definition, None)
10924 }
10925
10926 #[doc(hidden)]
10927 pub fn publish_materialized_building_table_controlled<F>(
10928 &self,
10929 build_name: &str,
10930 new_name: &str,
10931 definition: crate::catalog::MaterializedViewEntry,
10932 mut before_commit: F,
10933 ) -> Result<Epoch>
10934 where
10935 F: FnMut() -> Result<()>,
10936 {
10937 self.publish_materialized_building_table_inner(
10938 build_name,
10939 new_name,
10940 definition,
10941 Some(&mut before_commit),
10942 )
10943 }
10944
10945 fn publish_materialized_building_table_inner(
10946 &self,
10947 build_name: &str,
10948 new_name: &str,
10949 definition: crate::catalog::MaterializedViewEntry,
10950 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10951 ) -> Result<Epoch> {
10952 if definition.name != new_name || definition.query.trim().is_empty() {
10953 return Err(MongrelError::InvalidArgument(
10954 "invalid materialized-view publication".into(),
10955 ));
10956 }
10957 self.rename_table_with_state(build_name, new_name, true, Some(definition), before_commit)
10958 }
10959
10960 #[doc(hidden)]
10962 pub fn publish_rebuilding_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
10963 self.publish_rebuilding_table_inner(build_name, new_name, None, None)
10964 }
10965
10966 #[doc(hidden)]
10967 pub fn publish_rebuilding_table_controlled<F>(
10968 &self,
10969 build_name: &str,
10970 new_name: &str,
10971 mut before_commit: F,
10972 ) -> Result<Epoch>
10973 where
10974 F: FnMut() -> Result<()>,
10975 {
10976 self.publish_rebuilding_table_inner(build_name, new_name, None, Some(&mut before_commit))
10977 }
10978
10979 #[doc(hidden)]
10981 pub fn publish_materialized_rebuilding_table_controlled<F>(
10982 &self,
10983 build_name: &str,
10984 new_name: &str,
10985 definition: crate::catalog::MaterializedViewEntry,
10986 mut before_commit: F,
10987 ) -> Result<Epoch>
10988 where
10989 F: FnMut() -> Result<()>,
10990 {
10991 self.publish_rebuilding_table_inner(
10992 build_name,
10993 new_name,
10994 Some(definition),
10995 Some(&mut before_commit),
10996 )
10997 }
10998
10999 fn publish_rebuilding_table_inner(
11000 &self,
11001 build_name: &str,
11002 new_name: &str,
11003 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
11004 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11005 ) -> Result<Epoch> {
11006 use crate::wal::DdlOp;
11007
11008 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
11009 || new_name.is_empty()
11010 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
11011 {
11012 return Err(MongrelError::InvalidArgument(
11013 "invalid rebuilding-table publish identity".into(),
11014 ));
11015 }
11016 if materialized_view.as_ref().is_some_and(|definition| {
11017 definition.name != new_name || definition.query.trim().is_empty()
11018 }) {
11019 return Err(MongrelError::InvalidArgument(
11020 "invalid materialized-view replacement".into(),
11021 ));
11022 }
11023 self.require(&crate::auth::Permission::Ddl)?;
11024 if self.poisoned.load(Ordering::Relaxed) {
11025 return Err(MongrelError::Other(
11026 "database poisoned by fsync error".into(),
11027 ));
11028 }
11029
11030 let _ddl = self.ddl_lock.lock();
11031 let _security_write = self.security_write()?;
11032 let (table_id, replaced_table_id) = {
11033 let catalog = self.catalog.read();
11034 let build = catalog.building(build_name).ok_or_else(|| {
11035 MongrelError::NotFound(format!("building table {build_name:?} not found"))
11036 })?;
11037 let replaced_table_id = match &build.state {
11038 TableState::Building {
11039 intended_name,
11040 replaces_table_id: Some(replaced_table_id),
11041 ..
11042 } if intended_name == new_name => *replaced_table_id,
11043 _ => {
11044 return Err(MongrelError::InvalidArgument(format!(
11045 "building table {build_name:?} is not a replacement for {new_name:?}"
11046 )))
11047 }
11048 };
11049 if catalog
11050 .live(new_name)
11051 .is_none_or(|entry| entry.table_id != replaced_table_id)
11052 {
11053 return Err(MongrelError::Conflict(format!(
11054 "table {new_name:?} changed while its replacement was built"
11055 )));
11056 }
11057 (build.table_id, replaced_table_id)
11058 };
11059
11060 let _commit = self.commit_lock.lock();
11061 let epoch = self.epoch.assigned().next();
11062 let txn_id = self.alloc_txn_id()?;
11063 let mut next_catalog = self.catalog.read().clone();
11064 apply_rebuilding_publish(
11065 &mut next_catalog,
11066 table_id,
11067 replaced_table_id,
11068 new_name,
11069 epoch.0,
11070 )?;
11071 if let Some(definition) = materialized_view.as_mut() {
11072 definition.last_refresh_epoch = epoch.0;
11073 }
11074 let materialized_view_json = materialized_view
11075 .as_ref()
11076 .map(DdlOp::encode_materialized_view)
11077 .transpose()?;
11078 if let Some(definition) = materialized_view {
11079 if let Some(existing) = next_catalog
11080 .materialized_views
11081 .iter_mut()
11082 .find(|existing| existing.name == definition.name)
11083 {
11084 *existing = definition;
11085 } else {
11086 next_catalog.materialized_views.push(definition);
11087 }
11088 }
11089 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11090 if let Some(before_commit) = before_commit {
11091 before_commit()?;
11092 }
11093 let assigned_epoch = self.epoch.bump_assigned();
11094 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
11095 if assigned_epoch != epoch {
11096 return Err(MongrelError::Conflict(
11097 "commit epoch changed while sequencer lock was held".into(),
11098 ));
11099 }
11100 let commit_seq = {
11101 let mut wal = self.shared_wal.lock();
11102 let append: Result<u64> = (|| {
11103 wal.append(
11104 txn_id,
11105 table_id,
11106 crate::wal::Op::Ddl(DdlOp::ReplaceBuildingTable {
11107 table_id,
11108 replaced_table_id,
11109 new_name: new_name.to_string(),
11110 }),
11111 )?;
11112 if let Some(definition_json) = materialized_view_json {
11113 wal.append(
11114 txn_id,
11115 table_id,
11116 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
11117 name: new_name.to_string(),
11118 definition_json,
11119 }),
11120 )?;
11121 }
11122 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11123 wal.append_commit(txn_id, epoch, &[])
11124 })();
11125 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11126 };
11127 self.await_durable_commit(commit_seq, epoch)?;
11128
11129 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
11130 self.tables.write().remove(&replaced_table_id);
11131 if let Some(table) = self.tables.read().get(&table_id) {
11132 table.lock().set_catalog_name(new_name.to_string());
11133 }
11134 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
11135 Ok(epoch)
11136 }
11137
11138 fn rename_table_with_state(
11139 &self,
11140 name: &str,
11141 new_name: &str,
11142 building: bool,
11143 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
11144 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11145 ) -> Result<Epoch> {
11146 use crate::wal::DdlOp;
11147 use std::sync::atomic::Ordering;
11148
11149 self.require(&crate::auth::Permission::Ddl)?;
11150 if self.poisoned.load(Ordering::Relaxed) {
11151 return Err(MongrelError::Other(
11152 "database poisoned by fsync error".into(),
11153 ));
11154 }
11155
11156 if name == new_name {
11159 return Ok(self.visible_epoch());
11160 }
11161 if new_name.is_empty() {
11162 return Err(MongrelError::InvalidArgument(
11163 "rename_table: new name must not be empty".into(),
11164 ));
11165 }
11166
11167 let _g = self.ddl_lock.lock();
11168 let _security_write = self.security_write()?;
11169 self.require(&crate::auth::Permission::Ddl)?;
11170 let table_id = {
11171 let cat = self.catalog.read();
11172 let src = if building {
11173 cat.building(name)
11174 } else {
11175 cat.live(name)
11176 }
11177 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
11178 if building
11179 && !matches!(
11180 &src.state,
11181 TableState::Building { intended_name, .. } if intended_name == new_name
11182 )
11183 {
11184 return Err(MongrelError::InvalidArgument(format!(
11185 "building table {name:?} is not reserved for {new_name:?}"
11186 )));
11187 }
11188 if cat.live(new_name).is_some() || (!building && cat.building_for(new_name).is_some()) {
11192 return Err(MongrelError::InvalidArgument(format!(
11193 "rename_table: a table named {new_name:?} already exists"
11194 )));
11195 }
11196 src.table_id
11197 };
11198
11199 let commit_lock = Arc::clone(&self.commit_lock);
11200 let _c = commit_lock.lock();
11201 let epoch = self.epoch.bump_assigned();
11202 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
11203 let txn_id = self.alloc_txn_id()?;
11204 if let Some(definition) = materialized_view.as_mut() {
11205 definition.last_refresh_epoch = epoch.0;
11206 }
11207 let materialized_view_json = materialized_view
11208 .as_ref()
11209 .map(DdlOp::encode_materialized_view)
11210 .transpose()?;
11211 let mut next_catalog = self.catalog.read().clone();
11212 let entry = next_catalog
11213 .tables
11214 .iter_mut()
11215 .find(|t| t.table_id == table_id)
11216 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
11217 entry.name = new_name.to_string();
11218 if building {
11219 entry.state = TableState::Live;
11220 }
11221 for trigger in &mut next_catalog.triggers {
11222 if matches!(
11223 &trigger.trigger.target,
11224 TriggerTarget::Table(target) if target == name
11225 ) {
11226 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
11227 }
11228 }
11229 if let Some(definition) = next_catalog
11230 .materialized_views
11231 .iter_mut()
11232 .find(|definition| definition.name == name)
11233 {
11234 definition.name = new_name.to_string();
11235 }
11236 if let Some(definition) = materialized_view.take() {
11237 next_catalog.materialized_views.push(definition);
11238 }
11239 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11240 for table in &mut next_catalog.security.rls_tables {
11241 if table == name {
11242 *table = new_name.to_string();
11243 }
11244 }
11245 for policy in &mut next_catalog.security.policies {
11246 if policy.table == name {
11247 policy.table = new_name.to_string();
11248 }
11249 }
11250 for mask in &mut next_catalog.security.masks {
11251 if mask.table == name {
11252 mask.table = new_name.to_string();
11253 }
11254 }
11255 for role in &mut next_catalog.roles {
11256 for permission in &mut role.permissions {
11257 rename_permission_table(permission, name, new_name);
11258 }
11259 }
11260 advance_security_version(&mut next_catalog)?;
11261 let ddl = if building {
11262 DdlOp::PublishBuildingTable {
11263 table_id,
11264 new_name: new_name.to_string(),
11265 }
11266 } else {
11267 DdlOp::RenameTable {
11268 table_id,
11269 new_name: new_name.to_string(),
11270 }
11271 };
11272 let commit_seq = {
11273 let mut wal = self.shared_wal.lock();
11274 if let Some(before_commit) = before_commit {
11275 before_commit()?;
11276 }
11277 let append: Result<u64> = (|| {
11278 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
11279 if let Some(definition_json) = materialized_view_json {
11280 wal.append(
11281 txn_id,
11282 table_id,
11283 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
11284 name: new_name.to_string(),
11285 definition_json,
11286 }),
11287 )?;
11288 }
11289 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11290 wal.append_commit(txn_id, epoch, &[])
11291 })();
11292 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11293 };
11294 self.await_durable_commit(commit_seq, epoch)?;
11295
11296 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
11297 if let Some(table) = self.tables.read().get(&table_id) {
11300 table.lock().set_catalog_name(new_name.to_string());
11301 }
11302 self.finish_durable_publish(epoch, &mut _epoch_guard, checkpoint)?;
11303 Ok(epoch)
11304 }
11305
11306 pub fn alter_column(
11307 &self,
11308 table_name: &str,
11309 column_name: &str,
11310 change: AlterColumn,
11311 ) -> Result<ColumnDef> {
11312 self.alter_column_with_epoch(table_name, column_name, change)
11313 .map(|(column, _)| column)
11314 }
11315
11316 pub fn alter_column_with_epoch(
11317 &self,
11318 table_name: &str,
11319 column_name: &str,
11320 change: AlterColumn,
11321 ) -> Result<(ColumnDef, Option<Epoch>)> {
11322 self.alter_column_with_epoch_inner(table_name, column_name, change, None, None, None)
11323 }
11324
11325 pub fn alter_column_with_epoch_controlled<B, A>(
11330 &self,
11331 table_name: &str,
11332 column_name: &str,
11333 change: AlterColumn,
11334 control: &crate::ExecutionControl,
11335 mut before_commit: B,
11336 mut after_commit: A,
11337 ) -> Result<(ColumnDef, Option<Epoch>)>
11338 where
11339 B: FnMut() -> Result<()>,
11340 A: FnMut(Option<Epoch>) -> Result<()>,
11341 {
11342 self.alter_column_with_epoch_inner(
11343 table_name,
11344 column_name,
11345 change,
11346 Some(control),
11347 Some(&mut before_commit),
11348 Some(&mut after_commit),
11349 )
11350 }
11351
11352 #[allow(clippy::too_many_arguments)]
11353 fn alter_column_with_epoch_inner(
11354 &self,
11355 table_name: &str,
11356 column_name: &str,
11357 change: AlterColumn,
11358 control: Option<&crate::ExecutionControl>,
11359 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11360 mut after_commit: Option<&mut dyn FnMut(Option<Epoch>) -> Result<()>>,
11361 ) -> Result<(ColumnDef, Option<Epoch>)> {
11362 use crate::wal::DdlOp;
11363 use std::sync::atomic::Ordering;
11364
11365 self.require(&crate::auth::Permission::Ddl)?;
11366 commit_prepare_checkpoint(control, 0)?;
11367 if self.poisoned.load(Ordering::Relaxed) {
11368 return Err(MongrelError::Other(
11369 "database poisoned by fsync error".into(),
11370 ));
11371 }
11372
11373 let _g = self.ddl_lock.lock();
11374 let table_id = {
11375 let cat = self.catalog.read();
11376 cat.live(table_name)
11377 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
11378 .table_id
11379 };
11380 let handle =
11381 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
11382 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
11383 })?;
11384
11385 let backfill = {
11391 let table = handle.lock();
11392 let old = table
11393 .schema()
11394 .column(column_name)
11395 .cloned()
11396 .ok_or_else(|| MongrelError::Schema(format!("unknown column {column_name}")))?;
11397 let next_flags = change.flags.unwrap_or(old.flags);
11398 if old.flags.contains(crate::schema::ColumnFlags::NULLABLE)
11399 && !next_flags.contains(crate::schema::ColumnFlags::NULLABLE)
11400 && old.default_value.is_some()
11401 {
11402 let snapshot = Snapshot::at(self.epoch.visible());
11403 let mut updates = Vec::new();
11404 let rows = match control {
11405 Some(control) => table.visible_rows_controlled(snapshot, control)?,
11406 None => table.visible_rows(snapshot)?,
11407 };
11408 for (row_index, row) in rows.into_iter().enumerate() {
11409 commit_prepare_checkpoint(control, row_index)?;
11410 if row
11411 .columns
11412 .get(&old.id)
11413 .is_some_and(|value| !matches!(value, Value::Null))
11414 {
11415 continue;
11416 }
11417 let mut cells: Vec<(u16, Value)> = row.columns.into_iter().collect();
11418 table.apply_defaults(&mut cells)?;
11419 updates.push((
11420 table_id,
11421 crate::txn::Staged::Update {
11422 row_id: row.row_id,
11423 new_row: cells,
11424 changed_columns: vec![old.id],
11425 },
11426 ));
11427 }
11428 updates
11429 } else {
11430 Vec::new()
11431 }
11432 };
11433 let durable_epoch = std::cell::Cell::new(None);
11434 let backfill_epoch = if backfill.is_empty() {
11435 None
11436 } else {
11437 let (principal, catalog_bound) = self.transaction_principal_snapshot();
11438 let txn_id = self.alloc_txn_id()?;
11439 let mut entered_fence = false;
11440 let commit_result = match (control, before_commit.as_deref_mut()) {
11441 (Some(control), Some(before_commit)) => self
11442 .commit_transaction_with_external_states_controlled(
11443 txn_id,
11444 self.epoch.visible(),
11445 backfill,
11446 Vec::new(),
11447 Vec::new(),
11448 principal.clone(),
11449 catalog_bound,
11450 None,
11451 control,
11452 &mut || {
11453 before_commit()?;
11454 entered_fence = true;
11455 Ok(())
11456 },
11457 )
11458 .map(|(epoch, _)| epoch),
11459 _ => self
11460 .commit_transaction_with_external_states(
11461 txn_id,
11462 self.epoch.visible(),
11463 backfill,
11464 Vec::new(),
11465 Vec::new(),
11466 principal,
11467 catalog_bound,
11468 None,
11469 )
11470 .map(|(epoch, _)| epoch),
11471 };
11472 let commit_result = if entered_fence {
11473 finish_controlled_commit_attempt(commit_result, &mut after_commit)
11474 } else {
11475 commit_result
11476 };
11477 match &commit_result {
11478 Ok(epoch) => durable_epoch.set(Some(*epoch)),
11479 Err(MongrelError::DurableCommit { epoch, .. }) => {
11480 durable_epoch.set(Some(Epoch(*epoch)));
11481 }
11482 Err(_) => {}
11483 }
11484 Some(commit_result?)
11485 };
11486 let result: Result<(ColumnDef, Option<Epoch>)> = (|| {
11487 let _security_write = self.security_write()?;
11488 self.require(&crate::auth::Permission::Ddl)?;
11489 if self
11490 .catalog
11491 .read()
11492 .live(table_name)
11493 .is_none_or(|entry| entry.table_id != table_id)
11494 {
11495 return Err(MongrelError::Conflict(format!(
11496 "table {table_name:?} changed during ALTER"
11497 )));
11498 }
11499 let mut table = handle.lock();
11500 let (column, prepared_schema) = table.prepare_alter_column(column_name, &change)?;
11501 let renamed_column = (column.name != column_name).then(|| column.name.clone());
11502 let Some(prepared_schema) = prepared_schema else {
11503 return Ok((column, backfill_epoch));
11504 };
11505
11506 let commit_lock = Arc::clone(&self.commit_lock);
11507 let _c = commit_lock.lock();
11508 let epoch = self.epoch.bump_assigned();
11509 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
11510 let txn_id = self.alloc_txn_id()?;
11511 let column_json = DdlOp::encode_column(&column)?;
11512 let mut next_catalog = self.catalog.read().clone();
11513 let catalog_entry_index = next_catalog
11514 .tables
11515 .iter()
11516 .position(|entry| entry.table_id == table_id)
11517 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
11518 if let Some(new_column_name) = &renamed_column {
11519 for (trigger_index, trigger) in next_catalog.triggers.iter_mut().enumerate() {
11520 commit_prepare_checkpoint(control, trigger_index)?;
11521 if matches!(
11522 &trigger.trigger.target,
11523 TriggerTarget::Table(target) if target == table_name
11524 ) {
11525 trigger.trigger = trigger.trigger.renamed_update_column(
11526 column_name,
11527 new_column_name.clone(),
11528 epoch.0,
11529 )?;
11530 }
11531 }
11532 for (role_index, role) in next_catalog.roles.iter_mut().enumerate() {
11533 commit_prepare_checkpoint(control, role_index)?;
11534 for (permission_index, permission) in role.permissions.iter_mut().enumerate() {
11535 commit_prepare_checkpoint(control, permission_index)?;
11536 rename_permission_column(
11537 permission,
11538 table_name,
11539 column_name,
11540 new_column_name,
11541 );
11542 }
11543 }
11544 advance_security_version(&mut next_catalog)?;
11545 }
11546 next_catalog.tables[catalog_entry_index].schema = prepared_schema.clone();
11547 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11548 commit_prepare_checkpoint(control, 0)?;
11549 let mut entered_fence = false;
11550 if let Some(before_commit) = before_commit.as_deref_mut() {
11551 before_commit()?;
11552 entered_fence = true;
11553 }
11554 let commit_result: Result<Epoch> = (|| {
11555 let commit_seq = {
11556 let mut wal = self.shared_wal.lock();
11557 let append: Result<u64> = (|| {
11558 wal.append(
11559 txn_id,
11560 table_id,
11561 crate::wal::Op::Ddl(DdlOp::AlterTable {
11562 table_id,
11563 column_json,
11564 }),
11565 )?;
11566 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11567 wal.append_commit(txn_id, epoch, &[])
11568 })();
11569 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11570 };
11571 self.await_durable_commit(commit_seq, epoch)?;
11572 durable_epoch.set(Some(epoch));
11573
11574 table.apply_altered_schema_prepared(prepared_schema);
11575 let schema = table.schema().clone();
11576 let table_checkpoint = table.checkpoint_altered_schema();
11577 drop(table);
11578 next_catalog.tables[catalog_entry_index].schema = schema;
11579 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11580 let catalog_result =
11581 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref());
11582 let security_version = next_catalog.security_version;
11583 *self.catalog.write() = next_catalog;
11584 if renamed_column.is_some() {
11585 self.security_coordinator
11586 .version
11587 .store(security_version, Ordering::Release);
11588 }
11589 self.epoch.publish_in_order(epoch);
11590 _epoch_guard.disarm();
11591 if let Err(error) = table_checkpoint.and(catalog_result) {
11592 self.poisoned.store(true, Ordering::Relaxed);
11593 return Err(MongrelError::DurableCommit {
11594 epoch: epoch.0,
11595 message: error.to_string(),
11596 });
11597 }
11598 Ok(epoch)
11599 })();
11600 let commit_result = if entered_fence {
11601 finish_controlled_commit_attempt(commit_result, &mut after_commit)
11602 } else {
11603 commit_result
11604 };
11605 let epoch = commit_result?;
11606 Ok((column, Some(epoch)))
11607 })();
11608 result.map_err(|error| match (durable_epoch.get(), error) {
11609 (_, error @ MongrelError::DurableCommit { .. }) => error,
11610 (Some(epoch), error) => MongrelError::DurableCommit {
11611 epoch: epoch.0,
11612 message: error.to_string(),
11613 },
11614 (None, error) => error,
11615 })
11616 }
11617
11618 pub fn set_table_ttl(
11621 &self,
11622 table_name: &str,
11623 column_name: &str,
11624 duration_nanos: u64,
11625 ) -> Result<crate::manifest::TtlPolicy> {
11626 let policy = self.replace_table_ttl(table_name, Some((column_name, duration_nanos)))?;
11627 policy.ok_or_else(|| MongrelError::Other("set TTL produced no policy".into()))
11628 }
11629
11630 #[doc(hidden)]
11632 pub fn set_building_table_ttl(
11633 &self,
11634 table_name: &str,
11635 column_name: &str,
11636 duration_nanos: u64,
11637 ) -> Result<crate::manifest::TtlPolicy> {
11638 let policy = self.replace_table_ttl_with_state(
11639 table_name,
11640 Some((column_name, duration_nanos)),
11641 true,
11642 )?;
11643 policy
11644 .ok_or_else(|| MongrelError::Other("set building-table TTL produced no policy".into()))
11645 }
11646
11647 pub fn clear_table_ttl(&self, table_name: &str) -> Result<()> {
11648 self.replace_table_ttl(table_name, None)?;
11649 Ok(())
11650 }
11651
11652 fn replace_table_ttl(
11653 &self,
11654 table_name: &str,
11655 requested: Option<(&str, u64)>,
11656 ) -> Result<Option<crate::manifest::TtlPolicy>> {
11657 self.replace_table_ttl_with_state(table_name, requested, false)
11658 }
11659
11660 fn replace_table_ttl_with_state(
11661 &self,
11662 table_name: &str,
11663 requested: Option<(&str, u64)>,
11664 building: bool,
11665 ) -> Result<Option<crate::manifest::TtlPolicy>> {
11666 use crate::wal::DdlOp;
11667 use std::sync::atomic::Ordering;
11668
11669 self.require(&crate::auth::Permission::Ddl)?;
11670 if self.poisoned.load(Ordering::Relaxed) {
11671 return Err(MongrelError::Other(
11672 "database poisoned by fsync error".into(),
11673 ));
11674 }
11675
11676 let _g = self.ddl_lock.lock();
11677 let _security_write = self.security_write()?;
11678 self.require(&crate::auth::Permission::Ddl)?;
11679 let table_id = {
11680 let cat = self.catalog.read();
11681 if building {
11682 cat.building(table_name)
11683 } else {
11684 cat.live(table_name)
11685 }
11686 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
11687 .table_id
11688 };
11689 let handle =
11690 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
11691 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
11692 })?;
11693 let mut table = handle.lock();
11694 let policy = match requested {
11695 Some((column, duration)) => Some(table.prepare_ttl_policy(column, duration)?),
11696 None => None,
11697 };
11698 if table.ttl() == policy {
11699 return Ok(policy);
11700 }
11701
11702 let commit_lock = Arc::clone(&self.commit_lock);
11703 let _c = commit_lock.lock();
11704 let epoch = self.epoch.bump_assigned();
11705 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
11706 let txn_id = self.alloc_txn_id()?;
11707 let policy_json = DdlOp::encode_ttl(policy)?;
11708 let mut next_catalog = self.catalog.read().clone();
11709 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11710 let commit_seq = {
11711 let mut wal = self.shared_wal.lock();
11712 let append: Result<u64> = (|| {
11713 wal.append(
11714 txn_id,
11715 table_id,
11716 crate::wal::Op::Ddl(DdlOp::SetTtl {
11717 table_id,
11718 policy_json,
11719 }),
11720 )?;
11721 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11722 wal.append_commit(txn_id, epoch, &[])
11723 })();
11724 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11725 };
11726 self.await_durable_commit(commit_seq, epoch)?;
11727
11728 let mut publish_error = table.apply_ttl_policy_at(policy, epoch).err();
11729 drop(table);
11730 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
11731 publish_error.get_or_insert(error);
11732 }
11733 self.finish_durable_publish(epoch, &mut _epoch_guard, publish_error.map_or(Ok(()), Err))?;
11734 Ok(policy)
11735 }
11736
11737 pub fn gc(&self) -> Result<usize> {
11743 let control = crate::ExecutionControl::new(None);
11744 self.gc_controlled(&control, || true)
11745 }
11746
11747 #[doc(hidden)]
11750 pub fn gc_controlled<F>(
11751 &self,
11752 control: &crate::ExecutionControl,
11753 before_publish: F,
11754 ) -> Result<usize>
11755 where
11756 F: FnOnce() -> bool,
11757 {
11758 self.gc_controlled_with_receipt(control, before_publish)
11759 .map(|(reclaimed, _)| reclaimed)
11760 }
11761
11762 #[doc(hidden)]
11765 pub fn gc_controlled_with_receipt<F>(
11766 &self,
11767 control: &crate::ExecutionControl,
11768 before_publish: F,
11769 ) -> Result<(usize, Option<MaintenanceReceipt>)>
11770 where
11771 F: FnOnce() -> bool,
11772 {
11773 enum Candidate {
11774 Directory(PathBuf),
11775 File(PathBuf),
11776 }
11777
11778 self.require(&crate::auth::Permission::Ddl)?;
11779 let _ddl = self.ddl_lock.lock();
11780 self.require(&crate::auth::Permission::Ddl)?;
11781 control.checkpoint()?;
11782 let maintenance_epoch = self.epoch.visible();
11783 let min_active = self.snapshots.min_active(maintenance_epoch).0;
11784 let mut candidates = Vec::new();
11785
11786 let cat = self.catalog.read();
11788 for (entry_index, entry) in cat.tables.iter().enumerate() {
11789 if entry_index % 256 == 0 {
11790 control.checkpoint()?;
11791 }
11792 if let TableState::Dropped { at_epoch } = entry.state {
11793 if at_epoch <= min_active {
11794 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
11795 if tdir.exists() {
11796 candidates.push(Candidate::Directory(tdir));
11797 }
11798 }
11799 }
11800 }
11801 drop(cat);
11802
11803 let cat = self.catalog.read();
11808 for (entry_index, entry) in cat.tables.iter().enumerate() {
11809 if entry_index % 256 == 0 {
11810 control.checkpoint()?;
11811 }
11812 if !matches!(entry.state, TableState::Live) {
11813 continue;
11814 }
11815 let txn_dir = self
11816 .root
11817 .join(TABLES_DIR)
11818 .join(entry.table_id.to_string())
11819 .join("_txn");
11820 if !txn_dir.exists() {
11821 continue;
11822 }
11823 for (sub_index, sub) in std::fs::read_dir(&txn_dir)?.enumerate() {
11824 if sub_index % 256 == 0 {
11825 control.checkpoint()?;
11826 }
11827 let sub = sub?;
11828 let name = sub.file_name();
11829 let Some(name) = name.to_str() else { continue };
11830 let is_active = name
11832 .parse::<u64>()
11833 .map(|id| self.active_spills.is_active(id))
11834 .unwrap_or(false);
11835 if is_active {
11836 continue;
11837 }
11838 candidates.push(Candidate::Directory(sub.path()));
11839 }
11840 }
11841 drop(cat);
11842
11843 let external_names = {
11844 let cat = self.catalog.read();
11845 cat.external_tables
11846 .iter()
11847 .map(|entry| entry.name.clone())
11848 .collect::<std::collections::HashSet<_>>()
11849 };
11850 let vtab_dir = self.root.join(VTAB_DIR);
11851 if vtab_dir.exists() {
11852 for (entry_index, entry) in std::fs::read_dir(&vtab_dir)?.enumerate() {
11853 if entry_index % 256 == 0 {
11854 control.checkpoint()?;
11855 }
11856 let entry = entry?;
11857 let name = entry.file_name();
11858 let Some(name) = name.to_str() else { continue };
11859 if external_names.contains(name) {
11860 continue;
11861 }
11862 let path = entry.path();
11863 if path.is_dir() {
11864 candidates.push(Candidate::Directory(path));
11865 } else {
11866 candidates.push(Candidate::File(path));
11867 }
11868 }
11869 }
11870
11871 let tables = self
11875 .tables
11876 .read()
11877 .iter()
11878 .map(|(table_id, handle)| (*table_id, handle.clone()))
11879 .collect::<Vec<_>>();
11880 let mut retiring = Vec::new();
11881 for (table_index, (table_id, handle)) in tables.iter().enumerate() {
11882 if table_index % 256 == 0 {
11883 control.checkpoint()?;
11884 }
11885 let backup_pinned: HashSet<u128> = self
11886 .backup_pins
11887 .lock()
11888 .keys()
11889 .filter_map(|(pinned_table, run_id)| {
11890 (*pinned_table == *table_id).then_some(*run_id)
11891 })
11892 .collect();
11893 if handle
11894 .lock()
11895 .has_reapable_retiring(Epoch(min_active), &backup_pinned)
11896 {
11897 retiring.push((handle.clone(), backup_pinned));
11898 }
11899 }
11900
11901 let all_durable = self.active_spills.is_idle()
11909 && tables.iter().all(|(_, handle)| {
11910 let g = handle.lock();
11911 g.memtable_len() == 0 && g.mutable_run_len() == 0
11912 });
11913 let retain = self
11914 .replication_wal_retention_segments
11915 .load(std::sync::atomic::Ordering::Relaxed);
11916 let reap_wal = all_durable
11917 && self
11918 .shared_wal
11919 .lock()
11920 .has_gc_segments_retain_recent(retain)?;
11921
11922 if candidates.is_empty() && retiring.is_empty() && !reap_wal {
11923 return Ok((0, None));
11924 }
11925 control.checkpoint()?;
11926 if !before_publish() {
11927 return Err(MongrelError::Cancelled);
11928 }
11929
11930 let mut reclaimed = 0;
11931 for candidate in candidates {
11932 match candidate {
11933 Candidate::Directory(path) => std::fs::remove_dir_all(path)?,
11934 Candidate::File(path) => std::fs::remove_file(path)?,
11935 }
11936 reclaimed += 1;
11937 }
11938 for (handle, backup_pinned) in retiring {
11939 reclaimed += handle
11940 .lock()
11941 .reap_retiring(Epoch(min_active), &backup_pinned)?;
11942 }
11943 if reap_wal {
11944 reclaimed += self
11945 .shared_wal
11946 .lock()
11947 .gc_segments_retain_recent(u64::MAX, retain)?;
11948 }
11949
11950 Ok((
11951 reclaimed,
11952 Some(MaintenanceReceipt {
11953 epoch: maintenance_epoch,
11954 }),
11955 ))
11956 }
11957
11958 pub fn checkpoint(&self) -> Result<()> {
11976 self.checkpoint_controlled(|| Ok(()))
11977 }
11978
11979 #[doc(hidden)]
11982 pub fn checkpoint_controlled<F>(&self, before_wal_reset: F) -> Result<()>
11983 where
11984 F: FnOnce() -> Result<()>,
11985 {
11986 self.require(&crate::auth::Permission::Ddl)?;
11987 let _replication = self.replication_barrier.write();
11991 let _ddl = self.ddl_lock.lock();
11992 let _security = self.security_coordinator.gate.read();
11993 self.require(&crate::auth::Permission::Ddl)?;
11994
11995 let mut handles = self
11996 .tables
11997 .read()
11998 .iter()
11999 .map(|(table_id, handle)| (*table_id, handle.clone()))
12000 .collect::<Vec<_>>();
12001 handles.sort_by_key(|(table_id, _)| *table_id);
12002 let mut tables = handles
12003 .iter()
12004 .map(|(table_id, handle)| (*table_id, handle.lock()))
12005 .collect::<Vec<_>>();
12006
12007 for (_, table) in &mut tables {
12009 if table.has_pending_writes() || table.memtable_len() > 0 || table.mutable_run_len() > 0
12010 {
12011 table.force_flush()?;
12012 }
12013 }
12014
12015 for (_, table) in &mut tables {
12018 if table.run_count() >= 2 || table.should_compact() {
12019 table.compact()?;
12020 }
12021 }
12022
12023 before_wal_reset()?;
12024
12025 let maintenance_epoch = self.epoch.visible();
12027 let min_active = self.snapshots.min_active(maintenance_epoch);
12028 for (table_id, table) in &mut tables {
12029 let backup_pinned: HashSet<u128> = self
12030 .backup_pins
12031 .lock()
12032 .keys()
12033 .filter_map(|(pinned_table, run_id)| {
12034 (*pinned_table == *table_id).then_some(*run_id)
12035 })
12036 .collect();
12037 table.reap_retiring(min_active, &backup_pinned)?;
12038 }
12039
12040 self.shared_wal.lock().reset_after_checkpoint()?;
12043
12044 let catalog_snapshot = self.catalog.read().clone();
12046 for entry in &catalog_snapshot.tables {
12047 if matches!(entry.state, TableState::Dropped { at_epoch } if at_epoch <= min_active.0) {
12048 crate::durable_file::remove_directory_all(
12049 &self.root.join(TABLES_DIR).join(entry.table_id.to_string()),
12050 )?;
12051 }
12052 if !matches!(entry.state, TableState::Live) {
12053 continue;
12054 }
12055 let transaction_dir = self
12056 .root
12057 .join(TABLES_DIR)
12058 .join(entry.table_id.to_string())
12059 .join("_txn");
12060 if transaction_dir.is_dir() {
12061 for child in std::fs::read_dir(&transaction_dir)? {
12062 let child = child?;
12063 let active = child
12064 .file_name()
12065 .to_str()
12066 .and_then(|name| name.parse::<u64>().ok())
12067 .is_some_and(|txn_id| self.active_spills.is_active(txn_id));
12068 if !active {
12069 crate::durable_file::remove_directory_all(&child.path())?;
12070 }
12071 }
12072 }
12073 }
12074 let external_names = catalog_snapshot
12075 .external_tables
12076 .iter()
12077 .map(|entry| entry.name.as_str())
12078 .collect::<HashSet<_>>();
12079 let external_root = self.root.join(VTAB_DIR);
12080 if external_root.is_dir() {
12081 for entry in std::fs::read_dir(&external_root)? {
12082 let entry = entry?;
12083 let name = entry.file_name();
12084 if name
12085 .to_str()
12086 .is_some_and(|name| external_names.contains(name))
12087 {
12088 continue;
12089 }
12090 if entry.file_type()?.is_dir() {
12091 crate::durable_file::remove_directory_all(&entry.path())?;
12092 } else {
12093 std::fs::remove_file(entry.path())?;
12094 crate::durable_file::sync_directory(&external_root)?;
12095 }
12096 }
12097 }
12098
12099 catalog::write_atomic(&self.root, &catalog_snapshot, self.meta_dek.as_ref())?;
12102 let visible = self.epoch.visible();
12103 for (_, table) in &tables {
12104 table.persist_manifest(visible)?;
12105 }
12106
12107 Ok(())
12108 }
12109 fn alloc_txn_id(&self) -> Result<u64> {
12110 crate::txn::allocate_txn_id(&self.next_txn_id)
12111 }
12112
12113 pub fn set_spill_threshold(&self, bytes: u64) {
12117 self.spill_threshold
12118 .store(bytes, std::sync::atomic::Ordering::Relaxed);
12119 }
12120
12121 #[doc(hidden)]
12125 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12126 *self.spill_hook.lock() = Some(Box::new(f));
12127 }
12128
12129 #[doc(hidden)]
12132 pub fn __set_security_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12133 *self.security_commit_hook.lock() = Some(Box::new(f));
12134 }
12135
12136 #[doc(hidden)]
12139 pub fn __set_catalog_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12140 *self.catalog_commit_hook.lock() = Some(Box::new(f));
12141 }
12142
12143 #[doc(hidden)]
12146 pub fn __set_backup_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12147 *self.backup_hook.lock() = Some(Box::new(f));
12148 }
12149
12150 #[doc(hidden)]
12152 pub fn __set_replication_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12153 *self.replication_hook.lock() = Some(Box::new(f));
12154 }
12155
12156 #[doc(hidden)]
12160 pub fn __wal_group_sync_count(&self) -> u64 {
12161 self.shared_wal.lock().group_sync_count()
12162 }
12163
12164 #[doc(hidden)]
12167 pub fn __poison(&self) {
12168 self.poisoned
12169 .store(true, std::sync::atomic::Ordering::Relaxed);
12170 }
12171
12172 pub fn check(&self) -> Vec<CheckIssue> {
12185 match self.check_inner(None) {
12186 Ok(issues) => issues,
12187 Err(error) => vec![CheckIssue {
12188 table_id: WAL_TABLE_ID,
12189 table_name: "shared WAL".into(),
12190 severity: "error".into(),
12191 description: error.to_string(),
12192 }],
12193 }
12194 }
12195
12196 #[doc(hidden)]
12198 pub fn check_controlled(&self, control: &crate::ExecutionControl) -> Result<Vec<CheckIssue>> {
12199 self.check_inner(Some(control))
12200 }
12201
12202 fn check_inner(&self, control: Option<&crate::ExecutionControl>) -> Result<Vec<CheckIssue>> {
12203 let mut issues = Vec::new();
12204 let cat = self.catalog.read();
12205 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
12206 for (table_index, entry) in cat.tables.iter().enumerate() {
12207 if table_index % 256 == 0 {
12208 if let Some(control) = control {
12209 control.checkpoint()?;
12210 }
12211 }
12212 if !matches!(entry.state, TableState::Live) {
12213 continue;
12214 }
12215 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
12216 let mut err = |sev: &str, desc: String| {
12217 issues.push(CheckIssue {
12218 table_id: entry.table_id,
12219 table_name: entry.name.clone(),
12220 severity: sev.into(),
12221 description: desc,
12222 });
12223 };
12224 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
12225 Ok(m) => m,
12226 Err(e) => {
12227 err("error", format!("manifest read failed: {e}"));
12228 continue;
12229 }
12230 };
12231 if m.flushed_epoch > m.current_epoch {
12232 err(
12233 "error",
12234 format!(
12235 "flushed_epoch {} exceeds current_epoch {} (impossible)",
12236 m.flushed_epoch, m.current_epoch
12237 ),
12238 );
12239 }
12240
12241 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
12242 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
12243 for (run_index, rr) in m.runs.iter().enumerate() {
12244 if run_index % 256 == 0 {
12245 if let Some(control) = control {
12246 control.checkpoint()?;
12247 }
12248 }
12249 referenced.insert(rr.run_id);
12250 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
12251 if !run_path.exists() {
12252 err("error", format!("missing run file: r-{}.sr", rr.run_id));
12253 continue;
12254 }
12255 match crate::sorted_run::RunReader::open(
12256 &run_path,
12257 entry.schema.clone(),
12258 self.kek.clone(),
12259 ) {
12260 Ok(reader) => {
12261 if reader.row_count() as u64 != rr.row_count {
12262 err(
12263 "error",
12264 format!(
12265 "run r-{} row count mismatch: manifest {} vs run {}",
12266 rr.run_id,
12267 rr.row_count,
12268 reader.row_count()
12269 ),
12270 );
12271 }
12272 }
12273 Err(e) => {
12274 err(
12275 "error",
12276 format!("run r-{} integrity check failed: {e}", rr.run_id),
12277 );
12278 }
12279 }
12280 }
12281
12282 for r in &m.retiring {
12286 referenced.insert(r.run_id);
12287 }
12288
12289 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
12291 for (entry_index, ent) in rd.flatten().enumerate() {
12292 if entry_index % 256 == 0 {
12293 if let Some(control) = control {
12294 control.checkpoint()?;
12295 }
12296 }
12297 let p = ent.path();
12298 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
12299 continue;
12300 }
12301 let run_id = p
12302 .file_stem()
12303 .and_then(|s| s.to_str())
12304 .and_then(|s| s.strip_prefix("r-"))
12305 .and_then(|s| s.parse::<u128>().ok());
12306 if let Some(id) = run_id {
12307 if !referenced.contains(&id) {
12308 err(
12309 "warning",
12310 format!("orphan run file r-{id}.sr not referenced by the manifest"),
12311 );
12312 }
12313 }
12314 }
12315 }
12316 }
12317
12318 let external_names = cat
12319 .external_tables
12320 .iter()
12321 .map(|entry| entry.name.clone())
12322 .collect::<std::collections::HashSet<_>>();
12323 let vtab_dir = self.root.join(VTAB_DIR);
12324 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
12325 for (entry_index, entry) in entries.flatten().enumerate() {
12326 if entry_index % 256 == 0 {
12327 if let Some(control) = control {
12328 control.checkpoint()?;
12329 }
12330 }
12331 let name = entry.file_name();
12332 let Some(name) = name.to_str() else { continue };
12333 if !external_names.contains(name) {
12334 issues.push(CheckIssue {
12335 table_id: EXTERNAL_TABLE_ID,
12336 table_name: name.to_string(),
12337 severity: "warning".into(),
12338 description: format!(
12339 "orphan external table state entry {:?} not referenced by the catalog",
12340 entry.path()
12341 ),
12342 });
12343 }
12344 }
12345 }
12346
12347 if let Some(control) = control {
12354 control.checkpoint()?;
12355 }
12356 for (seg, msg) in self.shared_wal.lock().verify_segments() {
12357 issues.push(CheckIssue {
12358 table_id: WAL_TABLE_ID,
12359 table_name: "<wal>".into(),
12360 severity: "error".into(),
12361 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
12362 });
12363 }
12364 Ok(issues)
12365 }
12366
12367 pub fn doctor(&self) -> Result<Vec<u64>> {
12371 let control = crate::ExecutionControl::new(None);
12372 self.doctor_controlled(&control, || true)
12373 }
12374
12375 #[doc(hidden)]
12379 pub fn doctor_controlled<F>(
12380 &self,
12381 control: &crate::ExecutionControl,
12382 before_publish: F,
12383 ) -> Result<Vec<u64>>
12384 where
12385 F: FnOnce() -> bool,
12386 {
12387 self.doctor_controlled_with_receipt(control, before_publish)
12388 .map(|(quarantined, _)| quarantined)
12389 }
12390
12391 #[doc(hidden)]
12394 pub fn doctor_controlled_with_receipt<F>(
12395 &self,
12396 control: &crate::ExecutionControl,
12397 before_publish: F,
12398 ) -> Result<(Vec<u64>, Option<MaintenanceReceipt>)>
12399 where
12400 F: FnOnce() -> bool,
12401 {
12402 let _ddl = self.ddl_lock.lock();
12405 let _security_write = self.security_write()?;
12406 let issues = self.check_inner(Some(control))?;
12407 let bad_tables: std::collections::HashSet<u64> = issues
12412 .iter()
12413 .filter(|i| {
12414 i.severity == "error"
12415 && i.table_id != WAL_TABLE_ID
12416 && i.table_id != EXTERNAL_TABLE_ID
12417 })
12418 .map(|i| i.table_id)
12419 .collect();
12420 if bad_tables.is_empty() {
12421 return Ok((Vec::new(), None));
12422 }
12423 let _commit = self.commit_lock.lock();
12424 control.checkpoint()?;
12425 if !before_publish() {
12426 return Err(MongrelError::Cancelled);
12427 }
12428 let maintenance_epoch = self.epoch.bump_assigned();
12429 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), maintenance_epoch);
12430
12431 let qdir = self.root.join("_quarantine");
12432 crate::durable_file::create_directory(&qdir)?;
12433 let mut bad_tables = bad_tables.into_iter().collect::<Vec<_>>();
12434 bad_tables.sort_unstable();
12435
12436 let mut handles = self
12440 .tables
12441 .read()
12442 .iter()
12443 .filter(|(table_id, _)| bad_tables.binary_search(table_id).is_ok())
12444 .map(|(table_id, handle)| (*table_id, handle.clone()))
12445 .collect::<Vec<_>>();
12446 handles.sort_by_key(|(table_id, _)| *table_id);
12447 let mut table_guards = handles
12448 .iter()
12449 .map(|(table_id, handle)| (*table_id, handle.lock()))
12450 .collect::<Vec<_>>();
12451
12452 let mut next_catalog = self.catalog.read().clone();
12453 for table_id in &bad_tables {
12454 if let Some(entry) = next_catalog
12455 .tables
12456 .iter_mut()
12457 .find(|entry| entry.table_id == *table_id)
12458 {
12459 entry.state = TableState::Dropped {
12460 at_epoch: maintenance_epoch.0,
12461 };
12462 }
12463 }
12464 next_catalog.db_epoch = next_catalog.db_epoch.max(maintenance_epoch.0);
12465
12466 let txn_id = self.alloc_txn_id()?;
12467 let commit_seq = {
12468 let mut wal = self.shared_wal.lock();
12469 let append: Result<u64> = (|| {
12470 for table_id in &bad_tables {
12471 wal.append(
12472 txn_id,
12473 *table_id,
12474 crate::wal::Op::Ddl(crate::wal::DdlOp::DropTable {
12475 table_id: *table_id,
12476 }),
12477 )?;
12478 }
12479 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
12480 wal.append_commit(txn_id, maintenance_epoch, &[])
12481 })();
12482 append.map_err(|error| self.commit_outcome_unknown(maintenance_epoch, error))?
12483 };
12484 self.await_durable_commit(commit_seq, maintenance_epoch)?;
12485 for (_, table) in &mut table_guards {
12486 table.mark_unavailable_after_quarantine();
12487 }
12488 {
12489 let mut live_tables = self.tables.write();
12490 for table_id in &bad_tables {
12491 live_tables.remove(table_id);
12492 }
12493 }
12494 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
12495 self.finish_durable_publish(maintenance_epoch, &mut epoch_guard, checkpoint)?;
12496
12497 for table_id in &bad_tables {
12501 let source = self.root.join(TABLES_DIR).join(table_id.to_string());
12502 if source.exists() {
12503 let destination = qdir.join(table_id.to_string());
12504 if let Err(error) = crate::durable_file::rename(&source, &destination) {
12505 return Err(MongrelError::DurableCommit {
12506 epoch: maintenance_epoch.0,
12507 message: format!(
12508 "DOCTOR dropped table {table_id} but quarantine move failed: {error}"
12509 ),
12510 });
12511 }
12512 }
12513 }
12514 Ok((
12515 bad_tables,
12516 Some(MaintenanceReceipt {
12517 epoch: maintenance_epoch,
12518 }),
12519 ))
12520 }
12521
12522 #[allow(dead_code)]
12524 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
12525 self.kek.as_ref()
12526 }
12527
12528 #[allow(dead_code)]
12530 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
12531 &self.epoch
12532 }
12533
12534 #[allow(dead_code)]
12536 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
12537 &self.snapshots
12538 }
12539}
12540
12541fn external_state_dir(root: &Path, name: &str) -> PathBuf {
12542 root.join(VTAB_DIR).join(name)
12543}
12544
12545fn append_catalog_snapshot(
12546 wal: &mut crate::wal::SharedWal,
12547 txn_id: u64,
12548 catalog: &Catalog,
12549) -> Result<()> {
12550 let catalog_json = crate::wal::DdlOp::encode_catalog(catalog)?;
12551 wal.append(
12552 txn_id,
12553 WAL_TABLE_ID,
12554 crate::wal::Op::Ddl(crate::wal::DdlOp::CatalogSnapshot { catalog_json }),
12555 )?;
12556 Ok(())
12557}
12558
12559fn filter_ignored_staging(
12560 staging: Vec<(u64, crate::txn::Staged)>,
12561 ignored_indices: &std::collections::BTreeSet<usize>,
12562) -> Vec<(u64, crate::txn::Staged)> {
12563 if ignored_indices.is_empty() {
12564 return staging;
12565 }
12566 staging
12567 .into_iter()
12568 .enumerate()
12569 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
12570 .collect()
12571}
12572
12573fn external_state_file(root: &Path, name: &str) -> PathBuf {
12574 external_state_dir(root, name).join("state.json")
12575}
12576
12577fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
12578 let path = external_state_file(root, name);
12579 match std::fs::read(path) {
12580 Ok(bytes) => Ok(bytes),
12581 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
12582 Err(e) => Err(e.into()),
12583 }
12584}
12585
12586fn current_external_state_bytes(
12587 root: &Path,
12588 external_states: &[(String, Vec<u8>)],
12589 name: &str,
12590) -> Result<Vec<u8>> {
12591 for (table, state) in external_states.iter().rev() {
12592 if table == name {
12593 return Ok(state.clone());
12594 }
12595 }
12596 read_external_state_file(root, name)
12597}
12598
12599fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
12600 let mut out = external_states;
12601 dedup_external_states_in_place(&mut out);
12602 out
12603}
12604
12605fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
12606 let mut seen = std::collections::HashSet::new();
12607 let mut out = Vec::with_capacity(external_states.len());
12608 for (name, state) in std::mem::take(external_states).into_iter().rev() {
12609 if seen.insert(name.clone()) {
12610 out.push((name, state));
12611 }
12612 }
12613 out.reverse();
12614 *external_states = out;
12615}
12616
12617fn prepare_external_state_file(
12618 root: &Path,
12619 name: &str,
12620 state: &[u8],
12621 txn_id: u64,
12622) -> Result<PathBuf> {
12623 crate::durable_file::create_directory(&root.join(VTAB_DIR))?;
12624 let dir = external_state_dir(root, name);
12625 crate::durable_file::create_directory(&dir)?;
12626 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
12627 {
12628 let mut file = std::fs::OpenOptions::new()
12629 .create_new(true)
12630 .write(true)
12631 .open(&pending)?;
12632 file.write_all(state)?;
12633 file.sync_all()?;
12634 }
12635 Ok(pending)
12636}
12637
12638fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
12639 let path = external_state_file(root, name);
12640 crate::durable_file::replace(pending, &path)?;
12641 Ok(())
12642}
12643
12644fn write_external_state_file(
12645 durable: &crate::durable_file::DurableRoot,
12646 name: &str,
12647 state: &[u8],
12648) -> Result<()> {
12649 let directory = Path::new(VTAB_DIR).join(name);
12650 durable.create_directory_all(&directory)?;
12651 durable.write_atomic(directory.join("state.json"), state)?;
12652 Ok(())
12653}
12654
12655fn validate_recovered_data_table(
12656 catalog: &Catalog,
12657 tables: &HashMap<u64, TableHandle>,
12658 table_id: u64,
12659 commit_epoch: u64,
12660 offset: u64,
12661) -> Result<bool> {
12662 let entry = catalog
12663 .tables
12664 .iter()
12665 .find(|entry| entry.table_id == table_id)
12666 .ok_or_else(|| MongrelError::CorruptWal {
12667 offset,
12668 reason: format!("committed record references unknown table {table_id}"),
12669 })?;
12670 if commit_epoch < entry.created_epoch {
12671 return Err(MongrelError::CorruptWal {
12672 offset,
12673 reason: format!(
12674 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
12675 entry.created_epoch
12676 ),
12677 });
12678 }
12679 match entry.state {
12680 TableState::Dropped { at_epoch } => {
12681 let abandoned_build_boundary =
12686 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
12687 if commit_epoch >= at_epoch && !abandoned_build_boundary {
12688 Err(MongrelError::CorruptWal {
12689 offset,
12690 reason: format!(
12691 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
12692 ),
12693 })
12694 } else {
12695 Ok(false)
12696 }
12697 }
12698 TableState::Live | TableState::Building { .. } => {
12699 if tables.contains_key(&table_id) {
12700 Ok(true)
12701 } else {
12702 Err(MongrelError::CorruptWal {
12703 offset,
12704 reason: format!("live table {table_id} has no mounted recovery handle"),
12705 })
12706 }
12707 }
12708 }
12709}
12710
12711type RecoveryTableStage = (
12712 Vec<crate::memtable::Row>,
12713 Vec<(crate::rowid::RowId, Epoch)>,
12714 Option<Epoch>,
12715 Epoch,
12716);
12717
12718#[derive(Clone)]
12719struct RecoveryValidationTable {
12720 schema: Schema,
12721 flushed_epoch: u64,
12722}
12723
12724fn validate_shared_wal_recovery_plan(
12725 durable_root: &crate::durable_file::DurableRoot,
12726 catalog: &Catalog,
12727 recovered_table_ids: &HashSet<u64>,
12728 reconciled_table_ids: &HashSet<u64>,
12729 meta_dek: Option<&[u8; META_DEK_LEN]>,
12730 kek: Option<Arc<crate::encryption::Kek>>,
12731 records: &[crate::wal::Record],
12732) -> Result<()> {
12733 use crate::wal::{DdlOp, Op};
12734
12735 let mut tables = HashMap::<u64, RecoveryValidationTable>::new();
12736 for entry in &catalog.tables {
12737 if !matches!(entry.state, TableState::Live) {
12738 continue;
12739 }
12740 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
12741 let manifest = match crate::manifest::read_durable(durable_root, &relative_dir, meta_dek) {
12742 Ok(manifest) => Some(manifest),
12743 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
12744 Err(error) => return Err(error),
12745 };
12746 let flushed_epoch = if let Some(manifest) = manifest {
12747 if manifest.table_id != entry.table_id {
12748 return Err(MongrelError::Conflict(format!(
12749 "catalog table {} storage identity mismatch",
12750 entry.table_id
12751 )));
12752 }
12753 if (manifest.schema_id != entry.schema.schema_id
12754 && !reconciled_table_ids.contains(&entry.table_id))
12755 || manifest.flushed_epoch > manifest.current_epoch
12756 || manifest.global_idx_epoch > manifest.current_epoch
12757 || manifest.next_row_id == u64::MAX
12758 || manifest.auto_inc_next < 0
12759 || manifest.auto_inc_next == i64::MAX
12760 || (entry.schema.auto_increment_column().is_none() && manifest.auto_inc_next != 0)
12761 {
12762 return Err(MongrelError::InvalidArgument(format!(
12763 "table {} manifest counters or schema identity are invalid",
12764 entry.table_id
12765 )));
12766 }
12767 #[cfg(feature = "encryption")]
12768 let idx_dek = kek.as_ref().map(|key| key.derive_idx_key());
12769 #[cfg(not(feature = "encryption"))]
12770 let idx_dek: Option<zeroize::Zeroizing<[u8; 32]>> = None;
12771 crate::global_idx::read_durable_for(
12772 durable_root,
12773 &relative_dir,
12774 entry.table_id,
12775 &entry.schema,
12776 idx_dek.as_deref(),
12777 )?;
12778 let mut run_ids = HashSet::new();
12779 let mut maximum_row_id = None::<u64>;
12780 for run in &manifest.runs {
12781 if run.run_id >= u64::MAX as u128
12782 || run.epoch_created > manifest.current_epoch
12783 || !run_ids.insert(run.run_id)
12784 {
12785 return Err(MongrelError::InvalidArgument(format!(
12786 "table {} manifest contains an invalid or duplicate run id",
12787 entry.table_id
12788 )));
12789 }
12790 let relative = relative_dir
12791 .join(crate::engine::RUNS_DIR)
12792 .join(format!("r-{}.sr", run.run_id as u64));
12793 let file = durable_root.open_regular(&relative)?;
12794 let mut reader = crate::sorted_run::RunReader::open_file(
12795 file,
12796 entry.schema.clone(),
12797 kek.clone(),
12798 )?;
12799 let header = reader.header();
12800 if header.run_id != run.run_id
12801 || header.level != run.level
12802 || header.row_count != run.row_count
12803 || !header.is_uniform_epoch() && header.epoch_created != run.epoch_created
12804 || header.is_uniform_epoch() && header.epoch_created != 0
12805 || header.schema_id > entry.schema.schema_id
12806 {
12807 return Err(MongrelError::InvalidArgument(format!(
12808 "table {} run {} differs from its manifest: header=(id {}, level {}, rows {}, epoch {}, schema {}), manifest=(id {}, level {}, rows {}, epoch {}, schema <= {})",
12809 entry.table_id,
12810 run.run_id,
12811 header.run_id,
12812 header.level,
12813 header.row_count,
12814 header.epoch_created,
12815 header.schema_id,
12816 run.run_id,
12817 run.level,
12818 run.row_count,
12819 run.epoch_created,
12820 entry.schema.schema_id,
12821 )));
12822 }
12823 if header.row_count != 0 {
12824 maximum_row_id = Some(
12825 maximum_row_id
12826 .map_or(header.max_row_id, |value| value.max(header.max_row_id)),
12827 );
12828 }
12829 reader.validate_all_pages()?;
12830 }
12831 if maximum_row_id.is_some_and(|maximum| manifest.next_row_id <= maximum) {
12832 return Err(MongrelError::InvalidArgument(format!(
12833 "table {} next_row_id does not advance beyond persisted rows",
12834 entry.table_id
12835 )));
12836 }
12837 for run in &manifest.retiring {
12838 if run.run_id >= u64::MAX as u128
12839 || run.retire_epoch > manifest.current_epoch
12840 || !run_ids.insert(run.run_id)
12841 {
12842 return Err(MongrelError::InvalidArgument(format!(
12843 "table {} manifest contains an invalid or aliased retired run",
12844 entry.table_id
12845 )));
12846 }
12847 }
12848 manifest.flushed_epoch
12849 } else {
12850 if !recovered_table_ids.contains(&entry.table_id) {
12851 return Err(MongrelError::NotFound(format!(
12852 "live table {} manifest is missing",
12853 entry.table_id
12854 )));
12855 }
12856 0
12857 };
12858 tables.insert(
12859 entry.table_id,
12860 RecoveryValidationTable {
12861 schema: entry.schema.clone(),
12862 flushed_epoch,
12863 },
12864 );
12865 }
12866
12867 let committed = records
12868 .iter()
12869 .filter_map(|record| match record.op {
12870 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
12871 _ => None,
12872 })
12873 .collect::<HashMap<_, _>>();
12874 let mut run_ids = HashSet::new();
12875 let mut recovered_row_ids = HashMap::<u64, HashSet<u64>>::new();
12876 for record in records {
12877 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
12878 continue;
12879 };
12880 match &record.op {
12881 Op::Put { table_id, rows } => {
12882 let table = validate_recovery_data_table_plan(
12883 catalog,
12884 &tables,
12885 *table_id,
12886 commit_epoch,
12887 record.seq.0,
12888 )?;
12889 let decoded: Vec<crate::memtable::Row> =
12890 bincode::deserialize(rows).map_err(|error| MongrelError::CorruptWal {
12891 offset: record.seq.0,
12892 reason: format!(
12893 "committed Put payload for transaction {} could not be decoded: {error}",
12894 record.txn_id
12895 ),
12896 })?;
12897 if let Some(table) = table {
12898 for row in &decoded {
12899 if !recovered_row_ids
12900 .entry(*table_id)
12901 .or_default()
12902 .insert(row.row_id.0)
12903 {
12904 return Err(MongrelError::CorruptWal {
12905 offset: record.seq.0,
12906 reason: format!(
12907 "committed WAL repeats recovered row id {} for table {table_id}",
12908 row.row_id.0
12909 ),
12910 });
12911 }
12912 validate_recovered_row(&table.schema, row)?;
12913 }
12914 }
12915 }
12916 Op::Delete { table_id, .. } | Op::TruncateTable { table_id } => {
12917 validate_recovery_data_table_plan(
12918 catalog,
12919 &tables,
12920 *table_id,
12921 commit_epoch,
12922 record.seq.0,
12923 )?;
12924 }
12925 Op::ExternalTableState { name, .. } => validate_recovered_external_name(name)?,
12926 Op::Ddl(DdlOp::ResetExternalTableState {
12927 name,
12928 generation_epoch,
12929 }) => {
12930 if *generation_epoch != commit_epoch {
12931 return Err(MongrelError::CorruptWal {
12932 offset: record.seq.0,
12933 reason: format!(
12934 "external state reset epoch {generation_epoch} does not match WAL commit epoch {commit_epoch}"
12935 ),
12936 });
12937 }
12938 validate_recovered_external_name(name)?;
12939 }
12940 Op::TxnCommit { added_runs, .. } => {
12941 for added in added_runs {
12942 let Some(table) = validate_recovery_data_table_plan(
12943 catalog,
12944 &tables,
12945 added.table_id,
12946 commit_epoch,
12947 record.seq.0,
12948 )?
12949 else {
12950 continue;
12951 };
12952 if added.run_id >= u64::MAX as u128
12953 || !run_ids.insert((added.table_id, added.run_id))
12954 {
12955 return Err(MongrelError::CorruptWal {
12956 offset: record.seq.0,
12957 reason: format!(
12958 "duplicate or invalid recovered run {} for table {}",
12959 added.run_id, added.table_id
12960 ),
12961 });
12962 }
12963 if commit_epoch <= table.flushed_epoch {
12964 continue;
12965 }
12966 validate_planned_spilled_run(
12967 durable_root,
12968 record.txn_id,
12969 commit_epoch,
12970 added,
12971 &table.schema,
12972 kek.clone(),
12973 )?;
12974 }
12975 }
12976 _ => {}
12977 }
12978 }
12979 Ok(())
12980}
12981
12982fn validate_recovery_data_table_plan<'a>(
12983 catalog: &Catalog,
12984 tables: &'a HashMap<u64, RecoveryValidationTable>,
12985 table_id: u64,
12986 commit_epoch: u64,
12987 offset: u64,
12988) -> Result<Option<&'a RecoveryValidationTable>> {
12989 let entry = catalog
12990 .tables
12991 .iter()
12992 .find(|entry| entry.table_id == table_id)
12993 .ok_or_else(|| MongrelError::CorruptWal {
12994 offset,
12995 reason: format!("committed record references unknown table {table_id}"),
12996 })?;
12997 if commit_epoch < entry.created_epoch {
12998 return Err(MongrelError::CorruptWal {
12999 offset,
13000 reason: format!(
13001 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
13002 entry.created_epoch
13003 ),
13004 });
13005 }
13006 match entry.state {
13007 TableState::Dropped { at_epoch } => {
13008 let abandoned =
13009 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
13010 if commit_epoch >= at_epoch && !abandoned {
13011 return Err(MongrelError::CorruptWal {
13012 offset,
13013 reason: format!(
13014 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
13015 ),
13016 });
13017 }
13018 Ok(None)
13019 }
13020 TableState::Live => {
13021 tables
13022 .get(&table_id)
13023 .map(Some)
13024 .ok_or_else(|| MongrelError::CorruptWal {
13025 offset,
13026 reason: format!("live table {table_id} has no recovery plan"),
13027 })
13028 }
13029 TableState::Building { .. } => Err(MongrelError::CorruptWal {
13030 offset,
13031 reason: format!("building table {table_id} was not normalized before recovery"),
13032 }),
13033 }
13034}
13035
13036fn validate_planned_spilled_run(
13037 root: &crate::durable_file::DurableRoot,
13038 txn_id: u64,
13039 commit_epoch: u64,
13040 added: &crate::wal::AddedRun,
13041 schema: &Schema,
13042 kek: Option<Arc<crate::encryption::Kek>>,
13043) -> Result<()> {
13044 let table = Path::new(TABLES_DIR).join(added.table_id.to_string());
13045 let destination = table
13046 .join(crate::engine::RUNS_DIR)
13047 .join(format!("r-{}.sr", added.run_id as u64));
13048 let pending = table
13049 .join("_txn")
13050 .join(txn_id.to_string())
13051 .join(format!("r-{}.sr", added.run_id as u64));
13052 let file = match root.open_regular(&destination) {
13053 Ok(file) => file,
13054 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
13055 root.open_regular(&pending).map_err(|pending_error| {
13056 if pending_error.kind() == std::io::ErrorKind::NotFound {
13057 MongrelError::CorruptWal {
13058 offset: commit_epoch,
13059 reason: format!(
13060 "committed spilled run {} for transaction {txn_id} is missing",
13061 added.run_id
13062 ),
13063 }
13064 } else {
13065 pending_error.into()
13066 }
13067 })?
13068 }
13069 Err(error) => return Err(error.into()),
13070 };
13071 let mut reader = crate::sorted_run::RunReader::open_file(file, schema.clone(), kek)?;
13072 let header = reader.header();
13073 if header.run_id != added.run_id
13074 || header.content_hash != added.content_hash
13075 || header.row_count != added.row_count
13076 || header.level != added.level
13077 || header.min_row_id != added.min_row_id
13078 || header.max_row_id != added.max_row_id
13079 || header.schema_id != schema.schema_id
13080 || !header.is_uniform_epoch()
13081 || header.epoch_created != 0
13082 {
13083 return Err(MongrelError::CorruptWal {
13084 offset: commit_epoch,
13085 reason: format!(
13086 "committed spilled run {} metadata differs from WAL",
13087 added.run_id
13088 ),
13089 });
13090 }
13091 reader.validate_all_pages()?;
13092 Ok(())
13093}
13094
13095fn recover_shared_wal(
13104 durable_root: &crate::durable_file::DurableRoot,
13105 tables: &HashMap<u64, TableHandle>,
13106 catalog: &Catalog,
13107 epoch: &EpochAuthority,
13108 records: &[crate::wal::Record],
13109) -> Result<()> {
13110 use crate::memtable::Row;
13111 use crate::wal::{DdlOp, Op};
13112
13113 let mut committed: HashMap<u64, u64> = HashMap::new();
13115 let mut spilled_to_link: Vec<(
13116 u64, u64, Vec<crate::wal::AddedRun>,
13119 )> = Vec::new();
13120 for r in records {
13121 if let Op::TxnCommit {
13122 epoch: ce,
13123 ref added_runs,
13124 } = r.op
13125 {
13126 committed.insert(r.txn_id, ce);
13127 if !added_runs.is_empty() {
13128 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
13129 }
13130 }
13131 }
13132 for record in records {
13133 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
13134 continue;
13135 };
13136 match &record.op {
13137 Op::Put { table_id, .. }
13138 | Op::Delete { table_id, .. }
13139 | Op::TruncateTable { table_id } => {
13140 validate_recovered_data_table(
13141 catalog,
13142 tables,
13143 *table_id,
13144 commit_epoch,
13145 record.seq.0,
13146 )?;
13147 }
13148 Op::TxnCommit { added_runs, .. } => {
13149 for run in added_runs {
13150 validate_recovered_data_table(
13151 catalog,
13152 tables,
13153 run.table_id,
13154 commit_epoch,
13155 record.seq.0,
13156 )?;
13157 }
13158 }
13159 _ => {}
13160 }
13161 }
13162 let truncated_transactions: HashSet<(u64, u64)> = records
13163 .iter()
13164 .filter_map(|record| {
13165 committed.get(&record.txn_id)?;
13166 match record.op {
13167 Op::TruncateTable { table_id } => Some((record.txn_id, table_id)),
13168 _ => None,
13169 }
13170 })
13171 .collect();
13172
13173 enum ExternalRecoveryAction {
13175 Write { name: String, state: Vec<u8> },
13176 Reset { name: String },
13177 }
13178 let mut stage: HashMap<u64, RecoveryTableStage> = HashMap::new();
13179 let mut external_actions = Vec::new();
13180 let mut max_epoch = epoch.visible().0;
13181 for r in records.iter().cloned() {
13182 let Some(&ce) = committed.get(&r.txn_id) else {
13183 continue; };
13185 let commit_epoch = Epoch(ce);
13186 max_epoch = max_epoch.max(ce);
13187 match r.op {
13188 Op::Put { table_id, rows } => {
13189 let skip = tables
13191 .get(&table_id)
13192 .map(|h| h.lock().flushed_epoch() >= ce)
13193 .unwrap_or(true);
13194 if skip {
13195 continue;
13196 }
13197 let rows: Vec<Row> = bincode::deserialize(&rows).map_err(|error| {
13198 MongrelError::CorruptWal {
13199 offset: r.seq.0,
13200 reason: format!(
13201 "committed Put payload for transaction {} could not be decoded: {error}",
13202 r.txn_id
13203 ),
13204 }
13205 })?;
13206 let rows: Vec<Row> = rows
13209 .into_iter()
13210 .map(|mut row| {
13211 row.committed_epoch = commit_epoch;
13212 row
13213 })
13214 .collect();
13215 let entry = stage
13216 .entry(table_id)
13217 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
13218 entry.0.extend(rows);
13219 entry.3 = commit_epoch;
13220 }
13221 Op::Delete { table_id, row_ids } => {
13222 let skip = tables
13223 .get(&table_id)
13224 .map(|h| h.lock().flushed_epoch() >= ce)
13225 .unwrap_or(true);
13226 if skip {
13227 continue;
13228 }
13229 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
13230 let entry = stage
13231 .entry(table_id)
13232 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
13233 entry.1.extend(dels);
13234 entry.3 = commit_epoch;
13235 }
13236 Op::TruncateTable { table_id } => {
13237 let skip = tables
13238 .get(&table_id)
13239 .map(|h| h.lock().flushed_epoch() >= ce)
13240 .unwrap_or(true);
13241 if skip {
13242 continue;
13243 }
13244 stage.insert(
13245 table_id,
13246 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
13247 );
13248 }
13249 Op::ExternalTableState { name, state } => {
13250 let current_generation = catalog
13251 .external_tables
13252 .iter()
13253 .find(|entry| entry.name == name)
13254 .map(|entry| entry.created_epoch);
13255 if current_generation.is_some_and(|created_epoch| ce >= created_epoch) {
13256 validate_recovered_external_name(&name)?;
13257 external_actions.push(ExternalRecoveryAction::Write { name, state });
13258 }
13259 }
13260 Op::Ddl(DdlOp::ResetExternalTableState {
13261 name,
13262 generation_epoch,
13263 }) => {
13264 if generation_epoch != ce {
13265 return Err(MongrelError::CorruptWal {
13266 offset: r.seq.0,
13267 reason: format!(
13268 "external state reset epoch {generation_epoch} does not match WAL commit epoch {ce}"
13269 ),
13270 });
13271 }
13272 validate_recovered_external_name(&name)?;
13273 external_actions.push(ExternalRecoveryAction::Reset { name });
13274 }
13275 Op::Flush { .. }
13276 | Op::TxnCommit { .. }
13277 | Op::TxnAbort
13278 | Op::Ddl(_)
13279 | Op::BeforeImage { .. }
13280 | Op::CommitTimestamp { .. }
13281 | Op::SpilledRows { .. } => {}
13282 }
13283 }
13284 for (_, commit_epoch, added_runs) in &mut spilled_to_link {
13285 added_runs.retain(|added| {
13286 tables
13287 .get(&added.table_id)
13288 .is_some_and(|table| table.lock().flushed_epoch() < *commit_epoch)
13289 });
13290 }
13291 spilled_to_link.retain(|(_, _, added_runs)| !added_runs.is_empty());
13292 validate_recovery_table_stages(tables, &stage)?;
13293 validate_recovery_spilled_runs(durable_root, tables, &spilled_to_link)?;
13294
13295 for action in external_actions {
13299 match action {
13300 ExternalRecoveryAction::Write { name, state } => {
13301 write_external_state_file(durable_root, &name, &state)?;
13302 }
13303 ExternalRecoveryAction::Reset { name } => {
13304 durable_root.create_directory_all(VTAB_DIR)?;
13305 durable_root.remove_directory_all(Path::new(VTAB_DIR).join(name))?;
13306 }
13307 }
13308 }
13309 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
13310 let Some(handle) = tables.get(&table_id) else {
13311 continue;
13312 };
13313 let mut t = handle.lock();
13314 if let Some(epoch) = truncate_epoch {
13315 t.apply_truncate(epoch);
13316 }
13317 t.recover_apply(rows, deletes)?;
13318 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
13322 t.live_count = rows.len() as u64;
13323 t.persist_manifest(table_epoch.max(epoch.visible()))?;
13327 }
13328
13329 for (txn_id, ce, added_runs) in &spilled_to_link {
13333 for ar in added_runs {
13334 let Some(handle) = tables.get(&ar.table_id) else {
13335 continue;
13336 };
13337 let mut t = handle.lock();
13338 let table_dir = Path::new(TABLES_DIR).join(ar.table_id.to_string());
13339 let destination = table_dir
13340 .join(crate::engine::RUNS_DIR)
13341 .join(format!("r-{}.sr", ar.run_id));
13342 match durable_root.open_regular(&destination) {
13343 Ok(_) => {}
13344 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
13345 let pending = table_dir
13346 .join("_txn")
13347 .join(txn_id.to_string())
13348 .join(format!("r-{}.sr", ar.run_id));
13349 durable_root.rename_file_new(&pending, &destination)?;
13350 }
13351 Err(error) => return Err(error.into()),
13352 }
13353 let linked = t.recover_spilled_run(crate::manifest::RunRef {
13359 run_id: ar.run_id,
13360 level: ar.level,
13361 epoch_created: *ce,
13362 row_count: ar.row_count,
13363 });
13364 let replaced = truncated_transactions.contains(&(*txn_id, ar.table_id));
13365 if replaced {
13366 t.set_flushed_epoch(Epoch(*ce));
13367 }
13368 if linked || replaced {
13369 t.persist_manifest(Epoch(*ce).max(epoch.visible()))?;
13370 }
13371 }
13372 }
13373
13374 epoch.advance_recovered(Epoch(max_epoch));
13375 Ok(())
13376}
13377
13378fn reconcile_recovered_table_metadata(
13379 tables: &HashMap<u64, TableHandle>,
13380 epoch: Epoch,
13381) -> Result<()> {
13382 let mut table_ids = tables.keys().copied().collect::<Vec<_>>();
13383 table_ids.sort_unstable();
13384 let mut plans = Vec::with_capacity(table_ids.len());
13385 for table_id in &table_ids {
13386 let handle = tables.get(table_id).ok_or_else(|| {
13387 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
13388 })?;
13389 plans.push((*table_id, handle.lock().plan_recovered_metadata()?));
13390 }
13391 for (table_id, plan) in plans {
13394 let handle = tables.get(&table_id).ok_or_else(|| {
13395 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
13396 })?;
13397 handle.lock().apply_recovered_metadata(plan, epoch)?;
13398 }
13399 Ok(())
13400}
13401
13402fn validate_recovered_external_name(name: &str) -> Result<()> {
13403 if name.is_empty()
13404 || !name.chars().all(|character| {
13405 character.is_ascii_alphanumeric() || character == '_' || character == '-'
13406 })
13407 {
13408 return Err(MongrelError::CorruptWal {
13409 offset: 0,
13410 reason: format!("unsafe recovered external-table name {name:?}"),
13411 });
13412 }
13413 Ok(())
13414}
13415
13416fn validate_recovery_table_stages(
13417 tables: &HashMap<u64, TableHandle>,
13418 stages: &HashMap<u64, RecoveryTableStage>,
13419) -> Result<()> {
13420 for (table_id, (rows, _, _, _)) in stages {
13421 let handle = tables
13422 .get(table_id)
13423 .ok_or_else(|| MongrelError::CorruptWal {
13424 offset: *table_id,
13425 reason: format!("recovery stage references unmounted table {table_id}"),
13426 })?;
13427 let table = handle.lock();
13428 table.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
13431 for row in rows {
13432 validate_recovered_row(table.schema(), row)?;
13433 }
13434 }
13435 Ok(())
13436}
13437
13438fn validate_recovered_row(schema: &Schema, row: &crate::memtable::Row) -> Result<()> {
13439 if row.deleted || row.row_id.0 == u64::MAX {
13440 return Err(MongrelError::CorruptWal {
13441 offset: row.row_id.0,
13442 reason: "committed Put payload contains a tombstone or exhausted row id".into(),
13443 });
13444 }
13445 let cells = row
13446 .columns
13447 .iter()
13448 .map(|(column, value)| (*column, value.clone()))
13449 .collect::<Vec<_>>();
13450 schema
13451 .validate_persisted_values(&cells)
13452 .map_err(|error| MongrelError::CorruptWal {
13453 offset: row.row_id.0,
13454 reason: format!("recovered row violates table schema: {error}"),
13455 })?;
13456 if schema.auto_increment_column().is_some_and(|column| {
13457 matches!(row.columns.get(&column.id), Some(Value::Int64(value)) if *value == i64::MAX)
13458 }) {
13459 return Err(MongrelError::CorruptWal {
13460 offset: row.row_id.0,
13461 reason: "recovered AUTO_INCREMENT value exhausts i64".into(),
13462 });
13463 }
13464 Ok(())
13465}
13466
13467fn validate_recovery_spilled_runs(
13468 root: &crate::durable_file::DurableRoot,
13469 tables: &HashMap<u64, TableHandle>,
13470 spilled: &[(u64, u64, Vec<crate::wal::AddedRun>)],
13471) -> Result<()> {
13472 let mut identities = HashSet::new();
13473 for (txn_id, commit_epoch, added_runs) in spilled {
13474 for added in added_runs {
13475 if added.run_id >= u64::MAX as u128 {
13476 return Err(MongrelError::CorruptWal {
13477 offset: *commit_epoch,
13478 reason: format!(
13479 "recovered run id {} exceeds the on-disk namespace",
13480 added.run_id
13481 ),
13482 });
13483 }
13484 let Some(handle) = tables.get(&added.table_id) else {
13485 continue;
13486 };
13487 if !identities.insert((added.table_id, added.run_id)) {
13488 return Err(MongrelError::CorruptWal {
13489 offset: *commit_epoch,
13490 reason: format!(
13491 "duplicate recovered run {} for table {}",
13492 added.run_id, added.table_id
13493 ),
13494 });
13495 }
13496 let table = handle.lock();
13497 validate_planned_spilled_run(
13498 root,
13499 *txn_id,
13500 *commit_epoch,
13501 added,
13502 table.schema(),
13503 table.kek(),
13504 )?;
13505 }
13506 }
13507 Ok(())
13508}
13509
13510fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
13511 match condition {
13512 ProcedureCondition::Pk { .. } => {
13513 if schema.primary_key().is_none() {
13514 return Err(MongrelError::InvalidArgument(
13515 "procedure condition Pk references a table without a primary key".into(),
13516 ));
13517 }
13518 }
13519 ProcedureCondition::BitmapEq { column_id, .. }
13520 | ProcedureCondition::BitmapIn { column_id, .. }
13521 | ProcedureCondition::Range { column_id, .. }
13522 | ProcedureCondition::RangeF64 { column_id, .. }
13523 | ProcedureCondition::IsNull { column_id }
13524 | ProcedureCondition::IsNotNull { column_id }
13525 | ProcedureCondition::FmContains { column_id, .. } => {
13526 validate_column_id(*column_id, schema)?;
13527 }
13528 }
13529 Ok(())
13530}
13531
13532fn bind_procedure_args(
13533 procedure: &StoredProcedure,
13534 mut args: HashMap<String, crate::Value>,
13535) -> Result<HashMap<String, crate::Value>> {
13536 let mut out = HashMap::new();
13537 for param in &procedure.params {
13538 let value = match args.remove(¶m.name) {
13539 Some(value) => value,
13540 None => param.default.clone().ok_or_else(|| {
13541 MongrelError::InvalidArgument(format!(
13542 "missing required procedure parameter {:?}",
13543 param.name
13544 ))
13545 })?,
13546 };
13547 if !param.nullable && matches!(value, crate::Value::Null) {
13548 return Err(MongrelError::InvalidArgument(format!(
13549 "procedure parameter {:?} must not be NULL",
13550 param.name
13551 )));
13552 }
13553 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty.clone()) {
13554 return Err(MongrelError::InvalidArgument(format!(
13555 "procedure parameter {:?} has wrong type",
13556 param.name
13557 )));
13558 }
13559 out.insert(param.name.clone(), value);
13560 }
13561 if let Some(extra) = args.keys().next() {
13562 return Err(MongrelError::InvalidArgument(format!(
13563 "unknown procedure parameter {extra:?}"
13564 )));
13565 }
13566 Ok(out)
13567}
13568
13569fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
13570 matches!(
13571 (value, ty),
13572 (crate::Value::Bool(_), crate::TypeId::Bool)
13573 | (crate::Value::Int64(_), crate::TypeId::Int8)
13574 | (crate::Value::Int64(_), crate::TypeId::Int16)
13575 | (crate::Value::Int64(_), crate::TypeId::Int32)
13576 | (crate::Value::Int64(_), crate::TypeId::Int64)
13577 | (crate::Value::Int64(_), crate::TypeId::UInt8)
13578 | (crate::Value::Int64(_), crate::TypeId::UInt16)
13579 | (crate::Value::Int64(_), crate::TypeId::UInt32)
13580 | (crate::Value::Int64(_), crate::TypeId::UInt64)
13581 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
13582 | (crate::Value::Int64(_), crate::TypeId::Date32)
13583 | (crate::Value::Float64(_), crate::TypeId::Float32)
13584 | (crate::Value::Float64(_), crate::TypeId::Float64)
13585 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
13586 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
13587 )
13588}
13589
13590fn eval_cells(
13591 cells: &[crate::procedure::ProcedureCell],
13592 args: &HashMap<String, crate::Value>,
13593 outputs: &HashMap<String, ProcedureCallOutput>,
13594) -> Result<Vec<(u16, crate::Value)>> {
13595 cells
13596 .iter()
13597 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
13598 .collect()
13599}
13600
13601fn eval_condition(
13602 condition: &ProcedureCondition,
13603 args: &HashMap<String, crate::Value>,
13604 outputs: &HashMap<String, ProcedureCallOutput>,
13605) -> Result<crate::Condition> {
13606 Ok(match condition {
13607 ProcedureCondition::Pk { value } => {
13608 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
13609 }
13610 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
13611 column_id: *column_id,
13612 value: eval_value(value, args, outputs)?.encode_key(),
13613 },
13614 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
13615 column_id: *column_id,
13616 values: values
13617 .iter()
13618 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
13619 .collect::<Result<Vec<_>>>()?,
13620 },
13621 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
13622 column_id: *column_id,
13623 lo: expect_i64(eval_value(lo, args, outputs)?)?,
13624 hi: expect_i64(eval_value(hi, args, outputs)?)?,
13625 },
13626 ProcedureCondition::RangeF64 {
13627 column_id,
13628 lo,
13629 lo_inclusive,
13630 hi,
13631 hi_inclusive,
13632 } => crate::Condition::RangeF64 {
13633 column_id: *column_id,
13634 lo: expect_f64(eval_value(lo, args, outputs)?)?,
13635 lo_inclusive: *lo_inclusive,
13636 hi: expect_f64(eval_value(hi, args, outputs)?)?,
13637 hi_inclusive: *hi_inclusive,
13638 },
13639 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
13640 column_id: *column_id,
13641 },
13642 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
13643 column_id: *column_id,
13644 },
13645 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
13646 column_id: *column_id,
13647 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
13648 },
13649 })
13650}
13651
13652fn eval_value(
13653 value: &ProcedureValue,
13654 args: &HashMap<String, crate::Value>,
13655 outputs: &HashMap<String, ProcedureCallOutput>,
13656) -> Result<crate::Value> {
13657 match value {
13658 ProcedureValue::Literal(value) => Ok(value.clone()),
13659 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
13660 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
13661 }),
13662 ProcedureValue::StepScalar(id) => match outputs.get(id) {
13663 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
13664 _ => Err(MongrelError::InvalidArgument(format!(
13665 "procedure step {id:?} did not return a scalar"
13666 ))),
13667 },
13668 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
13669 Err(MongrelError::InvalidArgument(
13670 "row-valued procedure reference cannot be used as a scalar".into(),
13671 ))
13672 }
13673 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
13674 "structured procedure value cannot be used as a scalar cell".into(),
13675 )),
13676 }
13677}
13678
13679fn eval_return_output(
13680 value: &ProcedureValue,
13681 args: &HashMap<String, crate::Value>,
13682 outputs: &HashMap<String, ProcedureCallOutput>,
13683) -> Result<ProcedureCallOutput> {
13684 match value {
13685 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
13686 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
13687 args.get(name).cloned().ok_or_else(|| {
13688 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
13689 })?,
13690 )),
13691 ProcedureValue::StepRows(id)
13692 | ProcedureValue::StepRow(id)
13693 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
13694 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
13695 }),
13696 ProcedureValue::Object(fields) => {
13697 let mut out = Vec::with_capacity(fields.len());
13698 for (name, value) in fields {
13699 out.push((name.clone(), eval_return_output(value, args, outputs)?));
13700 }
13701 Ok(ProcedureCallOutput::Object(out))
13702 }
13703 ProcedureValue::Array(values) => {
13704 let mut out = Vec::with_capacity(values.len());
13705 for value in values {
13706 out.push(eval_return_output(value, args, outputs)?);
13707 }
13708 Ok(ProcedureCallOutput::Array(out))
13709 }
13710 }
13711}
13712
13713fn expect_i64(value: crate::Value) -> Result<i64> {
13714 match value {
13715 crate::Value::Int64(value) => Ok(value),
13716 _ => Err(MongrelError::InvalidArgument(
13717 "procedure value must be Int64".into(),
13718 )),
13719 }
13720}
13721
13722fn expect_f64(value: crate::Value) -> Result<f64> {
13723 match value {
13724 crate::Value::Float64(value) => Ok(value),
13725 _ => Err(MongrelError::InvalidArgument(
13726 "procedure value must be Float64".into(),
13727 )),
13728 }
13729}
13730
13731fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
13732 match value {
13733 crate::Value::Bytes(value) => Ok(value),
13734 _ => Err(MongrelError::InvalidArgument(
13735 "procedure value must be Bytes".into(),
13736 )),
13737 }
13738}
13739
13740fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
13741 if schema.columns.iter().any(|c| c.id == column_id) {
13742 Ok(())
13743 } else {
13744 Err(MongrelError::InvalidArgument(format!(
13745 "unknown column id {column_id}"
13746 )))
13747 }
13748}
13749
13750fn trigger_matches_event(
13751 trigger: &StoredTrigger,
13752 event: &WriteEvent,
13753 cat: &Catalog,
13754) -> Result<bool> {
13755 if trigger.event != event.kind {
13756 return Ok(false);
13757 }
13758 let TriggerTarget::Table(target) = &trigger.target else {
13759 return Ok(false);
13760 };
13761 if target != &event.table {
13762 return Ok(false);
13763 }
13764 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
13765 let schema = &cat
13766 .live(target)
13767 .ok_or_else(|| {
13768 MongrelError::InvalidArgument(format!(
13769 "trigger {:?} references unknown table {target:?}",
13770 trigger.name
13771 ))
13772 })?
13773 .schema;
13774 let mut watched = Vec::with_capacity(trigger.update_of.len());
13775 for name in &trigger.update_of {
13776 let col = schema.column(name).ok_or_else(|| {
13777 MongrelError::InvalidArgument(format!(
13778 "trigger {:?} references unknown UPDATE OF column {name:?}",
13779 trigger.name
13780 ))
13781 })?;
13782 watched.push(col.id);
13783 }
13784 if !event
13785 .changed_columns
13786 .iter()
13787 .any(|column_id| watched.contains(column_id))
13788 {
13789 return Ok(false);
13790 }
13791 }
13792 Ok(true)
13793}
13794
13795fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
13796 let mut ids = std::collections::BTreeSet::new();
13797 if let Some(old) = old {
13798 ids.extend(old.columns.keys().copied());
13799 }
13800 if let Some(new) = new {
13801 ids.extend(new.columns.keys().copied());
13802 }
13803 ids.into_iter()
13804 .filter(|id| {
13805 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
13806 })
13807 .collect()
13808}
13809
13810fn eval_trigger_cells(
13811 cells: &[crate::trigger::TriggerCell],
13812 event: &WriteEvent,
13813 selected: Option<&TriggerRowImage>,
13814) -> Result<Vec<(u16, Value)>> {
13815 cells
13816 .iter()
13817 .map(|cell| {
13818 Ok((
13819 cell.column_id,
13820 eval_trigger_value(&cell.value, event, selected)?,
13821 ))
13822 })
13823 .collect()
13824}
13825
13826fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
13827 match expr {
13828 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
13829 Value::Bool(value) => Ok(value),
13830 Value::Null => Ok(false),
13831 other => Err(MongrelError::InvalidArgument(format!(
13832 "trigger WHEN value must be boolean, got {other:?}"
13833 ))),
13834 },
13835 TriggerExpr::Eq { left, right } => Ok(values_equal(
13836 &eval_trigger_value(left, event, None)?,
13837 &eval_trigger_value(right, event, None)?,
13838 )),
13839 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
13840 &eval_trigger_value(left, event, None)?,
13841 &eval_trigger_value(right, event, None)?,
13842 )),
13843 TriggerExpr::Lt { left, right } => match value_order(
13844 &eval_trigger_value(left, event, None)?,
13845 &eval_trigger_value(right, event, None)?,
13846 ) {
13847 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
13848 None => Ok(false),
13849 },
13850 TriggerExpr::Lte { left, right } => match value_order(
13851 &eval_trigger_value(left, event, None)?,
13852 &eval_trigger_value(right, event, None)?,
13853 ) {
13854 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
13855 None => Ok(false),
13856 },
13857 TriggerExpr::Gt { left, right } => match value_order(
13858 &eval_trigger_value(left, event, None)?,
13859 &eval_trigger_value(right, event, None)?,
13860 ) {
13861 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
13862 None => Ok(false),
13863 },
13864 TriggerExpr::Gte { left, right } => match value_order(
13865 &eval_trigger_value(left, event, None)?,
13866 &eval_trigger_value(right, event, None)?,
13867 ) {
13868 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
13869 None => Ok(false),
13870 },
13871 TriggerExpr::IsNull(value) => Ok(matches!(
13872 eval_trigger_value(value, event, None)?,
13873 Value::Null
13874 )),
13875 TriggerExpr::IsNotNull(value) => Ok(!matches!(
13876 eval_trigger_value(value, event, None)?,
13877 Value::Null
13878 )),
13879 TriggerExpr::And { left, right } => {
13880 if !eval_trigger_expr(left, event)? {
13881 Ok(false)
13882 } else {
13883 Ok(eval_trigger_expr(right, event)?)
13884 }
13885 }
13886 TriggerExpr::Or { left, right } => {
13887 if eval_trigger_expr(left, event)? {
13888 Ok(true)
13889 } else {
13890 Ok(eval_trigger_expr(right, event)?)
13891 }
13892 }
13893 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
13894 }
13895}
13896
13897fn eval_trigger_condition(
13898 condition: &TriggerCondition,
13899 event: &WriteEvent,
13900 selected: &TriggerRowImage,
13901 schema: &Schema,
13902) -> Result<bool> {
13903 match condition {
13904 TriggerCondition::Pk { value } => {
13905 let pk = schema.primary_key().ok_or_else(|| {
13906 MongrelError::InvalidArgument(
13907 "trigger condition Pk references a table without a primary key".into(),
13908 )
13909 })?;
13910 let lhs = eval_trigger_value(value, event, Some(selected))?;
13911 Ok(values_equal(
13912 &lhs,
13913 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
13914 ))
13915 }
13916 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
13917 selected.columns.get(column_id).unwrap_or(&Value::Null),
13918 &eval_trigger_value(value, event, Some(selected))?,
13919 )),
13920 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
13921 selected.columns.get(column_id).unwrap_or(&Value::Null),
13922 &eval_trigger_value(value, event, Some(selected))?,
13923 )),
13924 TriggerCondition::Lt { column_id, value } => match value_order(
13925 selected.columns.get(column_id).unwrap_or(&Value::Null),
13926 &eval_trigger_value(value, event, Some(selected))?,
13927 ) {
13928 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
13929 None => Ok(false),
13930 },
13931 TriggerCondition::Lte { column_id, value } => match value_order(
13932 selected.columns.get(column_id).unwrap_or(&Value::Null),
13933 &eval_trigger_value(value, event, Some(selected))?,
13934 ) {
13935 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
13936 None => Ok(false),
13937 },
13938 TriggerCondition::Gt { column_id, value } => match value_order(
13939 selected.columns.get(column_id).unwrap_or(&Value::Null),
13940 &eval_trigger_value(value, event, Some(selected))?,
13941 ) {
13942 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
13943 None => Ok(false),
13944 },
13945 TriggerCondition::Gte { column_id, value } => match value_order(
13946 selected.columns.get(column_id).unwrap_or(&Value::Null),
13947 &eval_trigger_value(value, event, Some(selected))?,
13948 ) {
13949 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
13950 None => Ok(false),
13951 },
13952 TriggerCondition::IsNull { column_id } => Ok(matches!(
13953 selected.columns.get(column_id),
13954 None | Some(Value::Null)
13955 )),
13956 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
13957 selected.columns.get(column_id),
13958 None | Some(Value::Null)
13959 )),
13960 TriggerCondition::And { left, right } => {
13961 if !eval_trigger_condition(left, event, selected, schema)? {
13962 Ok(false)
13963 } else {
13964 Ok(eval_trigger_condition(right, event, selected, schema)?)
13965 }
13966 }
13967 TriggerCondition::Or { left, right } => {
13968 if eval_trigger_condition(left, event, selected, schema)? {
13969 Ok(true)
13970 } else {
13971 Ok(eval_trigger_condition(right, event, selected, schema)?)
13972 }
13973 }
13974 TriggerCondition::Not(condition) => {
13975 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
13976 }
13977 }
13978}
13979
13980fn eval_trigger_value(
13981 value: &TriggerValue,
13982 event: &WriteEvent,
13983 selected: Option<&TriggerRowImage>,
13984) -> Result<Value> {
13985 match value {
13986 TriggerValue::Literal(value) => Ok(value.clone()),
13987 TriggerValue::NewColumn(column_id) => event
13988 .new
13989 .as_ref()
13990 .and_then(|row| row.columns.get(column_id))
13991 .cloned()
13992 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
13993 TriggerValue::OldColumn(column_id) => event
13994 .old
13995 .as_ref()
13996 .and_then(|row| row.columns.get(column_id))
13997 .cloned()
13998 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
13999 TriggerValue::SelectedColumn(column_id) => selected
14000 .and_then(|row| row.columns.get(column_id))
14001 .cloned()
14002 .ok_or_else(|| {
14003 MongrelError::InvalidArgument("SELECTED column is not available".into())
14004 }),
14005 }
14006}
14007
14008fn values_equal(left: &Value, right: &Value) -> bool {
14009 match (left, right) {
14010 (Value::Null, Value::Null) => true,
14011 (Value::Bool(a), Value::Bool(b)) => a == b,
14012 (Value::Int64(a), Value::Int64(b)) => a == b,
14013 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
14014 (Value::Bytes(a), Value::Bytes(b)) => a == b,
14015 (Value::Embedding(a), Value::Embedding(b)) => {
14016 a.len() == b.len()
14017 && a.iter()
14018 .zip(b.iter())
14019 .all(|(a, b)| a.to_bits() == b.to_bits())
14020 }
14021 _ => false,
14022 }
14023}
14024
14025fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
14026 match (left, right) {
14027 (Value::Null, _) | (_, Value::Null) => None,
14028 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
14029 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
14030 (Value::Int64(a), Value::Float64(b)) => {
14033 let af = *a as f64;
14034 Some(af.total_cmp(b))
14035 }
14036 (Value::Float64(a), Value::Int64(b)) => {
14039 let bf = *b as f64;
14040 Some(a.total_cmp(&bf))
14041 }
14042 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
14043 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
14044 (Value::Embedding(_), Value::Embedding(_)) => None,
14045 _ => None,
14046 }
14047}
14048
14049fn trigger_message(value: Value) -> String {
14050 match value {
14051 Value::Null => "NULL".into(),
14052 Value::Bool(value) => value.to_string(),
14053 Value::Int64(value) => value.to_string(),
14054 Value::Float64(value) => value.to_string(),
14055 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
14056 Value::Embedding(value) => format!("{value:?}"),
14057 Value::Decimal(value) => value.to_string(),
14058 Value::Interval {
14059 months,
14060 days,
14061 nanos,
14062 } => format!("{months}m {days}d {nanos}ns"),
14063 Value::Uuid(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
14064 Value::Json(b) => String::from_utf8_lossy(&b).into_owned(),
14065 }
14066}
14067
14068fn validate_trigger_step<'a>(
14069 step: &TriggerStep,
14070 cat: &'a Catalog,
14071 target_schema: &Schema,
14072 event: TriggerEvent,
14073 select_schemas: &mut HashMap<String, &'a Schema>,
14074) -> Result<()> {
14075 match step {
14076 TriggerStep::SetNew { cells } => {
14077 if event == TriggerEvent::Delete {
14078 return Err(MongrelError::InvalidArgument(
14079 "SetNew trigger step is not valid for DELETE triggers".into(),
14080 ));
14081 }
14082 for cell in cells {
14083 validate_column_id(cell.column_id, target_schema)?;
14084 validate_trigger_value(&cell.value, target_schema, event)?;
14085 }
14086 }
14087 TriggerStep::Insert { table, cells } => {
14088 let schema = trigger_write_schema(cat, table, "insert")?;
14089 for cell in cells {
14090 validate_column_id(cell.column_id, schema)?;
14091 validate_trigger_value(&cell.value, target_schema, event)?;
14092 }
14093 }
14094 TriggerStep::UpdateByPk { table, pk, cells } => {
14095 let schema = trigger_write_schema(cat, table, "update")?;
14096 if schema.primary_key().is_none() {
14097 return Err(MongrelError::InvalidArgument(format!(
14098 "trigger update_by_pk references table {table:?} without a primary key"
14099 )));
14100 }
14101 validate_trigger_value(pk, target_schema, event)?;
14102 for cell in cells {
14103 validate_column_id(cell.column_id, schema)?;
14104 validate_trigger_value(&cell.value, target_schema, event)?;
14105 }
14106 }
14107 TriggerStep::DeleteByPk { table, pk } => {
14108 let schema = trigger_write_schema(cat, table, "delete")?;
14109 if schema.primary_key().is_none() {
14110 return Err(MongrelError::InvalidArgument(format!(
14111 "trigger delete_by_pk references table {table:?} without a primary key"
14112 )));
14113 }
14114 validate_trigger_value(pk, target_schema, event)?;
14115 }
14116 TriggerStep::Select {
14117 id,
14118 table,
14119 conditions,
14120 } => {
14121 let schema = trigger_read_schema(cat, table)?;
14122 for condition in conditions {
14123 validate_trigger_condition(condition, schema, target_schema, event)?;
14124 }
14125 if select_schemas.contains_key(id) {
14126 return Err(MongrelError::InvalidArgument(format!(
14127 "duplicate select id {id:?} in trigger program"
14128 )));
14129 }
14130 select_schemas.insert(id.clone(), schema);
14131 }
14132 TriggerStep::Foreach { id, steps } => {
14133 if !select_schemas.contains_key(id) {
14134 return Err(MongrelError::InvalidArgument(format!(
14135 "foreach references unknown select id {id:?}"
14136 )));
14137 }
14138 let mut inner_select_schemas = select_schemas.clone();
14139 for step in steps {
14140 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
14141 }
14142 }
14143 TriggerStep::DeleteWhere { table, conditions } => {
14144 let schema = trigger_write_schema(cat, table, "delete")?;
14145 for condition in conditions {
14146 validate_trigger_condition(condition, schema, target_schema, event)?;
14147 }
14148 }
14149 TriggerStep::UpdateWhere {
14150 table,
14151 conditions,
14152 cells,
14153 } => {
14154 let schema = trigger_write_schema(cat, table, "update")?;
14155 for condition in conditions {
14156 validate_trigger_condition(condition, schema, target_schema, event)?;
14157 }
14158 for cell in cells {
14159 validate_column_id(cell.column_id, schema)?;
14160 validate_trigger_value(&cell.value, target_schema, event)?;
14161 }
14162 }
14163 TriggerStep::Raise { message, .. } => {
14164 validate_trigger_value(message, target_schema, event)?
14165 }
14166 }
14167 Ok(())
14168}
14169
14170fn trigger_validation_error(error: MongrelError) -> MongrelError {
14171 match error {
14172 MongrelError::TriggerValidation(_) => error,
14173 MongrelError::InvalidArgument(message)
14174 | MongrelError::Conflict(message)
14175 | MongrelError::NotFound(message) => MongrelError::TriggerValidation(message),
14176 error => error,
14177 }
14178}
14179
14180fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
14181 if let Some(entry) = cat.live(table) {
14182 return Ok(&entry.schema);
14183 }
14184 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
14185 let allowed = match op {
14186 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
14187 "update" | "delete" => entry.capabilities.writable,
14188 _ => false,
14189 };
14190 if !allowed {
14191 return Err(MongrelError::InvalidArgument(format!(
14192 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
14193 entry.module
14194 )));
14195 }
14196 if !entry.capabilities.transaction_safe {
14197 return Err(MongrelError::InvalidArgument(format!(
14198 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
14199 entry.module
14200 )));
14201 }
14202 return Ok(&entry.declared_schema);
14203 }
14204 Err(MongrelError::InvalidArgument(format!(
14205 "trigger references unknown table {table:?}"
14206 )))
14207}
14208
14209fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
14210 if let Some(entry) = cat.live(table) {
14211 return Ok(&entry.schema);
14212 }
14213 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
14214 if entry.capabilities.trigger_safe {
14215 return Ok(&entry.declared_schema);
14216 }
14217 return Err(MongrelError::InvalidArgument(format!(
14218 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
14219 entry.module
14220 )));
14221 }
14222 Err(MongrelError::InvalidArgument(format!(
14223 "trigger references unknown table {table:?}"
14224 )))
14225}
14226
14227fn validate_trigger_condition(
14228 condition: &TriggerCondition,
14229 schema: &Schema,
14230 target_schema: &Schema,
14231 event: TriggerEvent,
14232) -> Result<()> {
14233 match condition {
14234 TriggerCondition::Pk { value } => {
14235 if schema.primary_key().is_none() {
14236 return Err(MongrelError::InvalidArgument(
14237 "trigger condition Pk references a table without a primary key".into(),
14238 ));
14239 }
14240 validate_trigger_value(value, target_schema, event)
14241 }
14242 TriggerCondition::Eq { column_id, value }
14243 | TriggerCondition::NotEq { column_id, value }
14244 | TriggerCondition::Lt { column_id, value }
14245 | TriggerCondition::Lte { column_id, value }
14246 | TriggerCondition::Gt { column_id, value }
14247 | TriggerCondition::Gte { column_id, value } => {
14248 validate_column_id(*column_id, schema)?;
14249 validate_trigger_value(value, target_schema, event)
14250 }
14251 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
14252 validate_column_id(*column_id, schema)
14253 }
14254 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
14255 validate_trigger_condition(left, schema, target_schema, event)?;
14256 validate_trigger_condition(right, schema, target_schema, event)
14257 }
14258 TriggerCondition::Not(condition) => {
14259 validate_trigger_condition(condition, schema, target_schema, event)
14260 }
14261 }
14262}
14263
14264fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
14265 match expr {
14266 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
14267 validate_trigger_value(value, schema, event)
14268 }
14269 TriggerExpr::Eq { left, right }
14270 | TriggerExpr::NotEq { left, right }
14271 | TriggerExpr::Lt { left, right }
14272 | TriggerExpr::Lte { left, right }
14273 | TriggerExpr::Gt { left, right }
14274 | TriggerExpr::Gte { left, right } => {
14275 validate_trigger_value(left, schema, event)?;
14276 validate_trigger_value(right, schema, event)
14277 }
14278 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
14279 validate_trigger_expr(left, schema, event)?;
14280 validate_trigger_expr(right, schema, event)
14281 }
14282 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
14283 }
14284}
14285
14286fn validate_trigger_value(
14287 value: &TriggerValue,
14288 schema: &Schema,
14289 event: TriggerEvent,
14290) -> Result<()> {
14291 match value {
14292 TriggerValue::Literal(_) => Ok(()),
14293 TriggerValue::NewColumn(id) => {
14294 if event == TriggerEvent::Delete {
14295 return Err(MongrelError::InvalidArgument(
14296 "DELETE triggers cannot reference NEW".into(),
14297 ));
14298 }
14299 validate_column_id(*id, schema)
14300 }
14301 TriggerValue::OldColumn(id) => {
14302 if event == TriggerEvent::Insert {
14303 return Err(MongrelError::InvalidArgument(
14304 "INSERT triggers cannot reference OLD".into(),
14305 ));
14306 }
14307 validate_column_id(*id, schema)
14308 }
14309 TriggerValue::SelectedColumn(_) => Ok(()),
14313 }
14314}
14315
14316fn recover_ddl_from_wal(
14322 root: &Path,
14323 durable_root: Option<&crate::durable_file::DurableRoot>,
14324 target_catalog: &mut Catalog,
14325 meta_dek: Option<&[u8; META_DEK_LEN]>,
14326 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
14327 apply: bool,
14328 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
14329) -> Result<()> {
14330 use crate::wal::SharedWal;
14331 let records = match durable_root {
14332 Some(root) => SharedWal::replay_durable_with_dek(root, wal_dek)?,
14333 None => SharedWal::replay_with_dek(root, wal_dek)?,
14334 };
14335 recover_ddl_from_records(
14336 root,
14337 durable_root,
14338 target_catalog,
14339 meta_dek,
14340 apply,
14341 table_roots,
14342 &records,
14343 )
14344}
14345
14346fn recover_ddl_from_records(
14347 root: &Path,
14348 durable_root: Option<&crate::durable_file::DurableRoot>,
14349 target_catalog: &mut Catalog,
14350 meta_dek: Option<&[u8; META_DEK_LEN]>,
14351 apply: bool,
14352 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
14353 records: &[crate::wal::Record],
14354) -> Result<()> {
14355 use crate::wal::{DdlOp, Op};
14356
14357 let original_catalog = target_catalog.clone();
14358 let mut recovered_catalog = original_catalog.clone();
14359 let cat = &mut recovered_catalog;
14360 let mut created_table_ids = HashSet::<u64>::new();
14361 let mut ttl_updates = HashMap::<u64, (Option<crate::manifest::TtlPolicy>, u64)>::new();
14362
14363 let mut committed: HashMap<u64, u64> = HashMap::new();
14364 for r in records {
14365 if let Op::TxnCommit { epoch: ce, .. } = r.op {
14366 committed.insert(r.txn_id, ce);
14367 }
14368 }
14369 let catalog_snapshot_txns = records
14370 .iter()
14371 .filter_map(|record| {
14372 (committed.contains_key(&record.txn_id)
14373 && matches!(&record.op, Op::Ddl(DdlOp::CatalogSnapshot { .. })))
14374 .then_some(record.txn_id)
14375 })
14376 .collect::<HashSet<_>>();
14377
14378 let mut changed = false;
14379 let mut applied_catalog_epoch = cat.db_epoch;
14380 let max_committed_epoch = committed.values().copied().max().unwrap_or(cat.db_epoch);
14381 for r in records.iter().cloned() {
14382 let Some(&ce) = committed.get(&r.txn_id) else {
14383 continue;
14384 };
14385 let txn_id = r.txn_id;
14386 match r.op {
14387 Op::Ddl(DdlOp::CreateTable {
14388 table_id,
14389 ref name,
14390 ref schema_json,
14391 }) => {
14392 if cat.tables.iter().any(|t| t.table_id == table_id) {
14393 continue;
14394 }
14395 let schema = DdlOp::decode_schema(schema_json)?;
14396 validate_recovered_schema(&schema)?;
14397 created_table_ids.insert(table_id);
14398 cat.tables.push(CatalogEntry {
14399 table_id,
14400 name: name.clone(),
14401 schema,
14402 state: TableState::Live,
14403 created_epoch: ce,
14404 });
14405 cat.next_table_id =
14406 cat.next_table_id
14407 .max(table_id.checked_add(1).ok_or_else(|| {
14408 MongrelError::Full("table id namespace exhausted".into())
14409 })?);
14410 changed = true;
14411 }
14412 Op::Ddl(DdlOp::CreateBuildingTable {
14413 table_id,
14414 ref build_name,
14415 ref intended_name,
14416 ref query_id,
14417 created_at_unix_nanos,
14418 ref schema_json,
14419 }) => {
14420 if cat.tables.iter().any(|table| table.table_id == table_id) {
14421 continue;
14422 }
14423 let schema = DdlOp::decode_schema(schema_json)?;
14424 validate_recovered_schema(&schema)?;
14425 created_table_ids.insert(table_id);
14426 cat.tables.push(CatalogEntry {
14427 table_id,
14428 name: build_name.clone(),
14429 schema,
14430 state: TableState::Building {
14431 intended_name: intended_name.clone(),
14432 query_id: query_id.clone(),
14433 created_at_unix_nanos,
14434 replaces_table_id: None,
14435 },
14436 created_epoch: ce,
14437 });
14438 cat.next_table_id =
14439 cat.next_table_id
14440 .max(table_id.checked_add(1).ok_or_else(|| {
14441 MongrelError::Full("table id namespace exhausted".into())
14442 })?);
14443 changed = true;
14444 }
14445 Op::Ddl(DdlOp::CreateRebuildingTable {
14446 table_id,
14447 ref build_name,
14448 ref intended_name,
14449 ref query_id,
14450 created_at_unix_nanos,
14451 replaces_table_id,
14452 ref schema_json,
14453 }) => {
14454 if cat.tables.iter().any(|table| table.table_id == table_id) {
14455 continue;
14456 }
14457 let schema = DdlOp::decode_schema(schema_json)?;
14458 validate_recovered_schema(&schema)?;
14459 created_table_ids.insert(table_id);
14460 cat.tables.push(CatalogEntry {
14461 table_id,
14462 name: build_name.clone(),
14463 schema,
14464 state: TableState::Building {
14465 intended_name: intended_name.clone(),
14466 query_id: query_id.clone(),
14467 created_at_unix_nanos,
14468 replaces_table_id: Some(replaces_table_id),
14469 },
14470 created_epoch: ce,
14471 });
14472 cat.next_table_id =
14473 cat.next_table_id
14474 .max(table_id.checked_add(1).ok_or_else(|| {
14475 MongrelError::Full("table id namespace exhausted".into())
14476 })?);
14477 changed = true;
14478 }
14479 Op::Ddl(DdlOp::DropTable { table_id }) => {
14480 let mut dropped_name = None;
14481 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
14482 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
14483 dropped_name = Some(entry.name.clone());
14484 entry.state = TableState::Dropped { at_epoch: ce };
14485 changed = true;
14486 }
14487 }
14488 if let Some(name) = dropped_name {
14489 let before = cat.materialized_views.len();
14490 cat.materialized_views
14491 .retain(|definition| definition.name != name);
14492 changed |= before != cat.materialized_views.len();
14493 cat.security.rls_tables.retain(|table| table != &name);
14494 cat.security.policies.retain(|policy| policy.table != name);
14495 cat.security.masks.retain(|mask| mask.table != name);
14496 for role in &mut cat.roles {
14497 role.permissions
14498 .retain(|permission| permission_table(permission) != Some(&name));
14499 }
14500 if !catalog_snapshot_txns.contains(&txn_id) {
14501 advance_security_version(cat)?;
14502 }
14503 }
14504 }
14505 Op::Ddl(DdlOp::PublishBuildingTable {
14506 table_id,
14507 ref new_name,
14508 }) => {
14509 if let Some(entry) = cat
14510 .tables
14511 .iter_mut()
14512 .find(|table| table.table_id == table_id)
14513 {
14514 if entry.name != *new_name || !matches!(entry.state, TableState::Live) {
14515 entry.name = new_name.clone();
14516 entry.state = TableState::Live;
14517 changed = true;
14518 }
14519 }
14520 }
14521 Op::Ddl(DdlOp::ReplaceBuildingTable {
14522 table_id,
14523 replaced_table_id,
14524 ref new_name,
14525 }) => {
14526 changed |=
14527 apply_rebuilding_publish(cat, table_id, replaced_table_id, new_name, ce)?;
14528 }
14529 Op::Ddl(DdlOp::RenameTable {
14530 table_id,
14531 ref new_name,
14532 }) => {
14533 let mut old_name = None;
14534 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
14535 if entry.name != *new_name {
14536 old_name = Some(entry.name.clone());
14537 entry.name = new_name.clone();
14538 changed = true;
14539 }
14540 }
14541 if let Some(old_name) = old_name {
14542 if let Some(definition) = cat
14543 .materialized_views
14544 .iter_mut()
14545 .find(|definition| definition.name == old_name)
14546 {
14547 definition.name = new_name.clone();
14548 }
14549 for table in &mut cat.security.rls_tables {
14550 if *table == old_name {
14551 *table = new_name.clone();
14552 }
14553 }
14554 for policy in &mut cat.security.policies {
14555 if policy.table == old_name {
14556 policy.table = new_name.clone();
14557 }
14558 }
14559 for mask in &mut cat.security.masks {
14560 if mask.table == old_name {
14561 mask.table = new_name.clone();
14562 }
14563 }
14564 for role in &mut cat.roles {
14565 for permission in &mut role.permissions {
14566 rename_permission_table(permission, &old_name, new_name);
14567 }
14568 }
14569 if !catalog_snapshot_txns.contains(&txn_id) {
14570 advance_security_version(cat)?;
14571 }
14572 }
14573 }
14577 Op::Ddl(DdlOp::AlterTable {
14578 table_id,
14579 ref column_json,
14580 }) => {
14581 let column = DdlOp::decode_column(column_json)?;
14582 let mut renamed = None;
14583 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
14584 renamed = entry
14585 .schema
14586 .columns
14587 .iter()
14588 .find(|existing| existing.id == column.id && existing.name != column.name)
14589 .map(|existing| {
14590 (
14591 entry.name.clone(),
14592 existing.name.clone(),
14593 column.name.clone(),
14594 )
14595 });
14596 if apply_recovered_column_def(&mut entry.schema, column)? {
14597 validate_recovered_schema(&entry.schema)?;
14598 changed = true;
14599 }
14600 }
14601 if let Some((table, old_name, new_name)) = renamed {
14602 for role in &mut cat.roles {
14603 for permission in &mut role.permissions {
14604 rename_permission_column(permission, &table, &old_name, &new_name);
14605 }
14606 }
14607 if !catalog_snapshot_txns.contains(&txn_id) {
14608 advance_security_version(cat)?;
14609 }
14610 }
14611 }
14612 Op::Ddl(DdlOp::SetTtl {
14613 table_id,
14614 ref policy_json,
14615 }) => {
14616 let policy = DdlOp::decode_ttl(policy_json)?;
14617 let entry = cat
14618 .tables
14619 .iter()
14620 .find(|entry| entry.table_id == table_id)
14621 .ok_or_else(|| {
14622 MongrelError::Schema(format!(
14623 "recovered TTL references unknown table id {table_id}"
14624 ))
14625 })?;
14626 if let Some(policy) = policy {
14627 let valid = entry
14628 .schema
14629 .columns
14630 .iter()
14631 .find(|column| column.id == policy.column_id)
14632 .is_some_and(|column| {
14633 column.ty == TypeId::TimestampNanos
14634 && policy.duration_nanos > 0
14635 && policy.duration_nanos <= i64::MAX as u64
14636 });
14637 if !valid {
14638 return Err(MongrelError::Schema(format!(
14639 "invalid recovered TTL policy for table id {table_id}"
14640 )));
14641 }
14642 }
14643 ttl_updates.insert(table_id, (policy, ce));
14644 }
14645 Op::Ddl(DdlOp::SetMaterializedView {
14646 ref name,
14647 ref definition_json,
14648 }) => {
14649 let definition = DdlOp::decode_materialized_view(definition_json)?;
14650 if definition.name != *name {
14651 return Err(MongrelError::Schema(format!(
14652 "materialized view WAL name mismatch: {name:?}"
14653 )));
14654 }
14655 if cat.live(name).is_some() {
14656 if let Some(existing) = cat
14657 .materialized_views
14658 .iter_mut()
14659 .find(|existing| existing.name == *name)
14660 {
14661 if *existing != definition {
14662 *existing = definition;
14663 changed = true;
14664 }
14665 } else {
14666 cat.materialized_views.push(definition);
14667 changed = true;
14668 }
14669 }
14670 }
14671 Op::Ddl(DdlOp::SetSecurityCatalog { ref security_json }) => {
14672 let security = DdlOp::decode_security(security_json)?;
14673 validate_security_catalog(cat, &security)?;
14674 if cat.security != security {
14675 cat.security = security;
14676 if !catalog_snapshot_txns.contains(&txn_id) {
14677 advance_security_version(cat)?;
14678 }
14679 changed = true;
14680 }
14681 }
14682 Op::Ddl(DdlOp::SetSqlPragma { ref key, value }) => {
14683 let target = match key.as_str() {
14684 "user_version" => &mut cat.user_version,
14685 "application_id" => &mut cat.application_id,
14686 _ => {
14687 return Err(MongrelError::InvalidArgument(format!(
14688 "unsupported recovered SQL pragma {key:?}"
14689 )))
14690 }
14691 };
14692 if *target != Some(value) {
14693 *target = Some(value);
14694 cat.db_epoch = cat.db_epoch.max(ce);
14695 changed = true;
14696 }
14697 }
14698 Op::Ddl(DdlOp::CatalogSnapshot { ref catalog_json }) => {
14699 if ce <= applied_catalog_epoch {
14700 continue;
14701 }
14702 let snapshot = DdlOp::decode_catalog(catalog_json)?;
14703 if snapshot.db_epoch != ce {
14704 return Err(MongrelError::Schema(format!(
14705 "catalog snapshot epoch {} does not match WAL commit epoch {ce}",
14706 snapshot.db_epoch
14707 )));
14708 }
14709 validate_recovered_catalog(&snapshot)?;
14710 validate_catalog_transition(cat, &snapshot)?;
14711 *cat = snapshot;
14712 applied_catalog_epoch = ce;
14713 changed = true;
14714 }
14715 _ => {}
14716 }
14717 }
14718
14719 if cat.db_epoch < max_committed_epoch {
14720 cat.db_epoch = max_committed_epoch;
14721 changed = true;
14722 }
14723 changed |= repair_catalog_allocator_counters(cat)?;
14724
14725 validate_recovered_catalog(cat)?;
14726 let storage_reconciliation = validate_recovered_storage_plan(
14727 root,
14728 durable_root,
14729 cat,
14730 &created_table_ids,
14731 &ttl_updates,
14732 meta_dek,
14733 )?;
14734
14735 let needs_storage_apply = !storage_reconciliation.is_empty() || !ttl_updates.is_empty();
14736 if apply && (changed || needs_storage_apply) {
14737 for table_id in storage_reconciliation {
14738 let entry = cat
14739 .tables
14740 .iter()
14741 .find(|entry| entry.table_id == table_id)
14742 .ok_or_else(|| MongrelError::CorruptWal {
14743 offset: table_id,
14744 reason: "recovery storage plan lost its catalog table".into(),
14745 })?;
14746 ensure_recovered_table_storage(
14747 table_roots
14748 .and_then(|roots| roots.get(&table_id))
14749 .map(Arc::as_ref),
14750 durable_root,
14751 &root.join(TABLES_DIR).join(table_id.to_string()),
14752 table_id,
14753 &entry.schema,
14754 meta_dek,
14755 )?;
14756 }
14757 for (table_id, (policy, ttl_epoch)) in ttl_updates {
14758 let Some(entry) = cat.tables.iter().find(|entry| {
14759 entry.table_id == table_id
14760 && matches!(entry.state, TableState::Live | TableState::Building { .. })
14761 }) else {
14762 continue;
14763 };
14764 let table_root = if let Some(root) = table_roots.and_then(|roots| roots.get(&table_id))
14765 {
14766 root.try_clone()?
14767 } else if let Some(root) = durable_root {
14768 root.open_directory(Path::new(TABLES_DIR).join(table_id.to_string()))?
14769 } else {
14770 crate::durable_file::DurableRoot::open(
14771 root.join(TABLES_DIR).join(table_id.to_string()),
14772 )?
14773 };
14774 let table_dir = table_root.io_path()?;
14775 let mut manifest = crate::manifest::read_durable(&table_root, "", meta_dek)?;
14776 if manifest.ttl != policy || manifest.current_epoch < ttl_epoch {
14777 manifest.ttl = policy;
14778 manifest.current_epoch = manifest.current_epoch.max(ttl_epoch);
14779 manifest.schema_id = entry.schema.schema_id;
14780 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
14781 }
14782 }
14783 if changed {
14784 match durable_root {
14785 Some(root) => catalog::write_durable(root, cat, meta_dek)?,
14786 None => catalog::write_atomic(root, cat, meta_dek)?,
14787 }
14788 }
14789 }
14790 *target_catalog = recovered_catalog;
14791 Ok(())
14792}
14793
14794fn ensure_recovered_table_storage(
14795 pinned_table: Option<&crate::durable_file::DurableRoot>,
14796 durable_root: Option<&crate::durable_file::DurableRoot>,
14797 fallback_table_dir: &Path,
14798 table_id: u64,
14799 schema: &Schema,
14800 meta_dek: Option<&[u8; META_DEK_LEN]>,
14801) -> Result<()> {
14802 let table_root = if let Some(root) = pinned_table {
14803 root.try_clone()?
14804 } else if let Some(root) = durable_root {
14805 let relative = Path::new(TABLES_DIR).join(table_id.to_string());
14806 match root.open_directory(&relative) {
14807 Ok(table) => table,
14808 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
14809 root.create_directory_all_pinned(relative)?
14810 }
14811 Err(error) => return Err(error.into()),
14812 }
14813 } else {
14814 crate::durable_file::create_directory_all(fallback_table_dir)?;
14815 crate::durable_file::DurableRoot::open(fallback_table_dir)?
14816 };
14817 let table_dir = table_root.io_path()?;
14818 let mut existing_manifest = match crate::manifest::read_durable(&table_root, "", meta_dek) {
14819 Ok(manifest) => {
14820 if manifest.table_id != table_id {
14821 return Err(MongrelError::Conflict(format!(
14822 "recovered table directory id mismatch: expected {table_id}, found {}",
14823 manifest.table_id
14824 )));
14825 }
14826 Some(manifest)
14827 }
14828 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
14829 Err(error) => return Err(error),
14830 };
14831
14832 table_root.create_directory_all(crate::engine::WAL_DIR)?;
14833 table_root.create_directory_all(crate::engine::RUNS_DIR)?;
14834 crate::engine::write_schema(&table_dir, schema)?;
14835
14836 if let Some(mut manifest) = existing_manifest.take() {
14837 if manifest.schema_id != schema.schema_id {
14838 manifest.schema_id = schema.schema_id;
14839 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
14840 }
14841 } else {
14842 let mut manifest = crate::manifest::Manifest::new(table_id, schema.schema_id);
14844 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
14845 }
14846 Ok(())
14847}
14848
14849fn validate_recovered_schema(schema: &Schema) -> Result<()> {
14850 schema.validate_auto_increment()?;
14851 schema.validate_defaults()?;
14852 schema.validate_ai()?;
14853 let mut column_ids = HashSet::new();
14854 let mut column_names = HashSet::new();
14855 for column in &schema.columns {
14856 if !column_ids.insert(column.id) || !column_names.insert(column.name.as_str()) {
14857 return Err(MongrelError::Schema(
14858 "recovered schema contains duplicate columns".into(),
14859 ));
14860 }
14861 match &column.ty {
14862 TypeId::Decimal128 { precision, scale }
14863 if *precision == 0 || *precision > 38 || scale.unsigned_abs() > *precision =>
14864 {
14865 return Err(MongrelError::Schema(format!(
14866 "column {:?} has invalid decimal precision or scale",
14867 column.name
14868 )));
14869 }
14870 TypeId::Enum { variants }
14871 if variants.is_empty()
14872 || variants.iter().any(String::is_empty)
14873 || variants.iter().collect::<HashSet<_>>().len() != variants.len() =>
14874 {
14875 return Err(MongrelError::Schema(format!(
14876 "column {:?} has invalid enum variants",
14877 column.name
14878 )));
14879 }
14880 _ => {}
14881 }
14882 }
14883 let mut index_names = HashSet::new();
14884 for index in &schema.indexes {
14885 index.validate_options()?;
14886 if index.name.is_empty()
14887 || !index_names.insert(index.name.as_str())
14888 || schema
14889 .columns
14890 .iter()
14891 .all(|column| column.id != index.column_id)
14892 {
14893 return Err(MongrelError::Schema(format!(
14894 "recovered index {:?} references missing column {}",
14895 index.name, index.column_id
14896 )));
14897 }
14898 }
14899 let mut colocated = HashSet::new();
14900 for group in &schema.colocation {
14901 if group.is_empty()
14902 || group.iter().any(|id| !column_ids.contains(id))
14903 || group.iter().any(|id| !colocated.insert(*id))
14904 {
14905 return Err(MongrelError::Schema(
14906 "recovered schema contains invalid column co-location groups".into(),
14907 ));
14908 }
14909 }
14910
14911 let mut constraint_ids = HashSet::new();
14912 let mut constraint_names = HashSet::<String>::new();
14913 let mut validate_constraint_identity = |id: u16, name: &str| -> Result<()> {
14914 if name.is_empty()
14915 || !constraint_ids.insert(id)
14916 || !constraint_names.insert(name.to_owned())
14917 {
14918 return Err(MongrelError::Schema(
14919 "recovered schema contains duplicate or empty constraint identities".into(),
14920 ));
14921 }
14922 Ok(())
14923 };
14924 for unique in &schema.constraints.uniques {
14925 validate_constraint_identity(unique.id, &unique.name)?;
14926 if unique.columns.is_empty()
14927 || unique.columns.iter().any(|id| !column_ids.contains(id))
14928 || unique.columns.iter().collect::<HashSet<_>>().len() != unique.columns.len()
14929 {
14930 return Err(MongrelError::Schema(format!(
14931 "unique constraint {:?} has invalid columns",
14932 unique.name
14933 )));
14934 }
14935 }
14936 for foreign_key in &schema.constraints.foreign_keys {
14937 validate_constraint_identity(foreign_key.id, &foreign_key.name)?;
14938 if foreign_key.ref_table.is_empty()
14939 || foreign_key.columns.is_empty()
14940 || foreign_key.columns.len() != foreign_key.ref_columns.len()
14941 || foreign_key
14942 .columns
14943 .iter()
14944 .any(|id| !column_ids.contains(id))
14945 || foreign_key.columns.iter().collect::<HashSet<_>>().len() != foreign_key.columns.len()
14946 || foreign_key.ref_columns.iter().collect::<HashSet<_>>().len()
14947 != foreign_key.ref_columns.len()
14948 {
14949 return Err(MongrelError::Schema(format!(
14950 "foreign key {:?} has invalid columns",
14951 foreign_key.name
14952 )));
14953 }
14954 if (matches!(foreign_key.on_delete, crate::constraint::FkAction::SetNull)
14955 || matches!(foreign_key.on_update, crate::constraint::FkAction::SetNull))
14956 && foreign_key.columns.iter().any(|id| {
14957 schema
14958 .columns
14959 .iter()
14960 .find(|column| column.id == *id)
14961 .is_none_or(|column| {
14962 !column.flags.contains(crate::schema::ColumnFlags::NULLABLE)
14963 })
14964 })
14965 {
14966 return Err(MongrelError::Schema(format!(
14967 "foreign key {:?} uses SET NULL on a non-nullable column",
14968 foreign_key.name
14969 )));
14970 }
14971 }
14972 for check in &schema.constraints.checks {
14973 validate_constraint_identity(check.id, &check.name)?;
14974 check.expr.validate()?;
14975 validate_check_columns(&check.expr, &column_ids)?;
14976 }
14977 Ok(())
14978}
14979
14980fn validate_check_columns(
14981 expression: &crate::constraint::CheckExpr,
14982 column_ids: &HashSet<u16>,
14983) -> Result<()> {
14984 use crate::constraint::CheckExpr;
14985 match expression {
14986 CheckExpr::Col(id) | CheckExpr::IsNull(id) | CheckExpr::IsNotNull(id) => {
14987 if column_ids.contains(id) {
14988 Ok(())
14989 } else {
14990 Err(MongrelError::Schema(format!(
14991 "check constraint references unknown column {id}"
14992 )))
14993 }
14994 }
14995 CheckExpr::Regex { col, .. } => {
14996 if column_ids.contains(col) {
14997 Ok(())
14998 } else {
14999 Err(MongrelError::Schema(format!(
15000 "check constraint references unknown column {col}"
15001 )))
15002 }
15003 }
15004 CheckExpr::Add(left, right)
15005 | CheckExpr::Sub(left, right)
15006 | CheckExpr::Mul(left, right)
15007 | CheckExpr::Div(left, right)
15008 | CheckExpr::Mod(left, right)
15009 | CheckExpr::Eq(left, right)
15010 | CheckExpr::Ne(left, right)
15011 | CheckExpr::Lt(left, right)
15012 | CheckExpr::Le(left, right)
15013 | CheckExpr::Gt(left, right)
15014 | CheckExpr::Ge(left, right)
15015 | CheckExpr::And(left, right)
15016 | CheckExpr::Or(left, right) => {
15017 validate_check_columns(left, column_ids)?;
15018 validate_check_columns(right, column_ids)
15019 }
15020 CheckExpr::Not(inner) => validate_check_columns(inner, column_ids),
15021 CheckExpr::True | CheckExpr::Lit(_) => Ok(()),
15022 }
15023}
15024
15025fn validate_catalog_transition(current: &Catalog, next: &Catalog) -> Result<()> {
15026 for (name, prior, candidate) in [
15027 ("db_epoch", current.db_epoch, next.db_epoch),
15028 ("next_table_id", current.next_table_id, next.next_table_id),
15029 (
15030 "next_segment_no",
15031 current.next_segment_no,
15032 next.next_segment_no,
15033 ),
15034 ("next_user_id", current.next_user_id, next.next_user_id),
15035 (
15036 "security_version",
15037 current.security_version,
15038 next.security_version,
15039 ),
15040 ] {
15041 if candidate < prior {
15042 return Err(MongrelError::Schema(format!(
15043 "catalog snapshot rolls back {name} from {prior} to {candidate}"
15044 )));
15045 }
15046 }
15047 for prior in ¤t.tables {
15048 let Some(candidate) = next
15049 .tables
15050 .iter()
15051 .find(|entry| entry.table_id == prior.table_id)
15052 else {
15053 return Err(MongrelError::Schema(format!(
15054 "catalog snapshot removes table identity {}",
15055 prior.table_id
15056 )));
15057 };
15058 if candidate.created_epoch != prior.created_epoch
15059 || candidate.schema.schema_id < prior.schema.schema_id
15060 || matches!(prior.state, TableState::Dropped { .. })
15061 && !matches!(candidate.state, TableState::Dropped { .. })
15062 {
15063 return Err(MongrelError::Schema(format!(
15064 "catalog snapshot rolls back table identity {}",
15065 prior.table_id
15066 )));
15067 }
15068 }
15069 for prior in ¤t.users {
15070 if let Some(candidate) = next.users.iter().find(|user| user.id == prior.id) {
15071 if candidate.username != prior.username
15072 || candidate.created_epoch != prior.created_epoch
15073 {
15074 return Err(MongrelError::Schema(format!(
15075 "catalog snapshot reuses user identity {}",
15076 prior.id
15077 )));
15078 }
15079 }
15080 }
15081 Ok(())
15082}
15083
15084fn validate_recovered_catalog(catalog: &Catalog) -> Result<()> {
15085 let mut table_ids = HashSet::new();
15086 let mut active_names = HashSet::new();
15087 let mut max_table_id = None::<u64>;
15088 for entry in &catalog.tables {
15089 if !table_ids.insert(entry.table_id) {
15090 return Err(MongrelError::Schema(format!(
15091 "catalog contains duplicate table id {}",
15092 entry.table_id
15093 )));
15094 }
15095 max_table_id = Some(max_table_id.map_or(entry.table_id, |value| value.max(entry.table_id)));
15096 if entry.name.is_empty() || entry.created_epoch > catalog.db_epoch {
15097 return Err(MongrelError::Schema(format!(
15098 "catalog table {} has invalid name or creation epoch",
15099 entry.table_id
15100 )));
15101 }
15102 validate_recovered_schema(&entry.schema)?;
15103 match &entry.state {
15104 TableState::Live => {
15105 if !active_names.insert(entry.name.as_str()) {
15106 return Err(MongrelError::Schema(format!(
15107 "catalog contains duplicate active table name {:?}",
15108 entry.name
15109 )));
15110 }
15111 }
15112 TableState::Dropped { at_epoch } => {
15113 if *at_epoch < entry.created_epoch || *at_epoch > catalog.db_epoch {
15114 return Err(MongrelError::Schema(format!(
15115 "catalog table {} has invalid drop epoch {at_epoch}",
15116 entry.table_id
15117 )));
15118 }
15119 }
15120 TableState::Building {
15121 intended_name,
15122 query_id,
15123 replaces_table_id,
15124 ..
15125 } => {
15126 if intended_name.is_empty() || query_id.is_empty() {
15127 return Err(MongrelError::Schema(format!(
15128 "building table {} has empty identity fields",
15129 entry.table_id
15130 )));
15131 }
15132 if !active_names.insert(entry.name.as_str()) {
15133 return Err(MongrelError::Schema(format!(
15134 "catalog contains duplicate active/building table name {:?}",
15135 entry.name
15136 )));
15137 }
15138 if replaces_table_id.is_some_and(|id| id == entry.table_id) {
15139 return Err(MongrelError::Schema(
15140 "building table cannot replace itself".into(),
15141 ));
15142 }
15143 }
15144 }
15145 }
15146 if let Some(maximum) = max_table_id {
15147 let required = maximum
15148 .checked_add(1)
15149 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
15150 if catalog.next_table_id < required {
15151 return Err(MongrelError::Schema(format!(
15152 "catalog next_table_id {} precedes required {required}",
15153 catalog.next_table_id
15154 )));
15155 }
15156 }
15157 for entry in &catalog.tables {
15158 if let TableState::Building {
15159 replaces_table_id: Some(replaced),
15160 ..
15161 } = entry.state
15162 {
15163 if !table_ids.contains(&replaced) {
15164 return Err(MongrelError::Schema(format!(
15165 "building table {} replaces unknown table {replaced}",
15166 entry.table_id
15167 )));
15168 }
15169 }
15170 }
15171 for entry in &catalog.tables {
15172 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
15173 validate_foreign_key_targets(catalog, &entry.schema)?;
15174 }
15175 }
15176
15177 let mut external_names = HashSet::new();
15178 for entry in &catalog.external_tables {
15179 entry.validate()?;
15180 validate_recovered_schema(&entry.declared_schema)?;
15181 if !entry.declared_schema.constraints.is_empty() {
15182 return Err(MongrelError::Schema(format!(
15183 "external table {:?} cannot carry engine-enforced constraints",
15184 entry.name
15185 )));
15186 }
15187 if entry.created_epoch > catalog.db_epoch
15188 || !external_names.insert(entry.name.as_str())
15189 || active_names.contains(entry.name.as_str())
15190 {
15191 return Err(MongrelError::Schema(format!(
15192 "invalid or duplicate external table {:?}",
15193 entry.name
15194 )));
15195 }
15196 }
15197
15198 let mut procedure_names = HashSet::new();
15199 for entry in &catalog.procedures {
15200 entry.procedure.validate()?;
15201 if entry.procedure.created_epoch > entry.procedure.updated_epoch
15202 || entry.procedure.updated_epoch > catalog.db_epoch
15203 || !procedure_names.insert(entry.procedure.name.as_str())
15204 {
15205 return Err(MongrelError::Schema(format!(
15206 "invalid or duplicate procedure {:?}",
15207 entry.procedure.name
15208 )));
15209 }
15210 validate_recovered_procedure_references(catalog, &entry.procedure)?;
15211 }
15212
15213 let mut trigger_names = HashSet::new();
15214 for entry in &catalog.triggers {
15215 entry.trigger.validate()?;
15216 if entry.trigger.created_epoch > entry.trigger.updated_epoch
15217 || entry.trigger.updated_epoch > catalog.db_epoch
15218 || !trigger_names.insert(entry.trigger.name.as_str())
15219 {
15220 return Err(MongrelError::Schema(format!(
15221 "invalid or duplicate trigger {:?}",
15222 entry.trigger.name
15223 )));
15224 }
15225 validate_recovered_trigger_references(catalog, &entry.trigger)?;
15226 }
15227
15228 let mut views = HashSet::new();
15229 for view in &catalog.materialized_views {
15230 let target = catalog.live(&view.name).ok_or_else(|| {
15231 MongrelError::Schema(format!(
15232 "materialized view {:?} has no live table",
15233 view.name
15234 ))
15235 })?;
15236 if view.name.is_empty()
15237 || view.query.trim().is_empty()
15238 || view.last_refresh_epoch > catalog.db_epoch
15239 || !views.insert(view.name.as_str())
15240 {
15241 return Err(MongrelError::Schema(format!(
15242 "materialized view {:?} has no unique live table",
15243 view.name
15244 )));
15245 }
15246 if let Some(incremental) = &view.incremental {
15247 let source = catalog.live(&incremental.source_table).ok_or_else(|| {
15248 MongrelError::Schema(format!(
15249 "materialized view {:?} references missing source {:?}",
15250 view.name, incremental.source_table
15251 ))
15252 })?;
15253 if source.table_id != incremental.source_table_id
15254 || source
15255 .schema
15256 .columns
15257 .iter()
15258 .all(|column| column.id != incremental.group_column)
15259 {
15260 return Err(MongrelError::Schema(format!(
15261 "materialized view {:?} has invalid incremental source",
15262 view.name
15263 )));
15264 }
15265 let target_ids = target
15266 .schema
15267 .columns
15268 .iter()
15269 .map(|column| column.id)
15270 .collect::<HashSet<_>>();
15271 let mut output_ids = HashSet::new();
15272 let count_outputs = incremental
15273 .outputs
15274 .iter()
15275 .filter(|output| {
15276 matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
15277 })
15278 .count();
15279 if incremental.checkpoint_event_id.is_empty()
15280 || !target_ids.contains(&incremental.group_output_column)
15281 || !target_ids.contains(&incremental.count_output_column)
15282 || incremental.outputs.is_empty()
15283 || count_outputs != 1
15284 || incremental.outputs.iter().any(|output| {
15285 !target_ids.contains(&output.output_column)
15286 || output.output_column == incremental.group_output_column
15287 || !output_ids.insert(output.output_column)
15288 || matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
15289 && output.output_column != incremental.count_output_column
15290 || match output.kind {
15291 crate::catalog::IncrementalAggregateKind::Sum { source_column } => {
15292 source
15293 .schema
15294 .columns
15295 .iter()
15296 .all(|column| column.id != source_column)
15297 }
15298 crate::catalog::IncrementalAggregateKind::Count => false,
15299 }
15300 })
15301 {
15302 return Err(MongrelError::Schema(format!(
15303 "materialized view {:?} has invalid incremental outputs",
15304 view.name
15305 )));
15306 }
15307 }
15308 }
15309
15310 validate_security_catalog(catalog, &catalog.security)?;
15311 validate_recovered_auth_catalog(catalog)?;
15312 Ok(())
15313}
15314
15315fn repair_catalog_allocator_counters(catalog: &mut Catalog) -> Result<bool> {
15316 let mut changed = false;
15317 if let Some(maximum) = catalog.tables.iter().map(|entry| entry.table_id).max() {
15318 let required = maximum
15319 .checked_add(1)
15320 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
15321 if catalog.next_table_id < required {
15322 catalog.next_table_id = required;
15323 changed = true;
15324 }
15325 }
15326 if let Some(maximum) = catalog.users.iter().map(|user| user.id).max() {
15327 let required = maximum
15328 .checked_add(1)
15329 .ok_or_else(|| MongrelError::Full("user id namespace exhausted".into()))?;
15330 if catalog.next_user_id < required {
15331 catalog.next_user_id = required;
15332 changed = true;
15333 }
15334 }
15335 Ok(changed)
15336}
15337
15338fn validate_foreign_key_targets(catalog: &Catalog, schema: &Schema) -> Result<()> {
15339 for foreign_key in &schema.constraints.foreign_keys {
15340 let parent = catalog.live(&foreign_key.ref_table).ok_or_else(|| {
15341 MongrelError::Schema(format!(
15342 "foreign key {:?} references unknown live table {:?}",
15343 foreign_key.name, foreign_key.ref_table
15344 ))
15345 })?;
15346 let referenced_unique = parent
15347 .schema
15348 .constraints
15349 .uniques
15350 .iter()
15351 .any(|unique| unique.columns == foreign_key.ref_columns)
15352 || foreign_key.ref_columns.len() == 1
15353 && parent
15354 .schema
15355 .primary_key()
15356 .is_some_and(|column| column.id == foreign_key.ref_columns[0]);
15357 if !referenced_unique {
15358 return Err(MongrelError::Schema(format!(
15359 "foreign key {:?} does not reference a unique key",
15360 foreign_key.name
15361 )));
15362 }
15363 for (local_id, parent_id) in foreign_key.columns.iter().zip(&foreign_key.ref_columns) {
15364 let local = schema.columns.iter().find(|column| column.id == *local_id);
15365 let referenced = parent
15366 .schema
15367 .columns
15368 .iter()
15369 .find(|column| column.id == *parent_id);
15370 if local
15371 .zip(referenced)
15372 .is_none_or(|(local, referenced)| local.ty != referenced.ty)
15373 {
15374 return Err(MongrelError::Schema(format!(
15375 "foreign key {:?} has missing or incompatible columns",
15376 foreign_key.name
15377 )));
15378 }
15379 }
15380 }
15381 Ok(())
15382}
15383
15384fn validate_recovered_procedure_references(
15385 catalog: &Catalog,
15386 procedure: &StoredProcedure,
15387) -> Result<()> {
15388 for step in &procedure.body.steps {
15389 let Some(table_name) = step.table() else {
15390 continue;
15391 };
15392 let schema = &catalog
15393 .live(table_name)
15394 .ok_or_else(|| {
15395 MongrelError::Schema(format!(
15396 "procedure {:?} references unknown table {table_name:?}",
15397 procedure.name
15398 ))
15399 })?
15400 .schema;
15401 match step {
15402 ProcedureStep::NativeQuery {
15403 conditions,
15404 projection,
15405 ..
15406 } => {
15407 for condition in conditions {
15408 validate_condition_columns(condition, schema)?;
15409 }
15410 for id in projection.iter().flatten() {
15411 validate_column_id(*id, schema)?;
15412 }
15413 }
15414 ProcedureStep::Put { cells, .. } => {
15415 for cell in cells {
15416 validate_column_id(cell.column_id, schema)?;
15417 }
15418 }
15419 ProcedureStep::Upsert {
15420 cells,
15421 update_cells,
15422 ..
15423 } => {
15424 for cell in cells.iter().chain(update_cells.iter().flatten()) {
15425 validate_column_id(cell.column_id, schema)?;
15426 }
15427 }
15428 ProcedureStep::DeleteByPk { .. } if schema.primary_key().is_none() => {
15429 return Err(MongrelError::Schema(format!(
15430 "procedure {:?} deletes by primary key on table without one",
15431 procedure.name
15432 )));
15433 }
15434 ProcedureStep::DeleteByPk { .. }
15435 | ProcedureStep::DeleteRows { .. }
15436 | ProcedureStep::SqlQuery { .. } => {}
15437 }
15438 }
15439 Ok(())
15440}
15441
15442fn validate_recovered_trigger_references(catalog: &Catalog, trigger: &StoredTrigger) -> Result<()> {
15443 let target_schema = match &trigger.target {
15444 TriggerTarget::Table(name) => catalog
15445 .live(name)
15446 .ok_or_else(|| {
15447 MongrelError::Schema(format!(
15448 "trigger {:?} references unknown table {name:?}",
15449 trigger.name
15450 ))
15451 })?
15452 .schema
15453 .clone(),
15454 TriggerTarget::View(_) => Schema {
15455 columns: trigger.target_columns.clone(),
15456 ..Schema::default()
15457 },
15458 };
15459 for column in &trigger.update_of {
15460 if target_schema.column(column).is_none() {
15461 return Err(MongrelError::Schema(format!(
15462 "trigger {:?} references unknown UPDATE OF column {column:?}",
15463 trigger.name
15464 )));
15465 }
15466 }
15467 if let Some(expr) = &trigger.when {
15468 validate_trigger_expr(expr, &target_schema, trigger.event)?;
15469 }
15470 let mut selects = HashMap::new();
15471 for step in &trigger.program.steps {
15472 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before {
15473 return Err(MongrelError::Schema(
15474 "SetNew is only valid in BEFORE triggers".into(),
15475 ));
15476 }
15477 validate_trigger_step(step, catalog, &target_schema, trigger.event, &mut selects)?;
15478 }
15479 Ok(())
15480}
15481
15482fn validate_recovered_auth_catalog(catalog: &Catalog) -> Result<()> {
15483 let mut role_names = HashSet::new();
15484 for role in &catalog.roles {
15485 if role.name.is_empty()
15486 || role.created_epoch > catalog.db_epoch
15487 || !role_names.insert(role.name.as_str())
15488 {
15489 return Err(MongrelError::Schema(format!(
15490 "invalid or duplicate role {:?}",
15491 role.name
15492 )));
15493 }
15494 for permission in &role.permissions {
15495 if let Some(table) = permission_table(permission) {
15496 let schema = catalog
15497 .live(table)
15498 .map(|entry| &entry.schema)
15499 .or_else(|| {
15500 catalog
15501 .external_tables
15502 .iter()
15503 .find(|entry| entry.name == table)
15504 .map(|entry| &entry.declared_schema)
15505 })
15506 .ok_or_else(|| {
15507 MongrelError::Schema(format!(
15508 "role {:?} references unknown table {table:?}",
15509 role.name
15510 ))
15511 })?;
15512 let columns = match permission {
15513 crate::auth::Permission::SelectColumns { columns, .. }
15514 | crate::auth::Permission::InsertColumns { columns, .. }
15515 | crate::auth::Permission::UpdateColumns { columns, .. } => Some(columns),
15516 _ => None,
15517 };
15518 if columns.is_some_and(|columns| {
15519 columns.is_empty()
15520 || columns.iter().any(|column| schema.column(column).is_none())
15521 }) {
15522 return Err(MongrelError::Schema(format!(
15523 "role {:?} contains invalid column permissions",
15524 role.name
15525 )));
15526 }
15527 }
15528 }
15529 }
15530 let mut user_ids = HashSet::new();
15531 let mut usernames = HashSet::new();
15532 let mut maximum_user_id = 0;
15533 for user in &catalog.users {
15534 maximum_user_id = maximum_user_id.max(user.id);
15535 if user.id == 0
15536 || user.username.is_empty()
15537 || user.password_hash.is_empty()
15538 || user.created_epoch > catalog.db_epoch
15539 || !user_ids.insert(user.id)
15540 || !usernames.insert(user.username.as_str())
15541 || user
15542 .roles
15543 .iter()
15544 .any(|role| !role_names.contains(role.as_str()))
15545 {
15546 return Err(MongrelError::Schema(format!(
15547 "invalid or duplicate user {:?}",
15548 user.username
15549 )));
15550 }
15551 }
15552 if !catalog.users.is_empty() && catalog.next_user_id <= maximum_user_id {
15553 return Err(MongrelError::Schema(
15554 "catalog next_user_id does not advance beyond existing user ids".into(),
15555 ));
15556 }
15557 if catalog.require_auth && !catalog.users.iter().any(|user| user.is_admin) {
15558 return Err(MongrelError::Schema(
15559 "authenticated catalog has no administrator".into(),
15560 ));
15561 }
15562 Ok(())
15563}
15564
15565fn validate_recovered_storage_plan(
15566 root: &Path,
15567 durable_root: Option<&crate::durable_file::DurableRoot>,
15568 catalog: &Catalog,
15569 created_table_ids: &HashSet<u64>,
15570 ttl_updates: &HashMap<u64, (Option<crate::manifest::TtlPolicy>, u64)>,
15571 meta_dek: Option<&[u8; META_DEK_LEN]>,
15572) -> Result<Vec<u64>> {
15573 const MAX_SCHEMA_BYTES: u64 = 16 * 1024 * 1024;
15574 let mut reconcile = Vec::new();
15575 for entry in &catalog.tables {
15576 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
15577 continue;
15578 }
15579 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
15580 let table_dir = root.join(TABLES_DIR).join(entry.table_id.to_string());
15581 let table_exists = match durable_root {
15582 Some(root) => match root.open_directory(&relative_dir) {
15583 Ok(_) => true,
15584 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
15585 Err(error) => return Err(error.into()),
15586 },
15587 None => table_dir.is_dir(),
15588 };
15589 if !table_exists {
15590 if created_table_ids.contains(&entry.table_id) {
15591 reconcile.push(entry.table_id);
15592 continue;
15593 }
15594 return Err(MongrelError::NotFound(format!(
15595 "catalog table {} storage is missing",
15596 entry.table_id
15597 )));
15598 }
15599 let manifest_result = match durable_root {
15600 Some(root) => crate::manifest::read_durable(root, &relative_dir, meta_dek),
15601 None => crate::manifest::read(&table_dir, meta_dek),
15602 };
15603 let manifest = match manifest_result {
15604 Ok(manifest) => manifest,
15605 Err(MongrelError::Io(error))
15606 if created_table_ids.contains(&entry.table_id)
15607 && error.kind() == std::io::ErrorKind::NotFound =>
15608 {
15609 reconcile.push(entry.table_id);
15610 continue;
15611 }
15612 Err(error) => return Err(error),
15613 };
15614 if manifest.table_id != entry.table_id {
15615 return Err(MongrelError::Conflict(format!(
15616 "catalog table {} storage identity mismatch",
15617 entry.table_id
15618 )));
15619 }
15620 let schema_result = match durable_root {
15621 Some(root) => root
15622 .open_regular(relative_dir.join(crate::engine::SCHEMA_FILENAME))
15623 .map_err(MongrelError::from),
15624 None => crate::durable_file::open_regular_nofollow(
15625 &table_dir.join(crate::engine::SCHEMA_FILENAME),
15626 ),
15627 };
15628 let file = match schema_result {
15629 Ok(file) => file,
15630 Err(MongrelError::Io(error))
15631 if created_table_ids.contains(&entry.table_id)
15632 && error.kind() == std::io::ErrorKind::NotFound =>
15633 {
15634 reconcile.push(entry.table_id);
15635 continue;
15636 }
15637 Err(error) => return Err(error),
15638 };
15639 let length = file.metadata()?.len();
15640 if length > MAX_SCHEMA_BYTES {
15641 return Err(MongrelError::ResourceLimitExceeded {
15642 resource: "recovered schema bytes",
15643 requested: usize::try_from(length).unwrap_or(usize::MAX),
15644 limit: MAX_SCHEMA_BYTES as usize,
15645 });
15646 }
15647 let disk_schema: Schema = serde_json::from_reader(file.take(MAX_SCHEMA_BYTES + 1))
15648 .map_err(|error| MongrelError::Schema(format!("decode recovered schema: {error}")))?;
15649 if manifest.schema_id != entry.schema.schema_id
15650 || crate::wal::DdlOp::encode_schema(&disk_schema)?
15651 != crate::wal::DdlOp::encode_schema(&entry.schema)?
15652 {
15653 reconcile.push(entry.table_id);
15654 }
15655 }
15656 for table_id in ttl_updates.keys() {
15657 if !catalog.tables.iter().any(|entry| {
15658 entry.table_id == *table_id
15659 && matches!(entry.state, TableState::Live | TableState::Building { .. })
15660 }) {
15661 continue;
15662 }
15663 let relative_dir = Path::new(TABLES_DIR).join(table_id.to_string());
15664 let table_exists = match durable_root {
15665 Some(root) => match root.open_directory(&relative_dir) {
15666 Ok(_) => true,
15667 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
15668 Err(error) => return Err(error.into()),
15669 },
15670 None => root.join(&relative_dir).is_dir(),
15671 };
15672 if !table_exists && !created_table_ids.contains(table_id) {
15673 return Err(MongrelError::NotFound(format!(
15674 "TTL recovery table {table_id} storage is missing"
15675 )));
15676 }
15677 }
15678 reconcile.sort_unstable();
15679 reconcile.dedup();
15680 Ok(reconcile)
15681}
15682
15683fn validate_catalog_table_storage(
15684 root: &crate::durable_file::DurableRoot,
15685 catalog: &Catalog,
15686 meta_dek: Option<&[u8; META_DEK_LEN]>,
15687) -> Result<()> {
15688 for entry in &catalog.tables {
15689 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
15690 continue;
15691 }
15692 let table_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
15693 let manifest = crate::manifest::read_durable(root, &table_dir, meta_dek)?;
15694 if manifest.table_id != entry.table_id || manifest.schema_id != entry.schema.schema_id {
15695 return Err(MongrelError::Conflict(format!(
15696 "catalog table {} storage identity mismatch",
15697 entry.table_id
15698 )));
15699 }
15700 root.open_regular(table_dir.join(crate::engine::SCHEMA_FILENAME))?;
15701 }
15702 Ok(())
15703}
15704
15705fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> Result<bool> {
15706 match schema.columns.iter_mut().find(|c| c.id == column.id) {
15707 Some(existing) if *existing == column => Ok(false),
15708 Some(existing) => {
15709 *existing = column;
15710 schema.schema_id = schema
15711 .schema_id
15712 .checked_add(1)
15713 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
15714 Ok(true)
15715 }
15716 None => {
15717 schema.columns.push(column);
15718 schema.schema_id = schema
15719 .schema_id
15720 .checked_add(1)
15721 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
15722 Ok(true)
15723 }
15724 }
15725}
15726
15727fn permission_table(permission: &crate::auth::Permission) -> Option<&str> {
15728 use crate::auth::Permission;
15729 match permission {
15730 Permission::Select { table }
15731 | Permission::Insert { table }
15732 | Permission::Update { table }
15733 | Permission::Delete { table }
15734 | Permission::SelectColumns { table, .. }
15735 | Permission::InsertColumns { table, .. }
15736 | Permission::UpdateColumns { table, .. } => Some(table),
15737 Permission::All | Permission::Ddl | Permission::Admin => None,
15738 }
15739}
15740
15741fn apply_rebuilding_publish(
15742 catalog: &mut Catalog,
15743 table_id: u64,
15744 replaced_table_id: u64,
15745 new_name: &str,
15746 epoch: u64,
15747) -> Result<bool> {
15748 let already_published = catalog.tables.iter().any(|entry| {
15749 entry.table_id == table_id
15750 && entry.name == new_name
15751 && matches!(entry.state, TableState::Live)
15752 }) && catalog.tables.iter().any(|entry| {
15753 entry.table_id == replaced_table_id && matches!(entry.state, TableState::Dropped { .. })
15754 });
15755 if already_published {
15756 return Ok(false);
15757 }
15758 let schema = catalog
15759 .tables
15760 .iter()
15761 .find(|entry| entry.table_id == table_id)
15762 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?
15763 .schema
15764 .clone();
15765 let replaced = catalog
15766 .tables
15767 .iter_mut()
15768 .find(|entry| entry.table_id == replaced_table_id)
15769 .ok_or_else(|| MongrelError::NotFound(format!("table id {replaced_table_id} not found")))?;
15770 replaced.state = TableState::Dropped { at_epoch: epoch };
15771 let replacement = catalog
15772 .tables
15773 .iter_mut()
15774 .find(|entry| entry.table_id == table_id)
15775 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?;
15776 replacement.name = new_name.to_string();
15777 replacement.state = TableState::Live;
15778
15779 for role in &mut catalog.roles {
15780 role.permissions.retain_mut(|permission| {
15781 retain_rebuilt_permission_columns(permission, new_name, &schema)
15782 });
15783 }
15784 for definition in &mut catalog.materialized_views {
15785 if let Some(incremental) = definition.incremental.as_mut() {
15786 if incremental.source_table == new_name
15787 && incremental.source_table_id == replaced_table_id
15788 {
15789 incremental.source_table_id = table_id;
15790 }
15791 }
15792 }
15793 advance_security_version(catalog)?;
15794 Ok(true)
15795}
15796
15797fn retain_rebuilt_permission_columns(
15798 permission: &mut crate::auth::Permission,
15799 target_table: &str,
15800 schema: &Schema,
15801) -> bool {
15802 use crate::auth::Permission;
15803 let columns = match permission {
15804 Permission::SelectColumns { table, columns }
15805 | Permission::InsertColumns { table, columns }
15806 | Permission::UpdateColumns { table, columns }
15807 if table == target_table =>
15808 {
15809 Some(columns)
15810 }
15811 _ => None,
15812 };
15813 if let Some(columns) = columns {
15814 columns.retain(|column| schema.column(column).is_some());
15815 !columns.is_empty()
15816 } else {
15817 true
15818 }
15819}
15820
15821fn rename_permission_table(permission: &mut crate::auth::Permission, old: &str, new: &str) {
15822 use crate::auth::Permission;
15823 let table = match permission {
15824 Permission::Select { table }
15825 | Permission::Insert { table }
15826 | Permission::Update { table }
15827 | Permission::Delete { table }
15828 | Permission::SelectColumns { table, .. }
15829 | Permission::InsertColumns { table, .. }
15830 | Permission::UpdateColumns { table, .. } => Some(table),
15831 Permission::All | Permission::Ddl | Permission::Admin => None,
15832 };
15833 if let Some(table) = table.filter(|table| table.as_str() == old) {
15834 *table = new.to_string();
15835 }
15836}
15837
15838fn rename_permission_column(
15839 permission: &mut crate::auth::Permission,
15840 target_table: &str,
15841 old: &str,
15842 new: &str,
15843) {
15844 use crate::auth::Permission;
15845 let columns = match permission {
15846 Permission::SelectColumns { table, columns }
15847 | Permission::InsertColumns { table, columns }
15848 | Permission::UpdateColumns { table, columns }
15849 if table == target_table =>
15850 {
15851 Some(columns)
15852 }
15853 _ => None,
15854 };
15855 if let Some(column) = columns
15856 .into_iter()
15857 .flatten()
15858 .find(|column| column.as_str() == old)
15859 {
15860 *column = new.to_string();
15861 }
15862}
15863
15864fn merge_permission(
15865 permissions: &mut Vec<crate::auth::Permission>,
15866 permission: crate::auth::Permission,
15867) {
15868 use crate::auth::Permission;
15869 let (kind, table, mut columns) = match permission {
15870 Permission::SelectColumns { table, columns } => (0, table, columns),
15871 Permission::InsertColumns { table, columns } => (1, table, columns),
15872 Permission::UpdateColumns { table, columns } => (2, table, columns),
15873 permission if !permissions.contains(&permission) => {
15874 permissions.push(permission);
15875 return;
15876 }
15877 _ => return,
15878 };
15879 for permission in permissions.iter_mut() {
15880 let existing = match permission {
15881 Permission::SelectColumns {
15882 table: existing_table,
15883 columns,
15884 } if kind == 0 && existing_table == &table => Some(columns),
15885 Permission::InsertColumns {
15886 table: existing_table,
15887 columns,
15888 } if kind == 1 && existing_table == &table => Some(columns),
15889 Permission::UpdateColumns {
15890 table: existing_table,
15891 columns,
15892 } if kind == 2 && existing_table == &table => Some(columns),
15893 _ => None,
15894 };
15895 if let Some(existing) = existing {
15896 existing.append(&mut columns);
15897 existing.sort();
15898 existing.dedup();
15899 return;
15900 }
15901 }
15902 columns.sort();
15903 columns.dedup();
15904 let permission = if kind == 0 {
15905 Permission::SelectColumns { table, columns }
15906 } else if kind == 1 {
15907 Permission::InsertColumns { table, columns }
15908 } else {
15909 Permission::UpdateColumns { table, columns }
15910 };
15911 permissions.push(permission);
15912}
15913
15914fn revoke_permission_from(
15915 permissions: &mut Vec<crate::auth::Permission>,
15916 revoked: &crate::auth::Permission,
15917) {
15918 use crate::auth::Permission;
15919 let revoked_columns = match revoked {
15920 Permission::SelectColumns { table, columns } => Some((0, table, columns)),
15921 Permission::InsertColumns { table, columns } => Some((1, table, columns)),
15922 Permission::UpdateColumns { table, columns } => Some((2, table, columns)),
15923 _ => None,
15924 };
15925 let Some((kind, table, columns)) = revoked_columns else {
15926 permissions.retain(|permission| permission != revoked);
15927 return;
15928 };
15929 for permission in permissions.iter_mut() {
15930 let current = match permission {
15931 Permission::SelectColumns {
15932 table: current_table,
15933 columns,
15934 } if kind == 0 && current_table == table => Some(columns),
15935 Permission::InsertColumns {
15936 table: current_table,
15937 columns,
15938 } if kind == 1 && current_table == table => Some(columns),
15939 Permission::UpdateColumns {
15940 table: current_table,
15941 columns,
15942 } if kind == 2 && current_table == table => Some(columns),
15943 _ => None,
15944 };
15945 if let Some(current) = current {
15946 current.retain(|column| !columns.contains(column));
15947 }
15948 }
15949 permissions.retain(|permission| match permission {
15950 Permission::SelectColumns { columns, .. }
15951 | Permission::InsertColumns { columns, .. }
15952 | Permission::UpdateColumns { columns, .. } => !columns.is_empty(),
15953 _ => true,
15954 });
15955}
15956
15957fn validate_security_catalog(
15958 catalog: &Catalog,
15959 security: &crate::security::SecurityCatalog,
15960) -> Result<()> {
15961 let mut policy_names = HashSet::new();
15962 for table in &security.rls_tables {
15963 if catalog.live(table).is_none() {
15964 return Err(MongrelError::NotFound(format!(
15965 "RLS table {table:?} not found"
15966 )));
15967 }
15968 }
15969 for policy in &security.policies {
15970 if !policy_names.insert((policy.table.clone(), policy.name.clone())) {
15971 return Err(MongrelError::InvalidArgument(format!(
15972 "duplicate policy {:?} on {:?}",
15973 policy.name, policy.table
15974 )));
15975 }
15976 let schema = &catalog
15977 .live(&policy.table)
15978 .ok_or_else(|| {
15979 MongrelError::NotFound(format!("policy table {:?} not found", policy.table))
15980 })?
15981 .schema;
15982 if let Some(expression) = &policy.using {
15983 validate_security_expression(expression, schema)?;
15984 }
15985 if let Some(expression) = &policy.with_check {
15986 validate_security_expression(expression, schema)?;
15987 }
15988 }
15989 let mut mask_names = HashSet::new();
15990 for mask in &security.masks {
15991 if !mask_names.insert((mask.table.clone(), mask.name.clone())) {
15992 return Err(MongrelError::InvalidArgument(format!(
15993 "duplicate mask {:?} on {:?}",
15994 mask.name, mask.table
15995 )));
15996 }
15997 let column = catalog
15998 .live(&mask.table)
15999 .and_then(|entry| {
16000 entry
16001 .schema
16002 .columns
16003 .iter()
16004 .find(|column| column.id == mask.column)
16005 })
16006 .ok_or_else(|| {
16007 MongrelError::NotFound(format!(
16008 "mask column {} on {:?} not found",
16009 mask.column, mask.table
16010 ))
16011 })?;
16012 if matches!(
16013 mask.strategy,
16014 crate::security::MaskStrategy::Redact { .. } | crate::security::MaskStrategy::Sha256
16015 ) && !matches!(column.ty, TypeId::Bytes | TypeId::Enum { .. })
16016 {
16017 return Err(MongrelError::InvalidArgument(format!(
16018 "mask {:?} requires a string/bytes column",
16019 mask.name
16020 )));
16021 }
16022 }
16023 Ok(())
16024}
16025
16026fn validate_security_expression(
16027 expression: &crate::security::SecurityExpr,
16028 schema: &Schema,
16029) -> Result<()> {
16030 use crate::security::SecurityExpr;
16031 match expression {
16032 SecurityExpr::True => Ok(()),
16033 SecurityExpr::ColumnEqCurrentUser { column }
16034 | SecurityExpr::ColumnEqValue { column, .. } => {
16035 if schema
16036 .columns
16037 .iter()
16038 .any(|candidate| candidate.id == *column)
16039 {
16040 Ok(())
16041 } else {
16042 Err(MongrelError::InvalidArgument(format!(
16043 "security expression references unknown column id {column}"
16044 )))
16045 }
16046 }
16047 SecurityExpr::And { left, right } | SecurityExpr::Or { left, right } => {
16048 validate_security_expression(left, schema)?;
16049 validate_security_expression(right, schema)
16050 }
16051 SecurityExpr::Not { expression } => validate_security_expression(expression, schema),
16052 }
16053}
16054
16055fn sweep_unreferenced_table_dirs(root: &Path, cat: &Catalog) -> Result<()> {
16057 let referenced = cat
16058 .tables
16059 .iter()
16060 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
16061 .map(|entry| entry.table_id)
16062 .collect::<HashSet<_>>();
16063 let tables_dir = root.join(TABLES_DIR);
16064 let entries = match std::fs::read_dir(&tables_dir) {
16065 Ok(entries) => entries,
16066 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
16067 Err(error) => return Err(error.into()),
16068 };
16069 for entry in entries {
16070 let entry = entry?;
16071 if !entry.file_type()?.is_dir() {
16072 continue;
16073 }
16074 let file_name = entry.file_name();
16075 let Some(name) = file_name.to_str() else {
16076 continue;
16077 };
16078 let Ok(table_id) = name.parse::<u64>() else {
16079 continue;
16080 };
16081 if name != table_id.to_string() {
16082 continue;
16083 }
16084 if !referenced.contains(&table_id) {
16085 crate::durable_file::remove_directory_all(&entry.path())?;
16086 }
16087 }
16088 Ok(())
16089}
16090
16091fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
16096 for entry in &cat.tables {
16097 let txn_dir = root
16098 .join(TABLES_DIR)
16099 .join(entry.table_id.to_string())
16100 .join("_txn");
16101 if txn_dir.exists() {
16102 let _ = std::fs::remove_dir_all(&txn_dir);
16103 }
16104 }
16105}
16106
16107#[cfg(test)]
16108mod write_permission_tests {
16109 use super::*;
16110 use crate::txn::Staged;
16111
16112 struct NoopExternalBridge;
16113
16114 impl ExternalTriggerBridge for NoopExternalBridge {
16115 fn apply_trigger_external_write(
16116 &self,
16117 _entry: &ExternalTableEntry,
16118 base_state: Vec<u8>,
16119 _op: ExternalTriggerWrite,
16120 ) -> Result<ExternalTriggerWriteResult> {
16121 Ok(ExternalTriggerWriteResult::new(base_state))
16122 }
16123 }
16124
16125 fn assert_txn_namespace_full<T>(result: Result<T>) {
16126 assert!(matches!(result, Err(MongrelError::Full(_))));
16127 }
16128
16129 #[test]
16130 fn every_begin_api_preserves_transaction_id_exhaustion_without_wal_mutation() {
16131 let directory = tempfile::tempdir().unwrap();
16132 let database = Database::create(directory.path()).unwrap();
16133 let generation = (*database.next_txn_id.lock() >> 32).saturating_add(1);
16134 *database.next_txn_id.lock() = generation << 32;
16135 let before = crate::wal::SharedWal::replay(directory.path())
16136 .unwrap()
16137 .len();
16138 let bridge = NoopExternalBridge;
16139
16140 assert_txn_namespace_full(database.begin().commit());
16141 assert_txn_namespace_full(database.begin_as(None).commit_with_row_ids());
16142 assert_txn_namespace_full(
16143 database
16144 .begin_with_isolation(crate::txn::IsolationLevel::Serializable)
16145 .commit(),
16146 );
16147 assert_txn_namespace_full(
16148 database
16149 .begin_with_external_trigger_bridge(&bridge)
16150 .commit(),
16151 );
16152 assert_txn_namespace_full(
16153 database
16154 .begin_with_external_trigger_bridge_as(&bridge, None)
16155 .commit_controlled(&crate::ExecutionControl::new(None), || Ok(())),
16156 );
16157
16158 assert_eq!(
16159 crate::wal::SharedWal::replay(directory.path())
16160 .unwrap()
16161 .len(),
16162 before
16163 );
16164 drop(database);
16165 Database::open(directory.path()).unwrap();
16166 }
16167
16168 #[test]
16169 fn recovered_storage_identity_mismatch_does_not_mutate_directory() {
16170 let directory = tempfile::tempdir().unwrap();
16171 let table_dir = directory.path().join("7");
16172 crate::durable_file::create_directory_all(&table_dir).unwrap();
16173 let original_schema = test_schema();
16174 crate::engine::write_schema(&table_dir, &original_schema).unwrap();
16175 let mut manifest = crate::manifest::Manifest::new(8, original_schema.schema_id);
16176 crate::manifest::write_atomic(&table_dir, &mut manifest, None).unwrap();
16177 let schema_path = table_dir.join(crate::engine::SCHEMA_FILENAME);
16178 let original_bytes = std::fs::read(&schema_path).unwrap();
16179
16180 let mut replacement_schema = original_schema;
16181 replacement_schema.schema_id += 1;
16182 assert!(matches!(
16183 ensure_recovered_table_storage(None, None, &table_dir, 7, &replacement_schema, None,),
16184 Err(MongrelError::Conflict(_))
16185 ));
16186
16187 assert_eq!(std::fs::read(schema_path).unwrap(), original_bytes);
16188 assert!(!table_dir.join(crate::engine::WAL_DIR).exists());
16189 assert!(!table_dir.join(crate::engine::RUNS_DIR).exists());
16190 assert_eq!(crate::manifest::read(&table_dir, None).unwrap().table_id, 8);
16191 }
16192
16193 #[test]
16194 fn catalog_table_missing_storage_fails_without_recreating_it() {
16195 let directory = tempfile::tempdir().unwrap();
16196 let table_dir = {
16197 let database = Database::create(directory.path()).unwrap();
16198 database.create_table("docs", test_schema()).unwrap();
16199 directory
16200 .path()
16201 .join(TABLES_DIR)
16202 .join(database.table_id("docs").unwrap().to_string())
16203 };
16204 std::fs::remove_dir_all(&table_dir).unwrap();
16205
16206 assert!(matches!(
16207 Database::open(directory.path()),
16208 Err(MongrelError::NotFound(_))
16209 ));
16210 assert!(!table_dir.exists());
16211 }
16212
16213 #[test]
16214 fn authentication_and_principal_resolution_share_one_catalog_snapshot() {
16215 let directory = tempfile::tempdir().unwrap();
16216 let database = std::sync::Arc::new(
16217 Database::create_with_credentials(directory.path(), "admin", "admin-password").unwrap(),
16218 );
16219 database.create_user("alice", "old-password").unwrap();
16220 let old_identity = database.user_identity("alice").unwrap();
16221 let (verified_tx, verified_rx) = std::sync::mpsc::channel();
16222 let (resume_tx, resume_rx) = std::sync::mpsc::channel();
16223 let (mutation_started_tx, mutation_started_rx) = std::sync::mpsc::channel();
16224 let (mutation_done_tx, mutation_done_rx) = std::sync::mpsc::channel();
16225
16226 std::thread::scope(|scope| {
16227 let authenticate = {
16228 let database = std::sync::Arc::clone(&database);
16229 scope.spawn(move || {
16230 database.authenticate_principal_inner("alice", "old-password", || {
16231 verified_tx.send(()).unwrap();
16232 resume_rx.recv().unwrap();
16233 })
16234 })
16235 };
16236 verified_rx.recv().unwrap();
16237 let mutate = {
16238 let database = std::sync::Arc::clone(&database);
16239 scope.spawn(move || {
16240 mutation_started_tx.send(()).unwrap();
16241 database.drop_user("alice").unwrap();
16242 database.create_user("alice", "new-password").unwrap();
16243 mutation_done_tx.send(()).unwrap();
16244 })
16245 };
16246 mutation_started_rx.recv().unwrap();
16247 assert!(mutation_done_rx
16248 .recv_timeout(std::time::Duration::from_millis(50))
16249 .is_err());
16250 resume_tx.send(()).unwrap();
16251 let principal = authenticate.join().unwrap().unwrap().unwrap();
16252 assert_eq!((principal.user_id, principal.created_epoch), old_identity);
16253 mutate.join().unwrap();
16254 });
16255
16256 assert_ne!(database.user_identity("alice").unwrap(), old_identity);
16257 assert!(database
16258 .authenticate_principal("alice", "old-password")
16259 .unwrap()
16260 .is_none());
16261 assert!(database
16262 .authenticate_principal("alice", "new-password")
16263 .unwrap()
16264 .is_some());
16265 }
16266
16267 #[test]
16268 fn homogeneous_batch_summarizes_to_one_permission_decision() {
16269 let staging = (0..10_050)
16270 .map(|_| {
16271 (
16272 7,
16273 Staged::Put(vec![(2, Value::Int64(2)), (1, Value::Int64(1))]),
16274 )
16275 })
16276 .collect::<Vec<_>>();
16277
16278 let needs = summarize_write_permissions(&staging);
16279 let table = needs.get(&7).unwrap();
16280 assert_eq!(needs.len(), 1);
16281 assert!(table.insert);
16282 assert_eq!(table.insert_columns, [1, 2]);
16283 assert!(!table.update);
16284 assert!(!table.delete);
16285 assert!(!table.truncate);
16286 }
16287
16288 #[test]
16289 fn mixed_writes_union_columns_and_preserve_empty_operations() {
16290 let staging = vec![
16291 (7, Staged::Put(vec![(2, Value::Int64(2))])),
16292 (7, Staged::Put(vec![(1, Value::Int64(1))])),
16293 (
16294 7,
16295 Staged::Update {
16296 row_id: RowId(1),
16297 new_row: vec![(1, Value::Int64(1)), (2, Value::Int64(2))],
16298 changed_columns: vec![2],
16299 },
16300 ),
16301 (7, Staged::Delete(RowId(2))),
16302 (8, Staged::Truncate),
16303 ];
16304
16305 let needs = summarize_write_permissions(&staging);
16306 let table = needs.get(&7).unwrap();
16307 assert_eq!(table.insert_columns, [1, 2]);
16308 assert!(table.update);
16309 assert_eq!(table.update_columns, [2]);
16310 assert!(table.delete);
16311 assert!(needs.get(&8).unwrap().truncate);
16312 }
16313
16314 #[test]
16315 fn final_permission_decisions_do_not_scale_with_rows() {
16316 let credentialless_dir = tempfile::tempdir().unwrap();
16317 let credentialless = Database::create(credentialless_dir.path()).unwrap();
16318 credentialless.create_table("docs", test_schema()).unwrap();
16319 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16320 credentialless
16321 .validate_write_permissions(&puts(credentialless.table_id("docs").unwrap()), None, None)
16322 .unwrap();
16323 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 0));
16324
16325 let authenticated_dir = tempfile::tempdir().unwrap();
16326 let authenticated =
16327 Database::create_with_credentials(authenticated_dir.path(), "admin", "admin-password")
16328 .unwrap();
16329 authenticated.create_table("docs", test_schema()).unwrap();
16330 let admin = authenticated.resolve_principal("admin").unwrap();
16331 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16332 authenticated
16333 .validate_write_permissions(
16334 &puts(authenticated.table_id("docs").unwrap()),
16335 Some(&admin),
16336 None,
16337 )
16338 .unwrap();
16339 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
16340 }
16341
16342 #[test]
16343 fn delete_batch_checks_permission_once_when_staged_and_once_when_committed() {
16344 let dir = tempfile::tempdir().unwrap();
16345 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
16346 db.create_table("docs", test_schema()).unwrap();
16347 let admin = db.resolve_principal("admin").unwrap();
16348 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16349
16350 let mut transaction = db.begin_as(Some(admin));
16351 transaction
16352 .delete_batch("docs", (0..100).map(RowId).collect())
16353 .unwrap();
16354 transaction.commit().unwrap();
16355
16356 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 2));
16357 }
16358
16359 #[test]
16360 fn truncate_validation_checks_admin_once_for_all_tables() {
16361 let dir = tempfile::tempdir().unwrap();
16362 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
16363 db.create_table("first", test_schema()).unwrap();
16364 db.create_table("second", test_schema()).unwrap();
16365 let admin = db.resolve_principal("admin").unwrap();
16366 let staging = vec![
16367 (db.table_id("first").unwrap(), Staged::Truncate),
16368 (db.table_id("second").unwrap(), Staged::Truncate),
16369 ];
16370
16371 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16372 db.validate_write_permissions(&staging, Some(&admin), None)
16373 .unwrap();
16374 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
16375 }
16376
16377 #[test]
16378 fn one_table_commit_batches_structural_work() {
16379 let dir = tempfile::tempdir().unwrap();
16380 let db = Database::create(dir.path()).unwrap();
16381 db.create_table("docs", test_schema()).unwrap();
16382 let table_id = db.table_id("docs").unwrap();
16383
16384 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(0));
16385 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
16386 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
16387 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
16388 db.transaction(|transaction| {
16389 for id in 0..100 {
16390 transaction.put("docs", vec![(1, Value::Int64(id))])?;
16391 }
16392 Ok(())
16393 })
16394 .unwrap();
16395
16396 AUTO_INCREMENT_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 2));
16397 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16398 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16399 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
16400
16401 let puts = crate::wal::SharedWal::replay(dir.path())
16402 .unwrap()
16403 .into_iter()
16404 .filter_map(|record| match record.op {
16405 crate::wal::Op::Put { table_id: id, rows } if id == table_id => Some(
16406 bincode::deserialize::<Vec<crate::memtable::Row>>(&rows)
16407 .unwrap()
16408 .len(),
16409 ),
16410 _ => None,
16411 })
16412 .collect::<Vec<_>>();
16413 assert_eq!(puts, [100]);
16414
16415 let row_ids = db
16416 .table("docs")
16417 .unwrap()
16418 .lock()
16419 .visible_rows(db.snapshot().0)
16420 .unwrap()
16421 .into_iter()
16422 .take(2)
16423 .map(|row| row.row_id)
16424 .collect::<Vec<_>>();
16425 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
16426 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
16427 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
16428 db.transaction(|transaction| {
16429 for row_id in row_ids {
16430 transaction.delete("docs", row_id)?;
16431 }
16432 Ok(())
16433 })
16434 .unwrap();
16435 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16436 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16437 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
16438
16439 let deletes = crate::wal::SharedWal::replay(dir.path())
16440 .unwrap()
16441 .into_iter()
16442 .filter_map(|record| match record.op {
16443 crate::wal::Op::Delete {
16444 table_id: id,
16445 row_ids,
16446 } if id == table_id => Some(row_ids.len()),
16447 _ => None,
16448 })
16449 .collect::<Vec<_>>();
16450 assert_eq!(deletes, [2]);
16451 }
16452
16453 fn puts(table_id: u64) -> Vec<(u64, Staged)> {
16454 (0..10_050)
16455 .map(|id| (table_id, Staged::Put(vec![(1, Value::Int64(id))])))
16456 .collect()
16457 }
16458
16459 fn test_schema() -> Schema {
16460 Schema {
16461 columns: vec![ColumnDef {
16462 id: 1,
16463 name: "id".into(),
16464 ty: TypeId::Int64,
16465 flags: crate::schema::ColumnFlags::empty()
16466 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
16467 default_value: None,
16468 }],
16469 ..Schema::default()
16470 }
16471 }
16472}
16473
16474#[cfg(test)]
16475mod cdc_bounds_tests {
16476 use super::*;
16477
16478 #[test]
16479 fn retained_byte_limit_rejects_without_allocating_payload() {
16480 let mut retained = 0;
16481 let error = charge_cdc_bytes(
16482 &mut retained,
16483 CDC_MAX_RETAINED_BYTES.saturating_add(1),
16484 "CDC retained bytes",
16485 )
16486 .unwrap_err();
16487 assert!(matches!(
16488 error,
16489 MongrelError::ResourceLimitExceeded {
16490 resource: "CDC retained bytes",
16491 ..
16492 }
16493 ));
16494 }
16495
16496 #[test]
16497 fn row_json_estimate_accounts_for_byte_array_expansion() {
16498 let row = crate::memtable::Row::new(RowId(1), Epoch(1))
16499 .with_column(1, Value::Bytes(vec![0; 1024]));
16500 assert!(cdc_row_json_bytes(&row) >= 1024 * std::mem::size_of::<serde_json::Value>());
16501 }
16502}
16503
16504#[cfg(test)]
16505mod generation_metrics_tests {
16506 use super::*;
16507 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
16508
16509 #[test]
16510 fn legacy_cow_fallback_is_measured() {
16511 let dir = tempfile::tempdir().unwrap();
16512 let table = Table::create(
16513 dir.path(),
16514 Schema {
16515 columns: vec![ColumnDef {
16516 id: 1,
16517 name: "id".into(),
16518 ty: TypeId::Int64,
16519 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
16520 default_value: None,
16521 }],
16522 ..Schema::default()
16523 },
16524 1,
16525 )
16526 .unwrap();
16527 let handle = TableHandle::from_table(table);
16528 let held = match &handle.inner {
16529 TableHandleInner::CopyOnWrite(slot) => Arc::clone(&slot.read()),
16530 TableHandleInner::Direct(_) => unreachable!(),
16531 };
16532
16533 handle.lock().set_sync_byte_threshold(1);
16534
16535 let stats = handle.generation_stats();
16536 assert_eq!(stats.cow_clone_count, 1);
16537 assert!(stats.estimated_cow_clone_bytes > 0);
16538 drop(held);
16539 }
16540}
16541
16542#[cfg(test)]
16543mod trigger_engine_tests {
16544 use super::*;
16545
16546 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
16547 WriteEvent {
16548 table: "test".into(),
16549 kind: TriggerEvent::Insert,
16550 new: Some(TriggerRowImage {
16551 columns: new_cells.iter().cloned().collect(),
16552 }),
16553 old: Some(TriggerRowImage {
16554 columns: old_cells.iter().cloned().collect(),
16555 }),
16556 changed_columns: Vec::new(),
16557 op_indices: Vec::new(),
16558 put_idx: None,
16559 trigger_stack: Vec::new(),
16560 }
16561 }
16562
16563 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
16564 WriteEvent {
16565 table: "test".into(),
16566 kind: TriggerEvent::Insert,
16567 new: Some(TriggerRowImage {
16568 columns: new_cells.iter().cloned().collect(),
16569 }),
16570 old: None,
16571 changed_columns: Vec::new(),
16572 op_indices: Vec::new(),
16573 put_idx: None,
16574 trigger_stack: Vec::new(),
16575 }
16576 }
16577
16578 #[test]
16579 fn value_order_int64_vs_float64() {
16580 assert_eq!(
16581 value_order(&Value::Int64(5), &Value::Float64(5.0)),
16582 Some(std::cmp::Ordering::Equal)
16583 );
16584 assert_eq!(
16585 value_order(&Value::Int64(5), &Value::Float64(3.0)),
16586 Some(std::cmp::Ordering::Greater)
16587 );
16588 assert_eq!(
16589 value_order(&Value::Int64(2), &Value::Float64(3.0)),
16590 Some(std::cmp::Ordering::Less)
16591 );
16592 }
16593
16594 #[test]
16595 fn value_order_null_returns_none() {
16596 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
16597 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
16598 assert_eq!(value_order(&Value::Null, &Value::Null), None);
16599 }
16600
16601 #[test]
16602 fn value_order_cross_group_returns_none() {
16603 assert_eq!(
16604 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
16605 None
16606 );
16607 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
16608 assert_eq!(
16609 value_order(
16610 &Value::Embedding(vec![1.0, 2.0]),
16611 &Value::Embedding(vec![1.0, 2.0])
16612 ),
16613 None
16614 );
16615 }
16616
16617 #[test]
16618 fn eval_trigger_expr_ranges_and_booleans() {
16619 let expr = TriggerExpr::And {
16620 left: Box::new(TriggerExpr::Gt {
16621 left: TriggerValue::NewColumn(1),
16622 right: TriggerValue::Literal(Value::Int64(0)),
16623 }),
16624 right: Box::new(TriggerExpr::Lte {
16625 left: TriggerValue::NewColumn(1),
16626 right: TriggerValue::Literal(Value::Int64(100)),
16627 }),
16628 };
16629 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
16630 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
16631 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
16632
16633 let or_expr = TriggerExpr::Or {
16634 left: Box::new(TriggerExpr::Lt {
16635 left: TriggerValue::NewColumn(1),
16636 right: TriggerValue::Literal(Value::Int64(0)),
16637 }),
16638 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
16639 TriggerValue::OldColumn(2),
16640 )))),
16641 };
16642 assert!(eval_trigger_expr(
16643 &or_expr,
16644 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
16645 )
16646 .unwrap());
16647 assert!(!eval_trigger_expr(
16648 &or_expr,
16649 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
16650 )
16651 .unwrap());
16652
16653 assert!(eval_trigger_expr(
16654 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
16655 &event_insert(&[])
16656 )
16657 .unwrap());
16658 assert!(!eval_trigger_expr(
16659 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
16660 &event_insert(&[])
16661 )
16662 .unwrap());
16663 assert!(!eval_trigger_expr(
16664 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
16665 &event_insert(&[])
16666 )
16667 .unwrap());
16668 }
16669}