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_locked_paths() -> &'static Mutex<HashSet<PathBuf>> {
56 static LOCKED_PATHS: std::sync::OnceLock<Mutex<HashSet<PathBuf>>> = std::sync::OnceLock::new();
57 LOCKED_PATHS.get_or_init(|| Mutex::new(HashSet::new()))
58}
59
60struct DatabaseFileLock {
61 bootstrap_file: std::fs::File,
62 legacy_file: Option<std::fs::File>,
63 canonical_path: PathBuf,
64 durable_root: Option<Arc<crate::durable_file::DurableRoot>>,
65}
66
67impl Drop for DatabaseFileLock {
68 fn drop(&mut self) {
69 if let Some(file) = &self.legacy_file {
70 let _ = fs2::FileExt::unlock(file);
71 }
72 let _ = fs2::FileExt::unlock(&self.bootstrap_file);
73 process_locked_paths().lock().remove(&self.canonical_path);
74 }
75}
76
77fn commit_prepare_checkpoint(
78 control: Option<&crate::ExecutionControl>,
79 index: usize,
80) -> Result<()> {
81 if index.is_multiple_of(256) {
82 if let Some(control) = control {
83 control.checkpoint()?;
84 }
85 }
86 Ok(())
87}
88
89fn finish_controlled_commit_attempt(
90 result: Result<Epoch>,
91 after_commit: &mut Option<&mut dyn FnMut(Option<Epoch>) -> Result<()>>,
92) -> Result<Epoch> {
93 let Some(after_commit) = after_commit.as_mut() else {
94 return result;
95 };
96 match result {
97 Ok(epoch) => match (**after_commit)(Some(epoch)) {
98 Ok(()) => Ok(epoch),
99 Err(error) => Err(MongrelError::DurableCommit {
100 epoch: epoch.0,
101 message: error.to_string(),
102 }),
103 },
104 Err(MongrelError::DurableCommit { epoch, message }) => {
105 let callback_error = (**after_commit)(Some(Epoch(epoch))).err();
106 Err(MongrelError::DurableCommit {
107 epoch,
108 message: callback_error
109 .map(|error| format!("{message}; commit callback: {error}"))
110 .unwrap_or(message),
111 })
112 }
113 Err(error) => match (**after_commit)(None) {
114 Ok(()) => Err(error),
115 Err(callback_error) => Err(MongrelError::Other(format!(
116 "{error}; commit callback: {callback_error}"
117 ))),
118 },
119 }
120}
121
122fn current_unix_nanos() -> u64 {
123 std::time::SystemTime::now()
124 .duration_since(std::time::UNIX_EPOCH)
125 .unwrap_or_default()
126 .as_nanos() as u64
127}
128
129#[cfg(feature = "encryption")]
130fn read_encryption_salt(
131 root: &crate::durable_file::DurableRoot,
132) -> Result<[u8; crate::encryption::SALT_LEN]> {
133 let mut file = root
134 .open_regular(Path::new(META_DIR).join(KEYS_FILENAME))
135 .map_err(|error| MongrelError::NotFound(format!("encryption salt file: {error}")))?;
136 let length = file.metadata()?.len();
137 if length != crate::encryption::SALT_LEN as u64 {
138 return Err(MongrelError::Encryption(format!(
139 "invalid encryption salt length: got {length}, expected {}",
140 crate::encryption::SALT_LEN
141 )));
142 }
143 let mut salt = [0_u8; crate::encryption::SALT_LEN];
144 file.read_exact(&mut salt)?;
145 Ok(salt)
146}
147
148fn incremental_aggregate_cache_key(
149 table: &str,
150 conditions: &[crate::query::Condition],
151 column: Option<u16>,
152 agg: crate::engine::NativeAgg,
153 principal: Option<&crate::auth::Principal>,
154 security_version: u64,
155) -> u64 {
156 use std::hash::{Hash, Hasher};
157 let projection = column.as_ref().map(std::slice::from_ref);
158 let query_key = crate::query::canonical_query_key(conditions, projection, security_version);
159 let mut hasher = std::collections::hash_map::DefaultHasher::new();
160 table.hash(&mut hasher);
161 query_key.hash(&mut hasher);
162 match agg {
163 crate::engine::NativeAgg::Count => 0u8,
164 crate::engine::NativeAgg::Sum => 1,
165 crate::engine::NativeAgg::Min => 2,
166 crate::engine::NativeAgg::Max => 3,
167 crate::engine::NativeAgg::Avg => 4,
168 }
169 .hash(&mut hasher);
170 if let Some(principal) = principal {
171 principal.user_id.hash(&mut hasher);
172 principal.created_epoch.hash(&mut hasher);
173 principal.username.hash(&mut hasher);
174 principal.is_admin.hash(&mut hasher);
175 let mut roles = principal.roles.clone();
176 roles.sort_unstable();
177 roles.hash(&mut hasher);
178 }
179 hasher.finish()
180}
181
182fn read_history_retention(
183 root: &crate::durable_file::DurableRoot,
184 current_epoch: Epoch,
185) -> Result<(u64, Epoch)> {
186 const MAX_HISTORY_RETENTION_BYTES: u64 = 128;
187 let file = match root.open_regular(Path::new(META_DIR).join(HISTORY_RETENTION_FILENAME)) {
188 Ok(file) => file,
189 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
190 return Ok((0, current_epoch));
191 }
192 Err(error) => return Err(error.into()),
193 };
194 let length = file.metadata()?.len();
195 if length > MAX_HISTORY_RETENTION_BYTES {
196 return Err(MongrelError::ResourceLimitExceeded {
197 resource: "history retention bytes",
198 requested: usize::try_from(length).unwrap_or(usize::MAX),
199 limit: MAX_HISTORY_RETENTION_BYTES as usize,
200 });
201 }
202 let mut bytes = Vec::with_capacity(length as usize);
203 file.take(MAX_HISTORY_RETENTION_BYTES + 1)
204 .read_to_end(&mut bytes)?;
205 if bytes.len() as u64 != length {
206 return Err(MongrelError::Other(
207 "history retention length changed while reading".into(),
208 ));
209 }
210 let text = std::str::from_utf8(&bytes)
211 .map_err(|error| MongrelError::Other(format!("history retention encoding: {error}")))?;
212 let mut fields = text.split_whitespace();
213 let epochs = fields
214 .next()
215 .ok_or_else(|| MongrelError::Other("history retention file is empty".into()))?
216 .parse::<u64>()
217 .map_err(|error| MongrelError::Other(format!("history retention epochs: {error}")))?;
218 let start = fields
219 .next()
220 .ok_or_else(|| MongrelError::Other("history retention start is missing".into()))?
221 .parse::<u64>()
222 .map_err(|error| MongrelError::Other(format!("history retention start: {error}")))?;
223 if fields.next().is_some() || start > current_epoch.0 {
224 return Err(MongrelError::Other(
225 "history retention file has trailing fields or a future start epoch".into(),
226 ));
227 }
228 Ok((epochs, Epoch(start)))
229}
230
231fn write_history_retention<F>(
232 root: &Path,
233 epochs: u64,
234 start: Epoch,
235 after_publish: F,
236) -> Result<()>
237where
238 F: FnOnce(),
239{
240 let meta = root.join(META_DIR);
241 let path = meta.join(HISTORY_RETENTION_FILENAME);
242 let bytes = format!("{epochs} {}\n", start.0);
243 crate::durable_file::write_atomic_with_after(&path, bytes.as_bytes(), after_publish)?;
244 Ok(())
245}
246
247struct PreparedBackupDestination {
248 parent: crate::durable_file::DurableRoot,
249 destination_name: std::ffi::OsString,
250 destination_path: PathBuf,
251 stage_name: std::ffi::OsString,
252 stage: Option<Box<crate::durable_file::DurableRoot>>,
253}
254
255fn prepare_backup_destination(
256 source: &Path,
257 destination: &Path,
258) -> Result<PreparedBackupDestination> {
259 let destination_name = destination
260 .file_name()
261 .ok_or_else(|| MongrelError::InvalidArgument("invalid backup destination".into()))?
262 .to_os_string();
263 let requested_parent = destination
264 .parent()
265 .filter(|path| !path.as_os_str().is_empty())
266 .unwrap_or_else(|| Path::new("."));
267 crate::durable_file::create_directory_all(requested_parent)?;
268 let parent = crate::durable_file::DurableRoot::open(requested_parent)?;
269 prepare_backup_destination_in(source, &parent, &destination_name)
270}
271
272fn prepare_backup_destination_in(
273 source: &Path,
274 parent: &crate::durable_file::DurableRoot,
275 destination_name: &std::ffi::OsStr,
276) -> Result<PreparedBackupDestination> {
277 let source = source.canonicalize()?;
278 if parent.canonical_path().starts_with(&source) {
279 return Err(MongrelError::InvalidArgument(
280 "backup destination must not be inside the source database".into(),
281 ));
282 }
283 if parent.entry_exists(Path::new(&destination_name))? {
284 return Err(MongrelError::Conflict(format!(
285 "backup destination already exists: {}",
286 parent.canonical_path().join(destination_name).display()
287 )));
288 }
289 let mut stage_name = None;
290 for _ in 0..128 {
291 let mut nonce = [0_u8; 8];
292 crate::encryption::fill_random(&mut nonce)?;
293 let suffix = nonce
294 .iter()
295 .map(|byte| format!("{byte:02x}"))
296 .collect::<String>();
297 let name = std::ffi::OsString::from(format!(
298 ".{}.backup-stage-{}-{suffix}",
299 destination_name.to_string_lossy(),
300 std::process::id()
301 ));
302 match parent.create_directory_new(Path::new(&name)) {
303 Ok(()) => {
304 stage_name = Some(name);
305 break;
306 }
307 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
308 Err(error) => return Err(error.into()),
309 }
310 }
311 let stage_name = stage_name
312 .ok_or_else(|| MongrelError::Conflict("could not allocate backup staging path".into()))?;
313 let stage = parent.open_directory(Path::new(&stage_name))?;
314 Ok(PreparedBackupDestination {
315 destination_path: parent.canonical_path().join(destination_name),
316 destination_name: destination_name.to_os_string(),
317 stage_name,
318 stage: Some(Box::new(stage)),
319 parent: parent.try_clone()?,
320 })
321}
322
323fn copy_backup_boundary(
324 source_root: &Path,
325 destination_root: &crate::durable_file::DurableRoot,
326 deferred_runs: &HashSet<PathBuf>,
327 copied: &mut Vec<PathBuf>,
328 control: Option<&crate::ExecutionControl>,
329) -> Result<()> {
330 let mut visited = 0;
331 crate::durable_file::walk_regular_files_nofollow(
332 source_root,
333 |relative, is_directory| {
334 if visited % 256 == 0 {
335 if let Some(control) = control {
336 control.checkpoint()?;
337 }
338 }
339 visited += 1;
340 if backup_path_excluded(relative) {
341 return Ok(false);
342 }
343 if is_directory {
344 return Ok(true);
345 }
346 if deferred_runs.contains(relative) {
347 return Ok(false);
348 }
349 Ok(!(relative
350 .parent()
351 .and_then(Path::file_name)
352 .is_some_and(|parent| parent == "_runs")
353 && relative
354 .extension()
355 .is_some_and(|extension| extension == "sr")))
356 },
357 |relative| {
358 destination_root.create_directory_all(relative)?;
359 Ok(())
360 },
361 |relative, source| {
362 destination_root.copy_new_from(relative, source)?;
363 copied.push(relative.to_path_buf());
364 Ok(())
365 },
366 )
367}
368
369fn backup_path_excluded(relative: &Path) -> bool {
370 relative == Path::new("_meta/.lock")
371 || relative == Path::new("_meta/replica")
372 || relative == Path::new("_meta/repl_epoch")
373 || relative == Path::new(crate::backup::BACKUP_MANIFEST_PATH)
374 || relative.components().any(|component| {
375 matches!(component, std::path::Component::Normal(name) if name == "_cache" || name == "_txn" || name == "backup-pins")
376 })
377}
378
379#[derive(Debug, Clone)]
380pub enum ExternalTriggerWrite {
381 Insert {
382 table: String,
383 cells: Vec<(u16, Value)>,
384 },
385 UpdateByPk {
386 table: String,
387 pk: Value,
388 cells: Vec<(u16, Value)>,
389 },
390 DeleteByPk {
391 table: String,
392 pk: Value,
393 },
394}
395
396impl ExternalTriggerWrite {
397 fn table(&self) -> &str {
398 match self {
399 Self::Insert { table, .. }
400 | Self::UpdateByPk { table, .. }
401 | Self::DeleteByPk { table, .. } => table,
402 }
403 }
404}
405
406#[derive(Debug, Clone, PartialEq)]
407pub enum ExternalTriggerBaseWrite {
408 Put {
409 table: String,
410 cells: Vec<(u16, Value)>,
411 },
412 Delete {
413 table: String,
414 row_id: RowId,
415 },
416}
417
418#[derive(Debug, Clone, PartialEq)]
419pub struct ExternalTriggerWriteResult {
420 pub state: Vec<u8>,
421 pub base_writes: Vec<ExternalTriggerBaseWrite>,
422}
423
424impl ExternalTriggerWriteResult {
425 pub fn new(state: Vec<u8>) -> Self {
426 Self {
427 state,
428 base_writes: Vec::new(),
429 }
430 }
431}
432
433pub trait ExternalTriggerBridge: Send + Sync {
434 fn apply_trigger_external_write(
435 &self,
436 entry: &ExternalTableEntry,
437 base_state: Vec<u8>,
438 op: ExternalTriggerWrite,
439 ) -> Result<ExternalTriggerWriteResult>;
440}
441
442struct SpilledRun {
444 table_id: u64,
445 run_id: u128,
446 pending_path: PathBuf,
447 final_path: PathBuf,
448 rows: Vec<crate::memtable::Row>,
449 row_count: u64,
450 min_rid: u64,
451 max_rid: u64,
452 content_hash: [u8; 32],
453}
454
455const SPILLED_WAL_PAYLOAD_MAX_BYTES: usize = 24 * 1024 * 1024;
456const SPILLED_WAL_TOTAL_MAX_BYTES: usize = 256 * 1024 * 1024;
457
458fn encode_spilled_row_chunks(
459 rows: &[crate::memtable::Row],
460 total_bytes: &mut usize,
461 total_limit: usize,
462 control: Option<&crate::ExecutionControl>,
463) -> Result<Vec<Vec<u8>>> {
464 let mut output = Vec::new();
465 let mut start = 0;
466 while start < rows.len() {
467 let mut estimated_bytes = std::mem::size_of::<u64>();
471 let mut end = start;
472 while end < rows.len() {
473 if end % 256 == 0 {
474 if let Some(control) = control {
475 control.checkpoint()?;
476 }
477 }
478 let row_bytes =
479 usize::try_from(bincode::serialized_size(&rows[end])?).map_err(|_| {
480 MongrelError::ResourceLimitExceeded {
481 resource: "spilled WAL row bytes",
482 requested: usize::MAX,
483 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
484 }
485 })?;
486 let next_bytes = estimated_bytes.checked_add(row_bytes).ok_or(
487 MongrelError::ResourceLimitExceeded {
488 resource: "spilled WAL row bytes",
489 requested: usize::MAX,
490 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
491 },
492 )?;
493 if next_bytes > SPILLED_WAL_PAYLOAD_MAX_BYTES {
494 break;
495 }
496 estimated_bytes = next_bytes;
497 end += 1;
498 }
499 if end == start {
500 return Err(MongrelError::ResourceLimitExceeded {
501 resource: "spilled WAL row bytes",
502 requested: estimated_bytes.saturating_add(1),
503 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
504 });
505 }
506 let payload = bincode::serialize(&rows[start..end])?;
507 if payload.len() > SPILLED_WAL_PAYLOAD_MAX_BYTES {
508 return Err(MongrelError::ResourceLimitExceeded {
509 resource: "spilled WAL row bytes",
510 requested: payload.len(),
511 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
512 });
513 }
514 let requested = total_bytes.checked_add(payload.len()).unwrap_or(usize::MAX);
515 if requested > total_limit {
516 return Err(MongrelError::ResourceLimitExceeded {
517 resource: "spilled WAL transaction bytes",
518 requested,
519 limit: total_limit,
520 });
521 }
522 *total_bytes = requested;
523 output.push(payload);
524 start = end;
525 }
526 Ok(output)
527}
528
529#[cfg(test)]
530mod spilled_wal_encoding_tests {
531 use super::*;
532
533 #[test]
534 fn logical_spill_payload_has_a_total_bound() {
535 let rows = (0..4)
536 .map(|row_id| crate::memtable::Row {
537 row_id: crate::rowid::RowId(row_id),
538 committed_epoch: Epoch::ZERO,
539 columns: [(1, Value::Bytes(vec![0; 64]))].into_iter().collect(),
540 deleted: false,
541 })
542 .collect::<Vec<_>>();
543 let mut total = 0;
544 let error = encode_spilled_row_chunks(&rows, &mut total, 32, None).unwrap_err();
545 assert!(matches!(
546 error,
547 MongrelError::ResourceLimitExceeded {
548 resource: "spilled WAL transaction bytes",
549 ..
550 }
551 ));
552 }
553}
554
555struct PreparedRunLinks {
560 links: Vec<(PathBuf, PathBuf)>,
561 armed: bool,
562}
563
564impl PreparedRunLinks {
565 fn prepare(spilled: &[SpilledRun]) -> Result<Self> {
566 let mut guard = Self {
567 links: Vec::with_capacity(spilled.len()),
568 armed: true,
569 };
570 for run in spilled {
571 crate::durable_file::rename(&run.pending_path, &run.final_path)?;
572 guard
573 .links
574 .push((run.pending_path.clone(), run.final_path.clone()));
575 }
576 Ok(guard)
577 }
578
579 fn disarm(&mut self) {
580 self.armed = false;
581 for (pending, _) in &self.links {
582 if let Some(parent) = pending.parent() {
583 let _ = std::fs::remove_dir_all(parent);
584 }
585 }
586 }
587}
588
589impl Drop for PreparedRunLinks {
590 fn drop(&mut self) {
591 if !self.armed {
592 return;
593 }
594 for (pending, final_path) in self.links.iter().rev() {
595 let _ = std::fs::rename(final_path, pending);
596 }
597 }
598}
599
600struct TableApplyBatch {
601 table_id: u64,
602 handle: TableHandle,
603 ops: Vec<crate::txn::StagedOp>,
604}
605
606#[derive(Debug, Clone)]
607struct TriggerRowImage {
608 columns: HashMap<u16, Value>,
609}
610
611impl TriggerRowImage {
612 fn from_row(row: crate::memtable::Row) -> Self {
613 Self {
614 columns: row.columns,
615 }
616 }
617
618 fn from_cells(cells: &[(u16, Value)]) -> Self {
619 Self {
620 columns: cells.iter().cloned().collect(),
621 }
622 }
623}
624
625#[derive(Debug, Clone)]
626struct WriteEvent {
627 table: String,
628 kind: TriggerEvent,
629 old: Option<TriggerRowImage>,
630 new: Option<TriggerRowImage>,
631 changed_columns: Vec<u16>,
632 op_indices: Vec<usize>,
633 put_idx: Option<usize>,
634 trigger_stack: Vec<String>,
635}
636
637#[derive(Default)]
638struct TriggerExpansion {
639 before: Vec<(u64, crate::txn::Staged)>,
640 before_stacks: Vec<Vec<String>>,
641 before_external: Vec<ExternalTriggerWrite>,
642 after: Vec<(u64, crate::txn::Staged)>,
643 after_stacks: Vec<Vec<String>>,
644 after_external: Vec<ExternalTriggerWrite>,
645 ignored_indices: std::collections::BTreeSet<usize>,
646}
647
648#[derive(Clone, PartialEq)]
649struct TriggerCatalogBinding {
650 triggers: Vec<TriggerEntry>,
651 tables: Vec<(String, u64, u64)>,
652 external_tables: Vec<(String, u64, u64)>,
653}
654
655fn trigger_catalog_binding(catalog: &Catalog) -> Option<TriggerCatalogBinding> {
656 let mut triggers = catalog
657 .triggers
658 .iter()
659 .filter(|entry| entry.trigger.enabled)
660 .cloned()
661 .collect::<Vec<_>>();
662 if triggers.is_empty() {
663 return None;
664 }
665 triggers.sort_by(|left, right| left.trigger.name.cmp(&right.trigger.name));
666 let mut tables = catalog
667 .tables
668 .iter()
669 .filter(|entry| matches!(entry.state, TableState::Live))
670 .map(|entry| (entry.name.clone(), entry.table_id, entry.schema.schema_id))
671 .collect::<Vec<_>>();
672 tables.sort_unstable();
673 let mut external_tables = catalog
674 .external_tables
675 .iter()
676 .map(|entry| {
677 (
678 entry.name.clone(),
679 entry.created_epoch,
680 entry.declared_schema.schema_id,
681 )
682 })
683 .collect::<Vec<_>>();
684 external_tables.sort_unstable();
685 Some(TriggerCatalogBinding {
686 triggers,
687 tables,
688 external_tables,
689 })
690}
691
692struct TriggerProgramOutput<'a> {
693 added: &'a mut Vec<(u64, crate::txn::Staged)>,
694 added_stacks: &'a mut Vec<Vec<String>>,
695 added_external: &'a mut Vec<ExternalTriggerWrite>,
696 ignored_indices: &'a mut std::collections::BTreeSet<usize>,
697}
698
699#[derive(Debug, Clone, Copy, PartialEq, Eq)]
700enum TriggerProgramOutcome {
701 Continue,
702 Ignore,
703}
704
705#[derive(Debug, Clone)]
707pub struct CheckIssue {
708 pub table_id: u64,
709 pub table_name: String,
710 pub severity: String,
711 pub description: String,
712}
713
714#[derive(Debug, Clone)]
716pub struct AuthorizedReadSnapshot {
717 pub table: String,
718 pub table_snapshot: Snapshot,
719 pub data_generation: u64,
720 pub security_version: u64,
721 pub allowed_row_ids: Option<HashSet<RowId>>,
722}
723
724#[derive(Debug, Clone, Copy, PartialEq, Eq)]
726pub struct AuthorizedReadStamp {
727 pub table_id: u64,
728 pub schema_id: u64,
729 pub data_generation: u64,
730 pub security_version: u64,
731 pub snapshot: Snapshot,
732}
733
734type RlsCacheKey = (String, u64, u64, String);
735
736#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
738pub struct RlsCacheStats {
739 pub entries: usize,
740 pub bytes: usize,
741 pub hits: u64,
742 pub misses: u64,
743 pub evictions: u64,
744 pub build_nanos: u64,
745 pub rows_evaluated: u64,
746}
747
748const RLS_CACHE_MAX_BYTES: usize = 64 * 1024 * 1024;
749const CDC_MAX_WAL_RECORDS: usize = 1_000_000;
750const CDC_MAX_WAL_REPLAY_BYTES: usize = 256 * 1024 * 1024;
751const CDC_MAX_EVENTS: usize = 100_000;
752const CDC_MAX_ROWS: usize = 1_000_000;
753const CDC_MAX_INLINE_PAYLOAD_BYTES: usize = 32 * 1024 * 1024;
754const CDC_MAX_RETAINED_BYTES: usize = 256 * 1024 * 1024;
755
756fn charge_cdc_bytes(total: &mut usize, amount: usize, resource: &'static str) -> Result<()> {
757 let requested = total.saturating_add(amount);
758 if requested > CDC_MAX_RETAINED_BYTES {
759 return Err(MongrelError::ResourceLimitExceeded {
760 resource,
761 requested,
762 limit: CDC_MAX_RETAINED_BYTES,
763 });
764 }
765 *total = requested;
766 Ok(())
767}
768
769fn cdc_row_storage_bytes(row: &crate::memtable::Row) -> usize {
770 usize::try_from(row.estimated_bytes())
771 .unwrap_or(usize::MAX)
772 .saturating_add(std::mem::size_of::<crate::memtable::Row>())
773}
774
775fn cdc_row_json_bytes(row: &crate::memtable::Row) -> usize {
776 let value_slot = std::mem::size_of::<serde_json::Value>();
777 row.columns.values().fold(512_usize, |bytes, value| {
778 let values = match value {
779 Value::Bytes(values) => values.len(),
780 Value::Json(values) => values.len(),
781 Value::Embedding(values) => values.len(),
782 _ => 1,
783 };
784 bytes.saturating_add(values.saturating_mul(value_slot))
785 })
786}
787
788fn cdc_rows_json_bytes(rows: &[crate::memtable::Row]) -> usize {
789 rows.iter().fold(0_usize, |bytes, row| {
790 bytes.saturating_add(cdc_row_json_bytes(row))
791 })
792}
793
794#[derive(Default)]
795struct RlsCache {
796 entries: HashMap<RlsCacheKey, (Arc<HashSet<RowId>>, usize)>,
797 lru: VecDeque<RlsCacheKey>,
798 bytes: usize,
799 hits: u64,
800 misses: u64,
801 evictions: u64,
802 build_nanos: u64,
803 rows_evaluated: u64,
804}
805
806impl RlsCache {
807 fn get(&mut self, key: &RlsCacheKey) -> Option<Arc<HashSet<RowId>>> {
808 let value = self.entries.get(key).map(|(value, _)| Arc::clone(value));
809 if value.is_some() {
810 self.hits = self.hits.saturating_add(1);
811 self.touch(key);
812 } else {
813 self.misses = self.misses.saturating_add(1);
814 }
815 value
816 }
817
818 fn insert(&mut self, key: RlsCacheKey, value: Arc<HashSet<RowId>>) {
819 let bytes = key
820 .0
821 .len()
822 .saturating_add(key.3.len())
823 .saturating_add(
824 value
825 .capacity()
826 .saturating_mul(std::mem::size_of::<RowId>() * 3),
827 )
828 .saturating_add(std::mem::size_of::<RlsCacheKey>());
829 if bytes > RLS_CACHE_MAX_BYTES {
830 return;
831 }
832 if let Some((_, old_bytes)) = self.entries.remove(&key) {
833 self.bytes = self.bytes.saturating_sub(old_bytes);
834 }
835 self.lru.retain(|candidate| candidate != &key);
836 while self.bytes.saturating_add(bytes) > RLS_CACHE_MAX_BYTES {
837 let Some(oldest) = self.lru.pop_front() else {
838 break;
839 };
840 if let Some((_, old_bytes)) = self.entries.remove(&oldest) {
841 self.bytes = self.bytes.saturating_sub(old_bytes);
842 self.evictions = self.evictions.saturating_add(1);
843 }
844 }
845 self.bytes = self.bytes.saturating_add(bytes);
846 self.lru.push_back(key.clone());
847 self.entries.insert(key, (value, bytes));
848 }
849
850 fn touch(&mut self, key: &RlsCacheKey) {
851 self.lru.retain(|candidate| candidate != key);
852 self.lru.push_back(key.clone());
853 }
854
855 fn stats(&self) -> RlsCacheStats {
856 RlsCacheStats {
857 entries: self.entries.len(),
858 bytes: self.bytes,
859 hits: self.hits,
860 misses: self.misses,
861 evictions: self.evictions,
862 build_nanos: self.build_nanos,
863 rows_evaluated: self.rows_evaluated,
864 }
865 }
866}
867
868#[derive(Clone)]
870pub struct TableHandle {
871 inner: TableHandleInner,
872 generation_metrics: Arc<TableGenerationMetrics>,
873}
874
875#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
876pub struct TableGenerationStats {
877 pub active_read_generations: usize,
878 pub max_live_read_generations: usize,
879 pub cow_clone_count: u64,
880 pub cow_clone_nanos: u64,
881 pub estimated_cow_clone_bytes: u64,
882 pub writer_wait_nanos: u64,
883}
884
885#[derive(Default)]
886#[doc(hidden)]
887pub struct TableGenerationMetrics {
888 active_read_generations: AtomicUsize,
889 max_live_read_generations: AtomicUsize,
890 cow_clone_count: AtomicU64,
891 cow_clone_nanos: AtomicU64,
892 estimated_cow_clone_bytes: AtomicU64,
893 writer_wait_nanos: AtomicU64,
894}
895
896impl TableGenerationMetrics {
897 fn activate(self: &Arc<Self>, table: Table) -> Arc<TableReadGeneration> {
898 let active = self.active_read_generations.fetch_add(1, Ordering::Relaxed) + 1;
899 self.max_live_read_generations
900 .fetch_max(active, Ordering::Relaxed);
901 Arc::new(TableReadGeneration {
902 table,
903 metrics: Arc::clone(self),
904 })
905 }
906
907 fn stats(&self) -> TableGenerationStats {
908 TableGenerationStats {
909 active_read_generations: self.active_read_generations.load(Ordering::Relaxed),
910 max_live_read_generations: self.max_live_read_generations.load(Ordering::Relaxed),
911 cow_clone_count: self.cow_clone_count.load(Ordering::Relaxed),
912 cow_clone_nanos: self.cow_clone_nanos.load(Ordering::Relaxed),
913 estimated_cow_clone_bytes: self.estimated_cow_clone_bytes.load(Ordering::Relaxed),
914 writer_wait_nanos: self.writer_wait_nanos.load(Ordering::Relaxed),
915 }
916 }
917}
918
919pub struct TableReadGeneration {
921 table: Table,
922 metrics: Arc<TableGenerationMetrics>,
923}
924
925impl std::ops::Deref for TableReadGeneration {
926 type Target = Table;
927
928 fn deref(&self) -> &Self::Target {
929 &self.table
930 }
931}
932
933impl Drop for TableReadGeneration {
934 fn drop(&mut self) {
935 self.metrics
936 .active_read_generations
937 .fetch_sub(1, Ordering::Relaxed);
938 }
939}
940
941#[derive(Clone)]
942enum TableHandleInner {
943 CopyOnWrite(Arc<RwLock<Arc<Table>>>),
944 Direct(Arc<Mutex<Table>>),
945}
946
947pub enum TableGuard<'a> {
948 CopyOnWrite {
949 table: parking_lot::RwLockWriteGuard<'a, Arc<Table>>,
950 metrics: Arc<TableGenerationMetrics>,
951 },
952 Direct {
953 table: parking_lot::MutexGuard<'a, Table>,
954 },
955}
956
957impl TableHandle {
958 fn new(table: Table) -> Self {
959 Self {
960 inner: TableHandleInner::CopyOnWrite(Arc::new(RwLock::new(Arc::new(table)))),
961 generation_metrics: Arc::new(TableGenerationMetrics::default()),
962 }
963 }
964
965 pub fn from_table(table: Table) -> Self {
966 Self::new(table)
967 }
968
969 pub fn lock(&self) -> TableGuard<'_> {
970 let started = std::time::Instant::now();
971 let guard = match &self.inner {
972 TableHandleInner::CopyOnWrite(table) => TableGuard::CopyOnWrite {
973 table: table.write(),
974 metrics: Arc::clone(&self.generation_metrics),
975 },
976 TableHandleInner::Direct(table) => TableGuard::Direct {
977 table: table.lock(),
978 },
979 };
980 self.generation_metrics.writer_wait_nanos.fetch_add(
981 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
982 Ordering::Relaxed,
983 );
984 guard
985 }
986
987 fn try_lock_for(&self, timeout: std::time::Duration) -> Option<TableGuard<'_>> {
988 let started = std::time::Instant::now();
989 let guard = match &self.inner {
990 TableHandleInner::CopyOnWrite(table) => {
991 table
992 .try_write_for(timeout)
993 .map(|table| TableGuard::CopyOnWrite {
994 table,
995 metrics: Arc::clone(&self.generation_metrics),
996 })
997 }
998 TableHandleInner::Direct(table) => table
999 .try_lock_for(timeout)
1000 .map(|table| TableGuard::Direct { table }),
1001 };
1002 self.generation_metrics.writer_wait_nanos.fetch_add(
1003 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1004 Ordering::Relaxed,
1005 );
1006 guard
1007 }
1008
1009 pub fn ptr_eq(&self, other: &Self) -> bool {
1010 match (&self.inner, &other.inner) {
1011 (TableHandleInner::CopyOnWrite(left), TableHandleInner::CopyOnWrite(right)) => {
1012 Arc::ptr_eq(left, right)
1013 }
1014 (TableHandleInner::Direct(left), TableHandleInner::Direct(right)) => {
1015 Arc::ptr_eq(left, right)
1016 }
1017 _ => false,
1018 }
1019 }
1020
1021 pub fn read_generation_with_context(
1022 &self,
1023 context: Option<&crate::query::AiExecutionContext>,
1024 ) -> Result<(Arc<TableReadGeneration>, Snapshot)> {
1025 let mut table = if let Some(context) = context {
1026 loop {
1027 context.checkpoint()?;
1028 let wait = context
1029 .remaining_duration()
1030 .unwrap_or(std::time::Duration::from_millis(5))
1031 .min(std::time::Duration::from_millis(5));
1032 if let Some(table) = self.try_lock_for(wait) {
1033 break table;
1034 }
1035 }
1036 } else {
1037 self.lock()
1038 };
1039 let snapshot = table.snapshot();
1040 let generation = table.clone_read_generation()?;
1041 Ok((self.generation_metrics.activate(generation), snapshot))
1042 }
1043
1044 pub fn generation_stats(&self) -> TableGenerationStats {
1045 self.generation_metrics.stats()
1046 }
1047}
1048
1049impl From<Arc<Mutex<Table>>> for TableHandle {
1050 fn from(table: Arc<Mutex<Table>>) -> Self {
1051 Self {
1052 inner: TableHandleInner::Direct(table),
1053 generation_metrics: Arc::new(TableGenerationMetrics::default()),
1054 }
1055 }
1056}
1057
1058impl std::ops::Deref for TableGuard<'_> {
1059 type Target = Table;
1060
1061 fn deref(&self) -> &Self::Target {
1062 match self {
1063 Self::CopyOnWrite { table, .. } => table.as_ref(),
1064 Self::Direct { table } => table,
1065 }
1066 }
1067}
1068
1069impl std::ops::DerefMut for TableGuard<'_> {
1070 fn deref_mut(&mut self) -> &mut Self::Target {
1071 match self {
1072 Self::CopyOnWrite { table, metrics } => {
1073 if Arc::strong_count(table) > 1 || Arc::weak_count(table) > 0 {
1074 let estimated_bytes = table.estimated_clone_bytes();
1075 let started = std::time::Instant::now();
1076 let table = Arc::make_mut(table);
1077 metrics.cow_clone_count.fetch_add(1, Ordering::Relaxed);
1078 metrics.cow_clone_nanos.fetch_add(
1079 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1080 Ordering::Relaxed,
1081 );
1082 metrics
1083 .estimated_cow_clone_bytes
1084 .fetch_add(estimated_bytes, Ordering::Relaxed);
1085 table
1086 } else {
1087 Arc::make_mut(table)
1088 }
1089 }
1090 Self::Direct { table } => table,
1091 }
1092 }
1093}
1094
1095#[derive(Clone, Debug)]
1096pub struct ReadAuthorization {
1097 pub operation: crate::auth::ColumnOperation,
1098 pub columns: Vec<u16>,
1099 pub permissions: Vec<crate::auth::Permission>,
1100}
1101
1102#[derive(Default, Debug)]
1103struct TableWritePermissionNeeds {
1104 insert: bool,
1105 insert_columns: Vec<u16>,
1106 update: bool,
1107 update_columns: Vec<u16>,
1108 delete: bool,
1109 truncate: bool,
1110}
1111
1112#[cfg(test)]
1113thread_local! {
1114 static WRITE_PERMISSION_DECISIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1115 static AUTO_INCREMENT_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1116 static PREBUILD_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1117 static PUBLISH_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1118 static COMMIT_MANIFEST_WRITES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1119 static TABLE_PERMISSION_DECISIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1120}
1121
1122fn summarize_write_permissions(
1123 staging: &[(u64, crate::txn::Staged)],
1124) -> HashMap<u64, TableWritePermissionNeeds> {
1125 use crate::txn::Staged;
1126
1127 let mut needs = HashMap::<u64, TableWritePermissionNeeds>::new();
1128 for (table_id, operation) in staging {
1129 let table = needs.entry(*table_id).or_default();
1130 match operation {
1131 Staged::Put(cells) => {
1132 table.insert = true;
1133 table
1134 .insert_columns
1135 .extend(cells.iter().map(|(column, _)| *column));
1136 }
1137 Staged::Update {
1138 changed_columns, ..
1139 } => {
1140 table.update = true;
1141 table.update_columns.extend(changed_columns);
1142 }
1143 Staged::Delete(_) => table.delete = true,
1144 Staged::Truncate => table.truncate = true,
1145 }
1146 }
1147 for table in needs.values_mut() {
1148 table.insert_columns.sort_unstable();
1149 table.insert_columns.dedup();
1150 table.update_columns.sort_unstable();
1151 table.update_columns.dedup();
1152 }
1153 needs
1154}
1155
1156struct SecurityCoordinator {
1157 gate: RwLock<()>,
1159 version: AtomicU64,
1160}
1161
1162fn security_coordinator(root: &Path, version: u64) -> Arc<SecurityCoordinator> {
1163 static COORDINATORS: std::sync::OnceLock<
1164 Mutex<HashMap<PathBuf, std::sync::Weak<SecurityCoordinator>>>,
1165 > = std::sync::OnceLock::new();
1166
1167 let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
1168 let mut coordinators = COORDINATORS
1169 .get_or_init(|| Mutex::new(HashMap::new()))
1170 .lock();
1171 coordinators.retain(|_, coordinator| coordinator.strong_count() > 0);
1172 if let Some(coordinator) = coordinators.get(&root).and_then(std::sync::Weak::upgrade) {
1173 return coordinator;
1174 }
1175 let coordinator = Arc::new(SecurityCoordinator {
1176 gate: RwLock::new(()),
1177 version: AtomicU64::new(version),
1178 });
1179 coordinators.insert(root, Arc::downgrade(&coordinator));
1180 coordinator
1181}
1182
1183pub fn lock_table_with_context<'a>(
1184 handle: &'a TableHandle,
1185 context: Option<&crate::query::AiExecutionContext>,
1186) -> Result<TableGuard<'a>> {
1187 let Some(context) = context else {
1188 return Ok(handle.lock());
1189 };
1190 loop {
1191 context.checkpoint()?;
1192 let wait = context
1193 .remaining_duration()
1194 .unwrap_or(std::time::Duration::from_millis(5))
1195 .min(std::time::Duration::from_millis(5));
1196 if let Some(guard) = handle.try_lock_for(wait) {
1197 return Ok(guard);
1198 }
1199 }
1200}
1201
1202#[derive(Clone, Debug, Default)]
1208pub struct OpenOptions {
1209 pub lock_timeout_ms: u32,
1224}
1225
1226impl OpenOptions {
1227 pub fn with_lock_timeout_ms(mut self, ms: u32) -> Self {
1230 self.lock_timeout_ms = ms;
1231 self
1232 }
1233}
1234
1235pub struct Database {
1238 root: PathBuf,
1239 durable_root: Arc<crate::durable_file::DurableRoot>,
1240 read_only: bool,
1242 catalog: RwLock<Catalog>,
1243 security_coordinator: Arc<SecurityCoordinator>,
1244 security_catalog_disk_reads: AtomicU64,
1245 rls_cache: Mutex<RlsCache>,
1246 epoch: Arc<EpochAuthority>,
1247 snapshots: Arc<SnapshotRegistry>,
1248 page_cache: Arc<crate::cache::Sharded<crate::cache::PageCache>>,
1249 decoded_cache: Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>,
1250 commit_lock: Arc<Mutex<()>>,
1251 shared_wal: Arc<Mutex<crate::wal::SharedWal>>,
1256 next_txn_id: Arc<Mutex<u64>>,
1260 tables: RwLock<HashMap<u64, TableHandle>>,
1261 kek: Option<Arc<crate::encryption::Kek>>,
1262 ddl_lock: Mutex<()>,
1265 meta_dek: Option<[u8; META_DEK_LEN]>,
1266 spill_threshold: std::sync::atomic::AtomicU64,
1269 conflicts: crate::txn::ConflictIndex,
1272 active_txns: crate::txn::ActiveTxns,
1275 poisoned: Arc<std::sync::atomic::AtomicBool>,
1278 group: Arc<crate::txn::GroupCommit>,
1282 active_spills: Arc<crate::retention::ActiveSpills>,
1285 replication_barrier: parking_lot::RwLock<()>,
1288 replication_wal_retention_segments: AtomicUsize,
1290 backup_pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
1293 #[doc(hidden)]
1297 spill_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1298 #[doc(hidden)]
1300 security_commit_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1301 #[doc(hidden)]
1304 catalog_commit_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1305 #[doc(hidden)]
1308 backup_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1309 replication_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1310 trigger_recursive: AtomicBool,
1311 trigger_max_depth: AtomicU32,
1312 trigger_max_loop_iterations: AtomicU32,
1313 _lock: Option<DatabaseFileLock>,
1316 notify: tokio::sync::broadcast::Sender<ChangeEvent>,
1319 change_wake: tokio::sync::broadcast::Sender<()>,
1322 principal: RwLock<Option<crate::auth::Principal>>,
1332 auth_state: crate::auth_state::AuthState,
1338}
1339
1340struct RunPins {
1341 pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
1342 runs: Vec<(u64, u128)>,
1343}
1344
1345struct BackupFilePins {
1346 root: PathBuf,
1347}
1348
1349struct PendingTableDir {
1350 path: PathBuf,
1351 armed: bool,
1352}
1353
1354impl PendingTableDir {
1355 fn new(path: PathBuf) -> Self {
1356 Self { path, armed: true }
1357 }
1358
1359 fn disarm(&mut self) {
1360 self.armed = false;
1361 }
1362}
1363
1364impl Drop for PendingTableDir {
1365 fn drop(&mut self) {
1366 if self.armed {
1367 let _ = std::fs::remove_dir_all(&self.path);
1368 }
1369 }
1370}
1371
1372impl Drop for BackupFilePins {
1373 fn drop(&mut self) {
1374 let _ = std::fs::remove_dir_all(&self.root);
1375 }
1376}
1377
1378impl Drop for RunPins {
1379 fn drop(&mut self) {
1380 let mut pins = self.pins.lock();
1381 for run in &self.runs {
1382 if let Some(count) = pins.get_mut(run) {
1383 *count -= 1;
1384 if *count == 0 {
1385 pins.remove(run);
1386 }
1387 }
1388 }
1389 }
1390}
1391
1392#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1395pub struct ChangeEvent {
1396 pub id: Option<String>,
1397 pub channel: String,
1398 pub table_id: Option<u64>,
1399 pub table: String,
1400 pub op: String,
1401 pub epoch: u64,
1402 pub txn_id: Option<u64>,
1403 pub message: Option<String>,
1404 pub data: Option<serde_json::Value>,
1405}
1406
1407#[derive(Debug, Clone)]
1408pub struct CdcBatch {
1409 pub events: Vec<ChangeEvent>,
1410 pub current_epoch: u64,
1411 pub earliest_epoch: Option<u64>,
1412 pub gap: bool,
1413}
1414
1415impl std::fmt::Debug for Database {
1422 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1423 let cat = self.catalog.read();
1424 let principal_guard = self.principal.read();
1425 let principal: &str = principal_guard
1426 .as_ref()
1427 .map(|p| p.username.as_str())
1428 .unwrap_or("<none>");
1429 f.debug_struct("Database")
1430 .field("root", &self.root)
1431 .field("db_epoch", &cat.db_epoch)
1432 .field("open_generation", &"sidecar")
1433 .field("tables", &cat.tables.len())
1434 .field("visible_epoch", &self.epoch.visible().0)
1435 .field("encrypted", &self.kek.is_some())
1436 .field("require_auth", &cat.require_auth)
1437 .field("principal", &principal)
1438 .finish()
1439 }
1440}
1441
1442impl Database {
1443 fn canonical_lock_target(root: &Path) -> std::io::Result<(PathBuf, PathBuf)> {
1444 if let Ok(canonical) = root.canonicalize() {
1445 let lock_dir = canonical.parent().ok_or_else(|| {
1446 std::io::Error::new(
1447 std::io::ErrorKind::InvalidInput,
1448 "database root must have a parent directory",
1449 )
1450 })?;
1451 return Ok((canonical.clone(), lock_dir.to_path_buf()));
1452 }
1453
1454 let absolute = if root.is_absolute() {
1455 root.to_path_buf()
1456 } else {
1457 std::env::current_dir()?.join(root)
1458 };
1459 let mut cursor = absolute.as_path();
1460 let mut suffix = Vec::new();
1461 while !cursor.exists() {
1462 let name = cursor.file_name().ok_or_else(|| {
1463 std::io::Error::new(
1464 std::io::ErrorKind::NotFound,
1465 format!("no existing ancestor for database root {}", root.display()),
1466 )
1467 })?;
1468 suffix.push(name.to_os_string());
1469 cursor = cursor.parent().ok_or_else(|| {
1470 std::io::Error::new(
1471 std::io::ErrorKind::NotFound,
1472 format!("no existing ancestor for database root {}", root.display()),
1473 )
1474 })?;
1475 }
1476 let lock_dir = cursor.canonicalize()?;
1477 let mut canonical = lock_dir.clone();
1478 for component in suffix.iter().rev() {
1479 canonical.push(component);
1480 }
1481 Ok((canonical, lock_dir))
1482 }
1483
1484 fn acquire_database_lock(root: &Path, timeout_ms: u32) -> Result<DatabaseFileLock> {
1485 use std::hash::{Hash, Hasher};
1486
1487 let (canonical_path, lock_dir) = Self::canonical_lock_target(root)?;
1488 {
1489 let mut locked = process_locked_paths().lock();
1490 if !locked.insert(canonical_path.clone()) {
1491 return Err(MongrelError::DatabaseLocked {
1492 path: root.to_path_buf(),
1493 message: "already open in this process".into(),
1494 });
1495 }
1496 }
1497
1498 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1499 canonical_path.hash(&mut hasher);
1500 let lock_path = lock_dir.join(format!(".mongreldb-{:016x}.lock", hasher.finish()));
1501 let file = match std::fs::OpenOptions::new()
1502 .create(true)
1503 .truncate(false)
1504 .write(true)
1505 .open(lock_path)
1506 {
1507 Ok(file) => file,
1508 Err(error) => {
1509 process_locked_paths().lock().remove(&canonical_path);
1510 return Err(error.into());
1511 }
1512 };
1513 if let Err(error) = Self::fs_lock_exclusive(&file, timeout_ms) {
1514 process_locked_paths().lock().remove(&canonical_path);
1515 return Err(MongrelError::DatabaseLocked {
1516 path: root.to_path_buf(),
1517 message: error.to_string(),
1518 });
1519 }
1520 Ok(DatabaseFileLock {
1521 bootstrap_file: file,
1522 legacy_file: None,
1523 canonical_path,
1524 durable_root: None,
1525 })
1526 }
1527
1528 fn acquire_legacy_database_lock(
1529 lock: &mut DatabaseFileLock,
1530 root: &Path,
1531 timeout_ms: u32,
1532 ) -> Result<()> {
1533 let durable_root = lock
1534 .durable_root
1535 .as_ref()
1536 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?;
1537 let file = durable_root.open_lock_file(Path::new(META_DIR).join(".lock"))?;
1538 if let Err(error) = Self::fs_lock_exclusive(&file, timeout_ms) {
1539 return Err(MongrelError::DatabaseLocked {
1540 path: root.to_path_buf(),
1541 message: error.to_string(),
1542 });
1543 }
1544 lock.legacy_file = Some(file);
1545 Ok(())
1546 }
1547
1548 fn pin_or_create_database_root(path: &Path) -> Result<crate::durable_file::DurableRoot> {
1549 if path.exists() {
1550 return crate::durable_file::DurableRoot::open(path).map_err(Into::into);
1551 }
1552 let mut ancestor = path;
1553 while !ancestor.exists() {
1554 ancestor = ancestor.parent().ok_or_else(|| {
1555 MongrelError::NotFound(format!(
1556 "no existing ancestor for database root {}",
1557 path.display()
1558 ))
1559 })?;
1560 }
1561 let relative = path.strip_prefix(ancestor).map_err(|error| {
1562 MongrelError::InvalidArgument(format!("invalid database root: {error}"))
1563 })?;
1564 crate::durable_file::DurableRoot::open(ancestor)?
1565 .create_directory_all_pinned(relative)
1566 .map_err(Into::into)
1567 }
1568
1569 fn begin_create(root: impl AsRef<Path>) -> Result<(PathBuf, DatabaseFileLock)> {
1570 let requested_root = root.as_ref();
1571 let mut lock = Self::acquire_database_lock(requested_root, 0)?;
1572 let root = lock.canonical_path.clone();
1573 Self::reject_existing_database(&root)?;
1574 let durable_root = Arc::new(Self::pin_or_create_database_root(&root)?);
1575 if durable_root.canonical_path() != lock.canonical_path {
1576 return Err(MongrelError::Conflict(
1577 "database root changed while it was being created".into(),
1578 ));
1579 }
1580 durable_root.create_directory_all(META_DIR)?;
1581 lock.durable_root = Some(durable_root);
1582 let io_root = lock
1583 .durable_root
1584 .as_ref()
1585 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
1586 .io_path()?;
1587 Self::acquire_legacy_database_lock(&mut lock, &io_root, 0)?;
1588 Self::reject_existing_database(&io_root)?;
1589 Ok((io_root, lock))
1590 }
1591
1592 fn begin_open(
1593 root: impl AsRef<Path>,
1594 lock_timeout_ms: u32,
1595 ) -> Result<(PathBuf, DatabaseFileLock)> {
1596 let root = root.as_ref();
1597 let durable_root = crate::durable_file::DurableRoot::open(root).map_err(|error| {
1598 if error.kind() == std::io::ErrorKind::NotFound {
1599 MongrelError::NotFound(format!("database root {}: {error}", root.display()))
1600 } else {
1601 error.into()
1602 }
1603 })?;
1604 Self::begin_open_durable(durable_root, lock_timeout_ms)
1605 }
1606
1607 fn begin_open_durable(
1608 durable_root: crate::durable_file::DurableRoot,
1609 lock_timeout_ms: u32,
1610 ) -> Result<(PathBuf, DatabaseFileLock)> {
1611 let io_root = durable_root.io_path()?;
1612 let current_root = io_root.canonicalize()?;
1613 let mut lock = Self::acquire_database_lock(¤t_root, lock_timeout_ms)?;
1614 lock.durable_root = Some(Arc::new(durable_root));
1615 let io_root = lock
1616 .durable_root
1617 .as_ref()
1618 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
1619 .io_path()?;
1620 if lock
1621 .durable_root
1622 .as_ref()
1623 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
1624 .open_directory(META_DIR)
1625 .is_err()
1626 {
1627 return Err(MongrelError::NotFound(format!(
1628 "no database metadata found at {:?}",
1629 current_root
1630 )));
1631 }
1632 Self::acquire_legacy_database_lock(&mut lock, &io_root, lock_timeout_ms)?;
1633 Ok((io_root, lock))
1634 }
1635
1636 pub fn create(root: impl AsRef<Path>) -> Result<Self> {
1638 let (root, lock) = Self::begin_create(root)?;
1639 Self::create_inner(root, None, lock)
1640 }
1641
1642 #[cfg(feature = "encryption")]
1645 pub fn create_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
1646 let (root, lock) = Self::begin_create(root)?;
1647 let salt = crate::encryption::random_salt()?;
1648 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
1649 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1650 Self::create_inner(root, Some(kek), lock)
1651 }
1652
1653 #[cfg(feature = "encryption")]
1656 pub fn create_with_key(root: impl AsRef<Path>, key: &[u8]) -> 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::from_raw_key(key, &salt)?);
1661 Self::create_inner(root, Some(kek), lock)
1662 }
1663
1664 fn create_inner(
1665 root: PathBuf,
1666 kek: Option<Arc<crate::encryption::Kek>>,
1667 lock: DatabaseFileLock,
1668 ) -> Result<Self> {
1669 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
1670 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
1671 let cat = Catalog::empty();
1672 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
1673 Self::finish_open(root, cat, kek, meta_dek, false, None, None, None, lock)
1674 }
1675
1676 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
1678 Self::open_inner(root, None, None)
1679 }
1680
1681 #[cfg(feature = "encryption")]
1683 pub fn open_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
1684 let (root, lock) = Self::begin_open(root, 0)?;
1685 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
1686 MongrelError::Other("database root descriptor was not pinned".into())
1687 })?)?;
1688 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1689 Self::open_inner_locked(root, Some(kek), lock)
1690 }
1691
1692 #[cfg(feature = "encryption")]
1695 pub fn open_encrypted_with_options(
1696 root: impl AsRef<Path>,
1697 passphrase: &str,
1698 options: OpenOptions,
1699 ) -> Result<Self> {
1700 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
1701 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
1702 MongrelError::Other("database root descriptor was not pinned".into())
1703 })?)?;
1704 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1705 Self::open_inner_locked(root, Some(kek), lock)
1706 }
1707
1708 #[cfg(feature = "encryption")]
1710 pub fn open_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
1711 let (root, lock) = Self::begin_open(root, 0)?;
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::from_raw_key(key, &salt)?);
1716 Self::open_inner_locked(root, Some(kek), lock)
1717 }
1718
1719 pub fn open_with_credentials(
1731 root: impl AsRef<Path>,
1732 username: &str,
1733 password: &str,
1734 ) -> Result<Self> {
1735 Self::open_inner_with_credentials(root, None, username, password)
1736 }
1737
1738 pub fn open_with_credentials_and_options(
1742 root: impl AsRef<Path>,
1743 username: &str,
1744 password: &str,
1745 options: OpenOptions,
1746 ) -> Result<Self> {
1747 Self::open_inner_with_credentials_and_lock_timeout(
1748 root,
1749 None,
1750 username,
1751 password,
1752 options.lock_timeout_ms,
1753 )
1754 }
1755
1756 #[cfg(feature = "encryption")]
1759 pub fn open_encrypted_with_credentials(
1760 root: impl AsRef<Path>,
1761 passphrase: &str,
1762 username: &str,
1763 password: &str,
1764 ) -> Result<Self> {
1765 let (root, lock) = Self::begin_open(root, 0)?;
1766 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
1767 MongrelError::Other("database root descriptor was not pinned".into())
1768 })?)?;
1769 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1770 Self::open_inner_with_credentials_locked(root, Some(kek), username, password, lock)
1771 }
1772
1773 #[cfg(feature = "encryption")]
1777 pub fn open_encrypted_with_credentials_and_options(
1778 root: impl AsRef<Path>,
1779 passphrase: &str,
1780 username: &str,
1781 password: &str,
1782 options: OpenOptions,
1783 ) -> Result<Self> {
1784 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
1785 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
1786 MongrelError::Other("database root descriptor was not pinned".into())
1787 })?)?;
1788 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1789 Self::open_inner_with_credentials_locked(root, Some(kek), username, password, lock)
1790 }
1791
1792 pub fn open_with_options(root: impl AsRef<Path>, options: OpenOptions) -> Result<Self> {
1799 Self::open_inner_with_lock_timeout(root, None, None, options.lock_timeout_ms)
1802 }
1803
1804 fn open_inner_with_lock_timeout(
1805 root: impl AsRef<Path>,
1806 kek: Option<Arc<crate::encryption::Kek>>,
1807 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
1808 lock_timeout_ms: u32,
1809 ) -> Result<Self> {
1810 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
1811 Self::open_inner_locked(root, kek, lock)
1812 }
1813
1814 fn open_inner_locked(
1815 root: PathBuf,
1816 kek: Option<Arc<crate::encryption::Kek>>,
1817 lock: DatabaseFileLock,
1818 ) -> Result<Self> {
1819 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
1820 let mut cat = catalog::read_durable(
1821 lock.durable_root.as_deref().ok_or_else(|| {
1822 MongrelError::Other("database root descriptor was not pinned".into())
1823 })?,
1824 meta_dek.as_ref(),
1825 )?
1826 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
1827 let recovery_checkpoint = cat.clone();
1828
1829 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
1832 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
1833 lock.durable_root.as_deref().ok_or_else(|| {
1834 MongrelError::Other("database root descriptor was not pinned".into())
1835 })?,
1836 wal_dek.as_ref(),
1837 )?;
1838 recover_ddl_from_records(
1839 &root,
1840 Some(lock.durable_root.as_deref().ok_or_else(|| {
1841 MongrelError::Other("database root descriptor was not pinned".into())
1842 })?),
1843 &mut cat,
1844 meta_dek.as_ref(),
1845 false,
1846 None,
1847 &recovery_records,
1848 )?;
1849 Self::finish_open(
1850 root,
1851 cat,
1852 kek,
1853 meta_dek,
1854 true,
1855 Some(recovery_checkpoint),
1856 Some(recovery_records),
1857 None,
1858 lock,
1859 )
1860 }
1861
1862 fn open_inner_with_credentials(
1869 root: impl AsRef<Path>,
1870 kek: Option<Arc<crate::encryption::Kek>>,
1871 username: &str,
1872 password: &str,
1873 ) -> Result<Self> {
1874 Self::open_inner_with_credentials_and_lock_timeout(root, kek, username, password, 0)
1875 }
1876
1877 fn open_inner_with_credentials_and_lock_timeout(
1881 root: impl AsRef<Path>,
1882 kek: Option<Arc<crate::encryption::Kek>>,
1883 username: &str,
1884 password: &str,
1885 lock_timeout_ms: u32,
1886 ) -> Result<Self> {
1887 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
1888 Self::open_inner_with_credentials_locked(root, kek, username, password, lock)
1889 }
1890
1891 fn open_inner_with_credentials_locked(
1892 root: PathBuf,
1893 kek: Option<Arc<crate::encryption::Kek>>,
1894 username: &str,
1895 password: &str,
1896 lock: DatabaseFileLock,
1897 ) -> Result<Self> {
1898 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
1899 let mut cat = catalog::read_durable(
1900 lock.durable_root.as_deref().ok_or_else(|| {
1901 MongrelError::Other("database root descriptor was not pinned".into())
1902 })?,
1903 meta_dek.as_ref(),
1904 )?
1905 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
1906 let recovery_checkpoint = cat.clone();
1907
1908 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
1911 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
1912 lock.durable_root.as_deref().ok_or_else(|| {
1913 MongrelError::Other("database root descriptor was not pinned".into())
1914 })?,
1915 wal_dek.as_ref(),
1916 )?;
1917 recover_ddl_from_records(
1918 &root,
1919 Some(lock.durable_root.as_deref().ok_or_else(|| {
1920 MongrelError::Other("database root descriptor was not pinned".into())
1921 })?),
1922 &mut cat,
1923 meta_dek.as_ref(),
1924 false,
1925 None,
1926 &recovery_records,
1927 )?;
1928
1929 if !cat.require_auth {
1932 return Err(MongrelError::AuthNotRequired);
1933 }
1934
1935 let user = cat
1940 .users
1941 .iter()
1942 .find(|u| u.username == username)
1943 .filter(|u| !u.password_hash.is_empty())
1944 .ok_or_else(|| MongrelError::InvalidCredentials {
1945 username: username.to_string(),
1946 })?;
1947 let password_ok = crate::auth::verify_password(password, &user.password_hash)
1948 .map_err(MongrelError::Other)?;
1949 if !password_ok {
1950 return Err(MongrelError::InvalidCredentials {
1951 username: username.to_string(),
1952 });
1953 }
1954
1955 let principal =
1957 Self::resolve_principal_from_catalog(&cat, &user.username).ok_or_else(|| {
1958 MongrelError::InvalidCredentials {
1959 username: username.to_string(),
1960 }
1961 })?;
1962
1963 Self::finish_open(
1964 root,
1965 cat,
1966 kek,
1967 meta_dek,
1968 true,
1969 Some(recovery_checkpoint),
1970 Some(recovery_records),
1971 Some(principal),
1972 lock,
1973 )
1974 }
1975
1976 pub fn create_with_credentials(
1986 root: impl AsRef<Path>,
1987 admin_username: &str,
1988 admin_password: &str,
1989 ) -> Result<Self> {
1990 let (root, lock) = Self::begin_create(root)?;
1991 Self::create_inner_with_credentials(root, None, admin_username, admin_password, lock)
1992 }
1993
1994 #[cfg(feature = "encryption")]
1998 pub fn create_encrypted_with_credentials(
1999 root: impl AsRef<Path>,
2000 passphrase: &str,
2001 admin_username: &str,
2002 admin_password: &str,
2003 ) -> Result<Self> {
2004 let (root, lock) = Self::begin_create(root)?;
2005 let salt = crate::encryption::random_salt()?;
2006 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
2007 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2008 Self::create_inner_with_credentials(root, Some(kek), admin_username, admin_password, lock)
2009 }
2010
2011 fn create_inner_with_credentials(
2012 root: PathBuf,
2013 kek: Option<Arc<crate::encryption::Kek>>,
2014 admin_username: &str,
2015 admin_password: &str,
2016 lock: DatabaseFileLock,
2017 ) -> Result<Self> {
2018 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
2019 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2020
2021 let password_hash =
2023 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
2024 let mut cat = Catalog::empty();
2025 cat.require_auth = true;
2026 cat.next_user_id = 2;
2027 cat.users.push(crate::auth::UserEntry {
2028 id: 1,
2029 username: admin_username.to_string(),
2030 password_hash,
2031 roles: Vec::new(),
2032 is_admin: true,
2033 created_epoch: 0,
2034 });
2035 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
2036
2037 let admin_principal = crate::auth::Principal {
2040 user_id: 1,
2041 created_epoch: 0,
2042 username: admin_username.to_string(),
2043 is_admin: true,
2044 roles: Vec::new(),
2045 permissions: Vec::new(),
2046 };
2047 Self::finish_open(
2048 root,
2049 cat,
2050 kek,
2051 meta_dek,
2052 false,
2053 None,
2054 None,
2055 Some(admin_principal),
2056 lock,
2057 )
2058 }
2059
2060 fn reject_existing_database(root: &Path) -> Result<()> {
2061 if root.join(catalog::CATALOG_FILENAME).exists() {
2064 return Err(MongrelError::InvalidArgument(format!(
2065 "database already exists at {}; use Database::open() to open it, \
2066 or remove the directory first",
2067 root.display()
2068 )));
2069 }
2070 Ok(())
2071 }
2072
2073 fn open_inner(
2074 root: impl AsRef<Path>,
2075 kek: Option<Arc<crate::encryption::Kek>>,
2076 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
2077 ) -> Result<Self> {
2078 Self::open_inner_with_lock_timeout(root, kek, None, 0)
2079 }
2080
2081 pub(crate) fn open_replica_recovery_durable(
2085 root: &crate::durable_file::DurableRoot,
2086 ) -> Result<Self> {
2087 let (root, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
2088 Self::open_replica_recovery_inner(root, None, lock)
2089 }
2090
2091 #[cfg(feature = "encryption")]
2092 pub(crate) fn open_encrypted_replica_recovery_durable(
2093 root: &crate::durable_file::DurableRoot,
2094 passphrase: &str,
2095 ) -> Result<Self> {
2096 let (root_path, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
2097 let salt = read_encryption_salt(root)?;
2098 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2099 Self::open_replica_recovery_inner(root_path, Some(kek), lock)
2100 }
2101
2102 fn open_replica_recovery_inner(
2103 root: PathBuf,
2104 kek: Option<Arc<crate::encryption::Kek>>,
2105 lock: DatabaseFileLock,
2106 ) -> Result<Self> {
2107 if !root.join(META_DIR).join("replica").is_file() {
2108 return Err(MongrelError::InvalidArgument(
2109 "recovery auth bypass requires a marked replica staging directory".into(),
2110 ));
2111 }
2112 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2113 let mut cat = catalog::read_durable(
2114 lock.durable_root.as_deref().ok_or_else(|| {
2115 MongrelError::Other("database root descriptor was not pinned".into())
2116 })?,
2117 meta_dek.as_ref(),
2118 )?
2119 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
2120 let recovery_checkpoint = cat.clone();
2121 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
2122 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
2123 lock.durable_root.as_deref().ok_or_else(|| {
2124 MongrelError::Other("database root descriptor was not pinned".into())
2125 })?,
2126 wal_dek.as_ref(),
2127 )?;
2128 recover_ddl_from_records(
2129 &root,
2130 Some(lock.durable_root.as_deref().ok_or_else(|| {
2131 MongrelError::Other("database root descriptor was not pinned".into())
2132 })?),
2133 &mut cat,
2134 meta_dek.as_ref(),
2135 false,
2136 None,
2137 &recovery_records,
2138 )?;
2139 let principal = if cat.require_auth {
2140 cat.users
2141 .iter()
2142 .find(|user| user.is_admin)
2143 .and_then(|user| Self::resolve_principal_from_catalog(&cat, &user.username))
2144 .ok_or_else(|| {
2145 MongrelError::Schema(
2146 "authenticated replica catalog has no recoverable admin".into(),
2147 )
2148 })?
2149 .into()
2150 } else {
2151 None
2152 };
2153 Self::finish_open(
2154 root,
2155 cat,
2156 kek,
2157 meta_dek,
2158 true,
2159 Some(recovery_checkpoint),
2160 Some(recovery_records),
2161 principal,
2162 lock,
2163 )
2164 }
2165
2166 fn fs_lock_exclusive(f: &std::fs::File, timeout_ms: u32) -> std::io::Result<()> {
2178 use fs2::FileExt;
2179 if timeout_ms == 0 {
2180 return f.try_lock_exclusive();
2181 }
2182 let deadline =
2184 std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms as u64);
2185 let mut next_sleep = std::time::Duration::from_millis(1);
2186 loop {
2187 match f.try_lock_exclusive() {
2188 Ok(()) => return Ok(()),
2189 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
2190 let now = std::time::Instant::now();
2191 if now >= deadline {
2192 return Err(std::io::Error::new(
2193 std::io::ErrorKind::WouldBlock,
2194 format!("could not acquire database lock within {timeout_ms}ms"),
2195 ));
2196 }
2197 let remaining = deadline - now;
2198 let sleep = next_sleep.min(remaining);
2199 std::thread::sleep(sleep);
2200 next_sleep = next_sleep
2203 .saturating_mul(10)
2204 .min(std::time::Duration::from_millis(50));
2205 }
2206 Err(e) => return Err(e),
2207 }
2208 }
2209 }
2210
2211 #[allow(clippy::too_many_arguments)]
2212 fn finish_open(
2213 root: PathBuf,
2214 cat: Catalog,
2215 kek: Option<Arc<crate::encryption::Kek>>,
2216 meta_dek: Option<[u8; META_DEK_LEN]>,
2217 existing: bool,
2218 recovery_checkpoint: Option<Catalog>,
2219 recovery_records: Option<Vec<crate::wal::Record>>,
2220 principal: Option<crate::auth::Principal>,
2221 lock: DatabaseFileLock,
2222 ) -> Result<Self> {
2223 let durable_root = Arc::clone(lock.durable_root.as_ref().ok_or_else(|| {
2224 MongrelError::Other("database root descriptor was not pinned".into())
2225 })?);
2226 let read_only = if existing {
2227 match durable_root.open_regular(Path::new(META_DIR).join("replica")) {
2228 Ok(_) => true,
2229 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
2230 Err(error) => return Err(error.into()),
2231 }
2232 } else {
2233 false
2234 };
2235 let recovered_catalog = cat;
2236 let mut cat = recovered_catalog.clone();
2237 let abandoned = if existing && !read_only {
2238 let abandoned = cat
2239 .tables
2240 .iter()
2241 .filter(|entry| matches!(entry.state, TableState::Building { .. }))
2242 .map(|entry| entry.table_id)
2243 .collect::<Vec<_>>();
2244 for entry in &mut cat.tables {
2245 if abandoned.contains(&entry.table_id) {
2246 entry.state = TableState::Dropped {
2247 at_epoch: cat.db_epoch,
2248 };
2249 }
2250 }
2251 abandoned
2252 } else {
2253 Vec::new()
2254 };
2255 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
2256 let recovery_records = match (existing, recovery_records) {
2257 (true, Some(records)) => records,
2258 (true, None) => {
2259 return Err(MongrelError::Other(
2260 "existing open has no validated WAL recovery plan".into(),
2261 ))
2262 }
2263 (false, _) => Vec::new(),
2264 };
2265 let (history_epochs, history_start) =
2266 read_history_retention(&durable_root, Epoch(cat.db_epoch))?;
2267 let open_generation = if existing {
2268 let checkpoint = recovery_checkpoint.as_ref().ok_or_else(|| {
2269 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
2270 })?;
2271 let recovered_table_ids = cat
2272 .tables
2273 .iter()
2274 .filter(|entry| {
2275 checkpoint
2276 .tables
2277 .iter()
2278 .all(|checkpoint| checkpoint.table_id != entry.table_id)
2279 })
2280 .map(|entry| entry.table_id)
2281 .collect::<HashSet<_>>();
2282 let reconciled_table_ids = cat
2283 .tables
2284 .iter()
2285 .filter(|entry| {
2286 checkpoint
2287 .tables
2288 .iter()
2289 .find(|checkpoint| checkpoint.table_id == entry.table_id)
2290 .is_some_and(|checkpoint| {
2291 crate::wal::DdlOp::encode_schema(&checkpoint.schema).ok()
2292 != crate::wal::DdlOp::encode_schema(&entry.schema).ok()
2293 })
2294 })
2295 .map(|entry| entry.table_id)
2296 .collect::<HashSet<_>>();
2297 validate_shared_wal_recovery_plan(
2298 &durable_root,
2299 &cat,
2300 &recovered_table_ids,
2301 &reconciled_table_ids,
2302 meta_dek.as_ref(),
2303 kek.clone(),
2304 &recovery_records,
2305 )?;
2306 let retained_generation = recovery_records
2307 .iter()
2308 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
2309 .map(|record| record.txn_id >> 32)
2310 .max()
2311 .unwrap_or(0);
2312 let head_generation =
2313 crate::wal::SharedWal::durable_open_generation(&durable_root, wal_dek.as_ref())?;
2314 let durable_floor = match head_generation {
2315 Some(head) if retained_generation > head => {
2316 return Err(MongrelError::CorruptWal {
2317 offset: retained_generation,
2318 reason: format!(
2319 "retained transaction generation {retained_generation} exceeds WAL head generation {head}"
2320 ),
2321 })
2322 }
2323 Some(head) => head,
2324 None => retained_generation,
2325 };
2326 let stored = catalog::read_generation(&durable_root)?;
2327 if stored.is_some_and(|generation| generation < durable_floor) {
2328 return Err(MongrelError::Other(format!(
2329 "open-generation {stored:?} precedes durable WAL generation {durable_floor}"
2330 )));
2331 }
2332 let bumped = stored
2333 .unwrap_or(durable_floor)
2334 .max(durable_floor)
2335 .checked_add(1)
2336 .ok_or_else(|| MongrelError::Full("open-generation namespace exhausted".into()))?;
2337 if bumped > u32::MAX as u64 {
2338 return Err(MongrelError::Full(
2339 "open-generation namespace exhausted".into(),
2340 ));
2341 }
2342 bumped
2343 } else {
2344 0
2345 };
2346 let principal = if cat.require_auth {
2347 let supplied = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
2348 Some(
2349 Self::resolve_bound_principal_from_catalog(&cat, supplied)
2350 .ok_or(MongrelError::AuthRequired)?,
2351 )
2352 } else {
2353 principal
2354 };
2355 let mut table_roots = HashMap::<u64, Arc<crate::durable_file::DurableRoot>>::new();
2356 if existing {
2357 for entry in &cat.tables {
2358 if !matches!(entry.state, TableState::Live) {
2359 continue;
2360 }
2361 match durable_root
2362 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))
2363 {
2364 Ok(root) => {
2365 table_roots.insert(entry.table_id, Arc::new(root));
2366 }
2367 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
2368 Err(error) => return Err(error.into()),
2369 }
2370 }
2371 }
2372
2373 if existing {
2377 let mut applied = recovery_checkpoint.ok_or_else(|| {
2378 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
2379 })?;
2380 recover_ddl_from_records(
2381 &root,
2382 Some(&durable_root),
2383 &mut applied,
2384 meta_dek.as_ref(),
2385 true,
2386 Some(&table_roots),
2387 &recovery_records,
2388 )?;
2389 let catalog_value = |catalog: &Catalog| {
2390 serde_json::to_value(catalog)
2391 .map_err(|error| MongrelError::Other(format!("catalog compare: {error}")))
2392 };
2393 if catalog_value(&applied)? != catalog_value(&recovered_catalog)? {
2394 return Err(MongrelError::CorruptWal {
2395 offset: 0,
2396 reason: "validated and applied DDL recovery plans differ".into(),
2397 });
2398 }
2399 if catalog_value(&cat)? != catalog_value(&applied)? {
2400 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
2401 }
2402 validate_catalog_table_storage(&durable_root, &cat, meta_dek.as_ref())?;
2403 if !read_only {
2404 sweep_unreferenced_table_dirs(&root, &cat)?;
2405 }
2406 match durable_root.remove_directory_all(Path::new(META_DIR).join("backup-pins")) {
2407 Ok(()) => {}
2408 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
2409 Err(error) => return Err(error.into()),
2410 }
2411 }
2412
2413 let epoch = Arc::new(EpochAuthority::new(cat.db_epoch));
2414 let snapshots = Arc::new(SnapshotRegistry::new());
2415 snapshots.configure_history(history_epochs, history_start);
2416 let page_cache = Arc::new(crate::cache::Sharded::new(
2417 crate::cache::CACHE_SHARDS,
2418 || {
2419 crate::cache::PageCache::new(
2420 crate::engine::PAGE_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
2421 )
2422 },
2423 ));
2424 let decoded_cache = Arc::new(crate::cache::Sharded::new(
2425 crate::cache::CACHE_SHARDS,
2426 || {
2427 crate::cache::DecodedPageCache::new(
2428 crate::engine::DECODED_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
2429 )
2430 },
2431 ));
2432 let commit_lock = Arc::new(Mutex::new(()));
2433 let shared_wal = Arc::new(Mutex::new(if existing {
2434 crate::wal::SharedWal::open_durable_root_validated(
2435 Arc::clone(&durable_root),
2436 Epoch(cat.db_epoch),
2437 wal_dek.clone(),
2438 Some(&recovery_records),
2439 )?
2440 } else {
2441 crate::wal::SharedWal::create_with_durable_root(
2442 Arc::clone(&durable_root),
2443 Epoch(cat.db_epoch),
2444 wal_dek.clone(),
2445 )?
2446 }));
2447 let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));
2451 let group = Arc::new(crate::txn::GroupCommit::new(
2452 shared_wal.lock().durable_seq(),
2453 ));
2454 let (change_wake, _change_rx) = tokio::sync::broadcast::channel(256);
2455 let txn_ids = Arc::new(Mutex::new(1u64));
2459 let _ = abandoned;
2460
2461 let auth_state = crate::auth_state::AuthState::new(cat.require_auth, principal.clone());
2466 let security_coordinator = security_coordinator(&root, cat.security_version);
2467 let auth_checker: Option<Arc<dyn crate::auth_state::TableAuthChecker>> = Some(Arc::new(
2468 crate::auth_state::DefaultTableAuthChecker::new(auth_state.clone()),
2469 ));
2470
2471 let mut tables: HashMap<u64, TableHandle> = HashMap::new();
2477 for entry in &cat.tables {
2478 if !matches!(entry.state, TableState::Live) {
2479 continue;
2480 }
2481 let table_root = match table_roots.remove(&entry.table_id) {
2482 Some(root) => root,
2483 None => Arc::new(
2484 durable_root
2485 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))?,
2486 ),
2487 };
2488 let tdir = table_root.io_path()?;
2489 let ctx = SharedCtx {
2490 root_guard: Some(table_root),
2491 epoch: Arc::clone(&epoch),
2492 page_cache: Arc::clone(&page_cache),
2493 decoded_cache: Arc::clone(&decoded_cache),
2494 snapshots: Arc::clone(&snapshots),
2495 kek: kek.clone(),
2496 commit_lock: Arc::clone(&commit_lock),
2497 shared: Some(crate::engine::SharedWalCtx {
2498 wal: Arc::clone(&shared_wal),
2499 group: Arc::clone(&group),
2500 poisoned: Arc::clone(&poisoned),
2501 txn_ids: Arc::clone(&txn_ids),
2502 change_wake: change_wake.clone(),
2503 }),
2504 table_name: Some(entry.name.clone()),
2505 auth: auth_checker.clone(),
2506 read_only,
2507 };
2508 let t = Table::open_in(&tdir, ctx)?;
2509 tables.insert(entry.table_id, TableHandle::new(t));
2510 }
2511
2512 if existing {
2518 recover_shared_wal(&durable_root, &tables, &cat, &epoch, &recovery_records)?;
2519 reconcile_recovered_table_metadata(&tables, epoch.visible())?;
2520 if read_only {
2521 crate::replication::reconcile_replica_epoch_durable(
2522 &durable_root,
2523 epoch.visible().0,
2524 )?;
2525 }
2526 sweep_pending_txn_dirs(&root, &cat);
2529 }
2530
2531 catalog::write_generation(&durable_root, open_generation)?;
2533 shared_wal.lock().seal_open_generation(open_generation)?;
2534 crate::replication::replication_identity_durable(&durable_root)?;
2535 let next_txn_id = (open_generation << 32) | 1;
2536 *txn_ids.lock() = next_txn_id;
2538
2539 Ok(Self {
2540 root,
2541 durable_root,
2542 read_only,
2543 catalog: RwLock::new(cat),
2544 security_coordinator,
2545 security_catalog_disk_reads: AtomicU64::new(0),
2546 rls_cache: Mutex::new(RlsCache::default()),
2547 epoch,
2548 snapshots,
2549 page_cache,
2550 decoded_cache,
2551 commit_lock,
2552 shared_wal,
2553 next_txn_id: txn_ids,
2554 tables: RwLock::new(tables),
2555 kek,
2556 ddl_lock: Mutex::new(()),
2557 meta_dek,
2558 conflicts: crate::txn::ConflictIndex::new(),
2559 active_txns: crate::txn::ActiveTxns::new(),
2560 poisoned,
2561 group,
2562 spill_threshold: std::sync::atomic::AtomicU64::new(64 * 1024 * 1024),
2563 active_spills: Arc::new(crate::retention::ActiveSpills::new()),
2564 replication_barrier: parking_lot::RwLock::new(()),
2565 replication_wal_retention_segments: AtomicUsize::new(0),
2566 backup_pins: Arc::new(Mutex::new(HashMap::new())),
2567 spill_hook: Mutex::new(None),
2568 security_commit_hook: Mutex::new(None),
2569 catalog_commit_hook: Mutex::new(None),
2570 backup_hook: Mutex::new(None),
2571 replication_hook: Mutex::new(None),
2572 trigger_recursive: AtomicBool::new(TriggerConfig::default().recursive_triggers),
2573 trigger_max_depth: AtomicU32::new(TriggerConfig::default().max_depth),
2574 trigger_max_loop_iterations: AtomicU32::new(
2575 TriggerConfig::default().max_loop_iterations,
2576 ),
2577 _lock: Some(lock),
2578 notify: {
2579 let (tx, _rx) = tokio::sync::broadcast::channel(256);
2580 tx
2581 },
2582 change_wake,
2583 principal: RwLock::new(principal),
2584 auth_state,
2585 })
2586 }
2587
2588 pub fn visible_epoch(&self) -> Epoch {
2590 self.epoch.visible()
2591 }
2592
2593 pub fn catalog_snapshot(&self) -> Catalog {
2595 self.catalog.read().clone()
2596 }
2597
2598 pub fn sql_pragma_i64(&self, key: &str) -> Result<Option<i64>> {
2600 let catalog = self.catalog.read();
2601 match key {
2602 "user_version" => Ok(catalog.user_version),
2603 "application_id" => Ok(catalog.application_id),
2604 _ => Err(MongrelError::InvalidArgument(format!(
2605 "unsupported persistent SQL pragma {key:?}"
2606 ))),
2607 }
2608 }
2609
2610 pub fn set_sql_pragma_i64_with_epoch(&self, key: &str, value: i64) -> Result<Option<Epoch>> {
2613 self.set_sql_pragma_i64_with_epoch_inner(key, value, None)
2614 }
2615
2616 pub fn set_sql_pragma_i64_with_epoch_controlled<F>(
2617 &self,
2618 key: &str,
2619 value: i64,
2620 mut before_commit: F,
2621 ) -> Result<Option<Epoch>>
2622 where
2623 F: FnMut() -> Result<()>,
2624 {
2625 self.set_sql_pragma_i64_with_epoch_inner(key, value, Some(&mut before_commit))
2626 }
2627
2628 fn set_sql_pragma_i64_with_epoch_inner(
2629 &self,
2630 key: &str,
2631 value: i64,
2632 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
2633 ) -> Result<Option<Epoch>> {
2634 use crate::wal::DdlOp;
2635
2636 self.require(&crate::auth::Permission::Ddl)?;
2637 if self.read_only {
2638 return Err(MongrelError::ReadOnlyReplica);
2639 }
2640 if self.poisoned.load(Ordering::Relaxed) {
2641 return Err(MongrelError::Other(
2642 "database poisoned by fsync error".into(),
2643 ));
2644 }
2645 let _ddl = self.ddl_lock.lock();
2646 let _security_write = self.security_write()?;
2647 self.require(&crate::auth::Permission::Ddl)?;
2648 let mut next_catalog = self.catalog.read().clone();
2649 let target = match key {
2650 "user_version" => &mut next_catalog.user_version,
2651 "application_id" => &mut next_catalog.application_id,
2652 _ => {
2653 return Err(MongrelError::InvalidArgument(format!(
2654 "unsupported persistent SQL pragma {key:?}"
2655 )))
2656 }
2657 };
2658 if *target == Some(value) {
2659 return Ok(None);
2660 }
2661 *target = Some(value);
2662
2663 let _commit = self.commit_lock.lock();
2664 let epoch = self.epoch.bump_assigned();
2665 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2666 let txn_id = self.alloc_txn_id()?;
2667 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
2668 let commit_seq = {
2669 let mut wal = self.shared_wal.lock();
2670 if let Some(before_commit) = before_commit {
2671 before_commit()?;
2672 }
2673 let append: Result<u64> = (|| {
2674 wal.append(
2675 txn_id,
2676 WAL_TABLE_ID,
2677 crate::wal::Op::Ddl(DdlOp::SetSqlPragma {
2678 key: key.to_string(),
2679 value,
2680 }),
2681 )?;
2682 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
2683 wal.append_commit(txn_id, epoch, &[])
2684 })();
2685 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
2686 };
2687 self.await_durable_commit(commit_seq, epoch)?;
2688 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
2689 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
2690 Ok(Some(epoch))
2691 }
2692
2693 pub fn materialized_view(&self, name: &str) -> Option<crate::catalog::MaterializedViewEntry> {
2694 self.catalog
2695 .read()
2696 .materialized_views
2697 .iter()
2698 .find(|definition| definition.name == name)
2699 .cloned()
2700 }
2701
2702 pub fn materialized_views(&self) -> Vec<crate::catalog::MaterializedViewEntry> {
2703 self.catalog.read().materialized_views.clone()
2704 }
2705
2706 pub fn security_catalog(&self) -> crate::security::SecurityCatalog {
2707 self.catalog.read().security.clone()
2708 }
2709
2710 pub fn security_active_for(&self, table: &str) -> bool {
2711 self.catalog.read().security.table_has_security(table)
2712 }
2713
2714 fn refresh_security_catalog_if_stale(&self, expected_version: u64) -> Result<()> {
2715 if self.catalog.read().security_version == expected_version {
2716 return Ok(());
2717 }
2718 self.security_catalog_disk_reads
2719 .fetch_add(1, Ordering::Relaxed);
2720 let fresh = catalog::read_durable(&self.durable_root, self.meta_dek.as_ref())?
2721 .ok_or_else(|| MongrelError::NotFound("catalog vanished during write".into()))?;
2722 let principal = self.principal.read().clone();
2723 let principal = if fresh.require_auth {
2724 principal
2725 .as_ref()
2726 .and_then(|principal| Self::resolve_bound_principal_from_catalog(&fresh, principal))
2727 } else {
2728 principal
2729 };
2730 self.auth_state.set_require_auth(fresh.require_auth);
2731 *self.catalog.write() = fresh;
2732 *self.principal.write() = principal.clone();
2733 self.auth_state.set_principal(principal);
2734 Ok(())
2735 }
2736
2737 fn security_write(&self) -> Result<parking_lot::RwLockWriteGuard<'_, ()>> {
2738 let guard = self.security_coordinator.gate.write();
2739 let version = self.security_coordinator.version.load(Ordering::Acquire);
2740 self.refresh_security_catalog_if_stale(version)?;
2741 Ok(guard)
2742 }
2743
2744 fn publish_catalog_candidate(
2748 &self,
2749 catalog: Catalog,
2750 epoch: Epoch,
2751 epoch_guard: &mut EpochGuard<'_>,
2752 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
2753 ) -> Result<()> {
2754 self.publish_catalog_candidate_with_prelude(
2755 catalog,
2756 epoch,
2757 epoch_guard,
2758 before_publish,
2759 Vec::new(),
2760 )
2761 }
2762
2763 fn publish_catalog_candidate_with_prelude(
2764 &self,
2765 catalog: Catalog,
2766 epoch: Epoch,
2767 epoch_guard: &mut EpochGuard<'_>,
2768 mut before_publish: Option<&mut dyn FnMut() -> Result<()>>,
2769 prelude: Vec<(u64, crate::wal::Op)>,
2770 ) -> Result<()> {
2771 use crate::wal::DdlOp;
2772
2773 if self.read_only {
2774 return Err(MongrelError::ReadOnlyReplica);
2775 }
2776 if self.poisoned.load(Ordering::Relaxed) {
2777 return Err(MongrelError::Other(
2778 "database poisoned by fsync error".into(),
2779 ));
2780 }
2781 if let Some(before_publish) = before_publish.as_mut() {
2782 (**before_publish)()?;
2783 }
2784 if catalog.db_epoch != epoch.0 {
2785 return Err(MongrelError::InvalidArgument(format!(
2786 "catalog epoch {} does not match commit epoch {}",
2787 catalog.db_epoch, epoch.0
2788 )));
2789 }
2790 {
2791 let current = self.catalog.read();
2792 validate_catalog_transition(¤t, &catalog)?;
2793 }
2794 validate_recovered_catalog(&catalog)?;
2795 let catalog_json = DdlOp::encode_catalog(&catalog)?;
2796 let txn_id = self.alloc_txn_id()?;
2797 let commit_seq = {
2798 let mut wal = self.shared_wal.lock();
2799 let append: Result<u64> = (|| {
2800 for (table_id, op) in prelude {
2801 wal.append(txn_id, table_id, op)?;
2802 }
2803 wal.append(
2804 txn_id,
2805 WAL_TABLE_ID,
2806 crate::wal::Op::Ddl(DdlOp::CatalogSnapshot { catalog_json }),
2807 )?;
2808 wal.append_commit(txn_id, epoch, &[])
2809 })();
2810 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
2811 };
2812 self.await_durable_commit(commit_seq, epoch)?;
2813 let checkpoint = self.checkpoint_catalog_after_durable(catalog);
2814 self.finish_durable_publish(epoch, epoch_guard, checkpoint)
2815 }
2816
2817 fn checkpoint_catalog_after_durable(&self, catalog: Catalog) -> Result<()> {
2821 let checkpoint = catalog::write_atomic(&self.root, &catalog, self.meta_dek.as_ref());
2822 let version = catalog.security_version;
2823 let principal = self.principal.read().clone();
2824 let principal = if catalog.require_auth {
2825 principal.as_ref().and_then(|principal| {
2826 Self::resolve_bound_principal_from_catalog(&catalog, principal)
2827 })
2828 } else {
2829 principal
2830 };
2831 *self.catalog.write() = catalog;
2832 self.security_coordinator
2833 .version
2834 .store(version, Ordering::Release);
2835 self.auth_state
2836 .set_require_auth(self.catalog.read().require_auth);
2837 *self.principal.write() = principal.clone();
2838 self.auth_state.set_principal(principal);
2839 checkpoint
2840 }
2841
2842 fn finish_durable_publish(
2843 &self,
2844 epoch: Epoch,
2845 epoch_guard: &mut EpochGuard<'_>,
2846 post_step: Result<()>,
2847 ) -> Result<()> {
2848 self.epoch.publish_in_order(epoch);
2849 epoch_guard.disarm();
2850 match post_step {
2851 Ok(()) => Ok(()),
2852 Err(error) => {
2853 self.poisoned.store(true, Ordering::Relaxed);
2854 Err(MongrelError::DurableCommit {
2855 epoch: epoch.0,
2856 message: error.to_string(),
2857 })
2858 }
2859 }
2860 }
2861
2862 fn await_durable_commit(&self, commit_seq: u64, epoch: Epoch) -> Result<()> {
2866 match self.group.await_durable(&self.shared_wal, commit_seq) {
2867 Ok(()) => Ok(()),
2868 Err(error) => {
2869 self.poisoned.store(true, Ordering::Relaxed);
2870 Err(MongrelError::CommitOutcomeUnknown {
2871 epoch: epoch.0,
2872 message: error.to_string(),
2873 })
2874 }
2875 }
2876 }
2877
2878 fn commit_outcome_unknown(&self, epoch: Epoch, error: impl std::fmt::Display) -> MongrelError {
2879 self.poisoned.store(true, Ordering::Relaxed);
2880 MongrelError::CommitOutcomeUnknown {
2881 epoch: epoch.0,
2882 message: error.to_string(),
2883 }
2884 }
2885
2886 pub fn set_security_catalog(&self, security: crate::security::SecurityCatalog) -> Result<()> {
2888 self.set_security_catalog_as_with_epoch(security, None)
2889 .map(|_| ())
2890 }
2891
2892 pub fn set_security_catalog_as(
2894 &self,
2895 security: crate::security::SecurityCatalog,
2896 principal: Option<&crate::auth::Principal>,
2897 ) -> Result<()> {
2898 self.set_security_catalog_as_with_epoch(security, principal)
2899 .map(|_| ())
2900 }
2901
2902 pub fn set_security_catalog_as_with_epoch(
2904 &self,
2905 security: crate::security::SecurityCatalog,
2906 principal: Option<&crate::auth::Principal>,
2907 ) -> Result<Epoch> {
2908 self.set_security_catalog_as_with_epoch_inner(security, principal, None)
2909 }
2910
2911 pub fn set_security_catalog_as_with_epoch_controlled<F>(
2914 &self,
2915 security: crate::security::SecurityCatalog,
2916 principal: Option<&crate::auth::Principal>,
2917 mut before_commit: F,
2918 ) -> Result<Epoch>
2919 where
2920 F: FnMut() -> Result<()>,
2921 {
2922 self.set_security_catalog_as_with_epoch_inner(security, principal, Some(&mut before_commit))
2923 }
2924
2925 fn set_security_catalog_as_with_epoch_inner(
2926 &self,
2927 security: crate::security::SecurityCatalog,
2928 principal: Option<&crate::auth::Principal>,
2929 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
2930 ) -> Result<Epoch> {
2931 use crate::wal::DdlOp;
2932 use std::sync::atomic::Ordering;
2933
2934 self.require_for(principal, &crate::auth::Permission::Admin)?;
2935 if self.poisoned.load(Ordering::Relaxed) {
2936 return Err(MongrelError::Other(
2937 "database poisoned by fsync error".into(),
2938 ));
2939 }
2940 let _ddl = self.ddl_lock.lock();
2941 let _security_write = self.security_write()?;
2944 self.require_for(principal, &crate::auth::Permission::Admin)?;
2945 let mut next_catalog = self.catalog.read().clone();
2946 validate_security_catalog(&next_catalog, &security)?;
2947 let payload = DdlOp::encode_security(&security)?;
2948 let _commit = self.commit_lock.lock();
2949 let epoch = self.epoch.bump_assigned();
2950 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2951 let txn_id = self.alloc_txn_id()?;
2952 next_catalog.security = security;
2953 advance_security_version(&mut next_catalog)?;
2954 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
2955 let commit_seq = {
2956 let mut wal = self.shared_wal.lock();
2957 if let Some(before_commit) = before_commit {
2958 before_commit()?;
2959 }
2960 let append: Result<u64> = (|| {
2961 wal.append(
2962 txn_id,
2963 WAL_TABLE_ID,
2964 crate::wal::Op::Ddl(DdlOp::SetSecurityCatalog {
2965 security_json: payload,
2966 }),
2967 )?;
2968 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
2969 wal.append_commit(txn_id, epoch, &[])
2970 })();
2971 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
2972 };
2973 self.await_durable_commit(commit_seq, epoch)?;
2974 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
2975 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
2976 Ok(epoch)
2977 }
2978
2979 pub fn require_for(
2980 &self,
2981 principal: Option<&crate::auth::Principal>,
2982 permission: &crate::auth::Permission,
2983 ) -> Result<()> {
2984 let Some(principal) = principal else {
2985 return self.require(permission);
2986 };
2987 let resolved;
2988 let principal = if self.auth_state.require_auth() || principal.user_id != 0 {
2989 resolved = Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
2990 .ok_or(MongrelError::AuthRequired)?;
2991 &resolved
2992 } else {
2993 principal
2994 };
2995 #[cfg(test)]
2996 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
2997 if principal.has_permission(permission) {
2998 Ok(())
2999 } else {
3000 Err(MongrelError::PermissionDenied {
3001 required: permission.clone(),
3002 principal: principal.username.clone(),
3003 })
3004 }
3005 }
3006
3007 fn require_exact_principal_current(
3011 &self,
3012 principal: Option<&crate::auth::Principal>,
3013 permission: &crate::auth::Permission,
3014 ) -> Result<()> {
3015 let catalog = self.catalog.read();
3016 if !catalog.require_auth {
3017 return Ok(());
3018 }
3019 let supplied = principal.ok_or(MongrelError::AuthRequired)?;
3020 let current = Self::resolve_bound_principal_from_catalog(&catalog, supplied)
3021 .ok_or(MongrelError::AuthRequired)?;
3022 if current.has_permission(permission) {
3023 Ok(())
3024 } else {
3025 Err(MongrelError::PermissionDenied {
3026 required: permission.clone(),
3027 principal: current.username,
3028 })
3029 }
3030 }
3031
3032 pub(crate) fn with_exact_principal_current<T, F>(
3033 &self,
3034 principal: Option<&crate::auth::Principal>,
3035 permission: &crate::auth::Permission,
3036 operation: F,
3037 ) -> Result<T>
3038 where
3039 F: FnOnce() -> Result<T>,
3040 {
3041 let _security = self.security_coordinator.gate.read();
3042 self.require_exact_principal_current(principal, permission)?;
3043 operation()
3044 }
3045
3046 pub fn principal_snapshot(&self) -> Option<crate::auth::Principal> {
3047 self.principal.read().clone()
3048 }
3049
3050 #[cfg(test)]
3051 pub(crate) fn set_cached_principal_for_test(&self, principal: Option<crate::auth::Principal>) {
3052 *self.principal.write() = principal.clone();
3053 self.auth_state.set_principal(principal);
3054 }
3055
3056 pub fn require_columns_for(
3057 &self,
3058 table: &str,
3059 operation: crate::auth::ColumnOperation,
3060 column_ids: &[u16],
3061 principal: Option<&crate::auth::Principal>,
3062 ) -> Result<()> {
3063 if principal.is_none() && !self.auth_state.require_auth() {
3064 return Ok(());
3065 }
3066 let cached = self.principal.read().clone();
3067 let principal = principal.or(cached.as_ref());
3068 let Some(principal) = principal else {
3069 let permission = match operation {
3070 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
3071 table: table.to_string(),
3072 },
3073 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
3074 table: table.to_string(),
3075 },
3076 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
3077 table: table.to_string(),
3078 },
3079 };
3080 return self.require(&permission);
3081 };
3082 let catalog = self.catalog.read();
3083 let resolved;
3084 let principal = if catalog.require_auth || principal.user_id != 0 {
3085 resolved = Self::resolve_bound_principal_from_catalog(&catalog, principal)
3086 .ok_or(MongrelError::AuthRequired)?;
3087 &resolved
3088 } else {
3089 principal
3090 };
3091 let schema = &catalog
3092 .live(table)
3093 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
3094 .schema;
3095 Self::require_columns_for_principal(table, schema, operation, column_ids, principal)
3096 }
3097
3098 fn require_columns_for_principal(
3099 table: &str,
3100 schema: &Schema,
3101 operation: crate::auth::ColumnOperation,
3102 column_ids: &[u16],
3103 principal: &crate::auth::Principal,
3104 ) -> Result<()> {
3105 #[cfg(test)]
3106 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
3107 match principal.column_access(table, operation) {
3108 crate::auth::ColumnAccess::All => Ok(()),
3109 crate::auth::ColumnAccess::Columns(allowed) => {
3110 let denied = column_ids.iter().find_map(|column_id| {
3111 schema
3112 .columns
3113 .iter()
3114 .find(|column| column.id == *column_id)
3115 .filter(|column| !allowed.contains(&column.name))
3116 });
3117 if denied.is_none() {
3118 Ok(())
3119 } else {
3120 Err(MongrelError::PermissionDenied {
3121 required: match operation {
3122 crate::auth::ColumnOperation::Select => {
3123 crate::auth::Permission::SelectColumns {
3124 table: table.to_string(),
3125 columns: denied
3126 .into_iter()
3127 .map(|column| column.name.clone())
3128 .collect(),
3129 }
3130 }
3131 crate::auth::ColumnOperation::Insert => {
3132 crate::auth::Permission::InsertColumns {
3133 table: table.to_string(),
3134 columns: denied
3135 .into_iter()
3136 .map(|column| column.name.clone())
3137 .collect(),
3138 }
3139 }
3140 crate::auth::ColumnOperation::Update => {
3141 crate::auth::Permission::UpdateColumns {
3142 table: table.to_string(),
3143 columns: denied
3144 .into_iter()
3145 .map(|column| column.name.clone())
3146 .collect(),
3147 }
3148 }
3149 },
3150 principal: principal.username.clone(),
3151 })
3152 }
3153 }
3154 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
3155 required: match operation {
3156 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
3157 table: table.to_string(),
3158 },
3159 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
3160 table: table.to_string(),
3161 },
3162 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
3163 table: table.to_string(),
3164 },
3165 },
3166 principal: principal.username.clone(),
3167 }),
3168 }
3169 }
3170
3171 pub fn select_column_ids_for(
3172 &self,
3173 table: &str,
3174 principal: Option<&crate::auth::Principal>,
3175 ) -> Result<Vec<u16>> {
3176 let catalog = self.catalog.read();
3177 let columns = catalog
3178 .live(table)
3179 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
3180 .schema
3181 .columns
3182 .iter()
3183 .map(|column| (column.id, column.name.clone()))
3184 .collect::<Vec<_>>();
3185 let principal = self.principal_for_authorized_read(&catalog, principal, false)?;
3186 drop(catalog);
3187 let Some(principal) = principal.as_ref() else {
3188 self.require(&crate::auth::Permission::Select {
3189 table: table.to_string(),
3190 })?;
3191 return Ok(columns.iter().map(|(id, _)| *id).collect());
3192 };
3193 match principal.column_access(table, crate::auth::ColumnOperation::Select) {
3194 crate::auth::ColumnAccess::All => Ok(columns.iter().map(|(id, _)| *id).collect()),
3195 crate::auth::ColumnAccess::Columns(allowed) => Ok(columns
3196 .iter()
3197 .filter(|(_, name)| allowed.contains(name))
3198 .map(|(id, _)| *id)
3199 .collect()),
3200 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
3201 required: crate::auth::Permission::Select {
3202 table: table.to_string(),
3203 },
3204 principal: principal.username.clone(),
3205 }),
3206 }
3207 }
3208
3209 pub fn secure_rows_for(
3210 &self,
3211 table: &str,
3212 rows: Vec<crate::memtable::Row>,
3213 principal: Option<&crate::auth::Principal>,
3214 ) -> Result<Vec<crate::memtable::Row>> {
3215 self.secure_rows_for_with_context(table, rows, principal, None)
3216 }
3217
3218 pub fn secure_rows_for_with_context(
3219 &self,
3220 table: &str,
3221 rows: Vec<crate::memtable::Row>,
3222 principal: Option<&crate::auth::Principal>,
3223 context: Option<&crate::query::AiExecutionContext>,
3224 ) -> Result<Vec<crate::memtable::Row>> {
3225 let (security, principal) = {
3226 let catalog = self.catalog.read();
3227 (
3228 catalog.security.clone(),
3229 self.principal_for_authorized_read(&catalog, principal, false)?,
3230 )
3231 };
3232 if !security.table_has_security(table) {
3233 return Ok(rows);
3234 }
3235 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
3236 let mut output = Vec::new();
3237 for mut row in rows {
3238 if let Some(context) = context {
3239 context.consume(1)?;
3240 }
3241 if security.row_allowed(
3242 table,
3243 crate::security::PolicyCommand::Select,
3244 &row,
3245 principal,
3246 false,
3247 ) {
3248 security.apply_masks(table, &mut row, principal);
3249 output.push(row);
3250 }
3251 }
3252 Ok(output)
3253 }
3254
3255 pub fn mask_search_hits_for(
3258 &self,
3259 table: &str,
3260 hits: &mut [crate::query::SearchHit],
3261 principal: Option<&crate::auth::Principal>,
3262 ) -> Result<()> {
3263 let (security, principal) = {
3264 let catalog = self.catalog.read();
3265 (
3266 catalog.security.clone(),
3267 self.principal_for_authorized_read(&catalog, principal, false)?,
3268 )
3269 };
3270 if !security.table_has_security(table) {
3271 return Ok(());
3272 }
3273 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
3274 for hit in hits {
3275 security.apply_masks_to_cells(table, &mut hit.cells, principal);
3276 }
3277 Ok(())
3278 }
3279
3280 pub fn mask_rows_for(
3282 &self,
3283 table: &str,
3284 rows: &mut [crate::memtable::Row],
3285 principal: Option<&crate::auth::Principal>,
3286 ) -> Result<()> {
3287 let (security, principal) = {
3288 let catalog = self.catalog.read();
3289 (
3290 catalog.security.clone(),
3291 self.principal_for_authorized_read(&catalog, principal, false)?,
3292 )
3293 };
3294 if !security.table_has_security(table) {
3295 return Ok(());
3296 }
3297 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
3298 for row in rows {
3299 security.apply_masks(table, row, principal);
3300 }
3301 Ok(())
3302 }
3303
3304 pub fn authorized_candidate_ids_for(
3306 &self,
3307 table: &str,
3308 principal: Option<&crate::auth::Principal>,
3309 ) -> Result<Option<std::collections::HashSet<RowId>>> {
3310 Ok(self
3311 .authorized_read_snapshot(table, principal)?
3312 .allowed_row_ids)
3313 }
3314
3315 fn allowed_row_ids_locked(
3316 &self,
3317 table_name: &str,
3318 table: &Table,
3319 table_snapshot: Snapshot,
3320 security_state: (&crate::security::SecurityCatalog, u64),
3321 principal: Option<&crate::auth::Principal>,
3322 context: Option<&crate::query::AiExecutionContext>,
3323 ) -> Result<Option<Arc<HashSet<RowId>>>> {
3324 let (security, security_version) = security_state;
3325 if !security.rls_enabled(table_name) {
3326 return Ok(None);
3327 }
3328 let authorization_started = std::time::Instant::now();
3329 let principal = principal.ok_or(MongrelError::AuthRequired)?;
3330 let mut roles = principal.roles.clone();
3331 roles.sort_unstable();
3332 let principal_key = format!(
3333 "{}:{}:{}:{}:{roles:?}",
3334 principal.user_id, principal.created_epoch, principal.username, principal.is_admin
3335 );
3336 let cache_key = (
3337 table_name.to_string(),
3338 table.data_generation(),
3339 security_version,
3340 principal_key,
3341 );
3342 if let Some(allowed) = self.rls_cache.lock().get(&cache_key) {
3343 crate::trace::QueryTrace::record(|trace| {
3344 trace.rls_cache_hit = true;
3345 trace.authorization_nanos = trace
3346 .authorization_nanos
3347 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
3348 });
3349 return Ok(Some(allowed));
3350 }
3351 if let Some(context) = context {
3352 context.checkpoint()?;
3353 }
3354 let started = std::time::Instant::now();
3356 let rows = table.visible_rows(table_snapshot)?;
3357 let rows_evaluated = rows.len() as u64;
3358 let mut allowed = HashSet::new();
3359 for chunk in rows.chunks(256) {
3360 if let Some(context) = context {
3361 context.consume(chunk.len())?;
3362 }
3363 allowed.extend(chunk.iter().filter_map(|row| {
3364 security
3365 .row_allowed(
3366 table_name,
3367 crate::security::PolicyCommand::Select,
3368 row,
3369 principal,
3370 false,
3371 )
3372 .then_some(row.row_id)
3373 }));
3374 }
3375 let allowed = Arc::new(allowed);
3376 let mut cache = self.rls_cache.lock();
3377 cache.build_nanos = cache
3378 .build_nanos
3379 .saturating_add(started.elapsed().as_nanos() as u64);
3380 cache.rows_evaluated = cache.rows_evaluated.saturating_add(rows_evaluated);
3381 cache.insert(cache_key, Arc::clone(&allowed));
3382 crate::trace::QueryTrace::record(|trace| {
3383 trace.rls_rows_evaluated = trace
3384 .rls_rows_evaluated
3385 .saturating_add(rows_evaluated as usize);
3386 trace.authorization_nanos = trace
3387 .authorization_nanos
3388 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
3389 });
3390 Ok(Some(allowed))
3391 }
3392
3393 fn principal_for_authorized_read(
3394 &self,
3395 catalog: &Catalog,
3396 principal: Option<&crate::auth::Principal>,
3397 catalog_bound: bool,
3398 ) -> Result<Option<crate::auth::Principal>> {
3399 let principal = principal.cloned().or_else(|| self.principal.read().clone());
3400 let Some(principal) = principal else {
3401 return Ok(None);
3402 };
3403 if catalog.require_auth || catalog_bound || principal.user_id != 0 {
3404 return Self::resolve_bound_principal_from_catalog(catalog, &principal)
3405 .map(Some)
3406 .ok_or(MongrelError::AuthRequired);
3407 }
3408 Ok(Some(principal))
3409 }
3410
3411 pub fn with_authorized_read<T, F>(
3415 &self,
3416 table_name: &str,
3417 principal: Option<&crate::auth::Principal>,
3418 catalog_bound: bool,
3419 read: F,
3420 ) -> Result<T>
3421 where
3422 F: FnMut(
3423 &mut Table,
3424 Snapshot,
3425 Option<&HashSet<RowId>>,
3426 Option<&crate::auth::Principal>,
3427 ) -> Result<T>,
3428 {
3429 self.with_authorized_read_context(
3430 table_name,
3431 principal,
3432 catalog_bound,
3433 None,
3434 None,
3435 None,
3436 read,
3437 )
3438 }
3439
3440 #[allow(clippy::too_many_arguments)]
3441 pub fn with_authorized_read_context<T, F>(
3442 &self,
3443 table_name: &str,
3444 principal: Option<&crate::auth::Principal>,
3445 catalog_bound: bool,
3446 authorization: Option<&ReadAuthorization>,
3447 context: Option<&crate::query::AiExecutionContext>,
3448 snapshot_override: Option<Snapshot>,
3449 read: F,
3450 ) -> Result<T>
3451 where
3452 F: FnMut(
3453 &mut Table,
3454 Snapshot,
3455 Option<&HashSet<RowId>>,
3456 Option<&crate::auth::Principal>,
3457 ) -> Result<T>,
3458 {
3459 self.with_authorized_read_context_stamped(
3460 table_name,
3461 principal,
3462 catalog_bound,
3463 authorization,
3464 context,
3465 snapshot_override,
3466 read,
3467 )
3468 .map(|(result, _)| result)
3469 }
3470
3471 #[allow(clippy::too_many_arguments)]
3472 pub fn with_authorized_read_context_stamped<T, F>(
3473 &self,
3474 table_name: &str,
3475 principal: Option<&crate::auth::Principal>,
3476 catalog_bound: bool,
3477 authorization: Option<&ReadAuthorization>,
3478 context: Option<&crate::query::AiExecutionContext>,
3479 snapshot_override: Option<Snapshot>,
3480 mut read: F,
3481 ) -> Result<(T, AuthorizedReadStamp)>
3482 where
3483 F: FnMut(
3484 &mut Table,
3485 Snapshot,
3486 Option<&HashSet<RowId>>,
3487 Option<&crate::auth::Principal>,
3488 ) -> Result<T>,
3489 {
3490 if principal.is_none() && self.principal.read().is_some() {
3491 self.refresh_principal()?;
3492 }
3493 const RETRIES: usize = 3;
3494 let handle = self.table(table_name)?;
3495 for attempt in 0..RETRIES {
3496 crate::trace::QueryTrace::record(|trace| {
3497 trace.authorization_retries = attempt;
3498 });
3499 let (security, security_version, effective_principal) = {
3500 let catalog = self.catalog.read();
3501 (
3502 catalog.security.clone(),
3503 catalog.security_version,
3504 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
3505 )
3506 };
3507 if let Some(authorization) = authorization {
3508 for permission in &authorization.permissions {
3509 self.require_for(effective_principal.as_ref(), permission)?;
3510 }
3511 self.require_columns_for(
3512 table_name,
3513 authorization.operation,
3514 &authorization.columns,
3515 effective_principal.as_ref(),
3516 )?;
3517 }
3518 let result = {
3519 let mut table = lock_table_with_context(&handle, context)?;
3520 let snapshot = snapshot_override.unwrap_or_else(|| table.snapshot());
3521 let allowed = self.allowed_row_ids_locked(
3522 table_name,
3523 &table,
3524 snapshot,
3525 (&security, security_version),
3526 effective_principal.as_ref(),
3527 context,
3528 )?;
3529 let stamp = AuthorizedReadStamp {
3530 table_id: table.table_id(),
3531 schema_id: table.schema().schema_id,
3532 data_generation: table.data_generation(),
3533 security_version,
3534 snapshot,
3535 };
3536 let result = read(
3537 &mut table,
3538 snapshot,
3539 allowed.as_deref(),
3540 effective_principal.as_ref(),
3541 )?;
3542 (result, stamp)
3543 };
3544 if let Some(context) = context {
3545 context.checkpoint()?;
3546 }
3547 if self.catalog.read().security_version == security_version {
3548 return Ok(result);
3549 }
3550 if attempt + 1 == RETRIES {
3551 return Err(MongrelError::Conflict(
3552 "security policy changed during scored read".into(),
3553 ));
3554 }
3555 }
3556 Err(MongrelError::Conflict(
3557 "authorization retry loop exhausted".into(),
3558 ))
3559 }
3560
3561 fn with_authorized_aggregate_table<T, F>(
3562 &self,
3563 table_name: &str,
3564 columns: &[u16],
3565 principal: Option<&crate::auth::Principal>,
3566 catalog_bound: bool,
3567 allow_table_security: bool,
3568 mut aggregate: F,
3569 ) -> Result<T>
3570 where
3571 F: FnMut(
3572 &mut Table,
3573 Option<&crate::security::CandidateAuthorization<'_>>,
3574 Option<&crate::auth::Principal>,
3575 u64,
3576 ) -> Result<T>,
3577 {
3578 if principal.is_none() && self.principal.read().is_some() {
3579 self.refresh_principal()?;
3580 }
3581 const RETRIES: usize = 3;
3582 let handle = self.table(table_name)?;
3583 for attempt in 0..RETRIES {
3584 let (security, security_version, effective_principal) = {
3585 let catalog = self.catalog.read();
3586 (
3587 catalog.security.clone(),
3588 catalog.security_version,
3589 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
3590 )
3591 };
3592 self.require_columns_for(
3593 table_name,
3594 crate::auth::ColumnOperation::Select,
3595 columns,
3596 effective_principal.as_ref(),
3597 )?;
3598 if !allow_table_security && security.table_has_security(table_name) {
3599 return Err(MongrelError::InvalidArgument(
3600 "incremental aggregate is unsupported while RLS or column masks are active"
3601 .into(),
3602 ));
3603 }
3604 let result = {
3605 let mut table = handle.lock();
3606 let authorization = if security.rls_enabled(table_name) {
3607 Some(crate::security::CandidateAuthorization {
3608 table: table_name,
3609 security: &security,
3610 principal: effective_principal
3611 .as_ref()
3612 .ok_or(MongrelError::AuthRequired)?,
3613 })
3614 } else {
3615 None
3616 };
3617 aggregate(
3618 &mut table,
3619 authorization.as_ref(),
3620 effective_principal.as_ref(),
3621 security_version,
3622 )?
3623 };
3624 if self.catalog.read().security_version == security_version {
3625 return Ok(result);
3626 }
3627 if attempt + 1 == RETRIES {
3628 return Err(MongrelError::Conflict(
3629 "security policy changed during aggregate read".into(),
3630 ));
3631 }
3632 }
3633 Err(MongrelError::Conflict(
3634 "aggregate authorization retry loop exhausted".into(),
3635 ))
3636 }
3637
3638 pub fn with_authorized_scored_read_context<T, F>(
3642 &self,
3643 table_name: &str,
3644 principal: Option<&crate::auth::Principal>,
3645 catalog_bound: bool,
3646 authorization: Option<&ReadAuthorization>,
3647 context: Option<&crate::query::AiExecutionContext>,
3648 mut read: F,
3649 ) -> Result<T>
3650 where
3651 F: FnMut(
3652 &mut Table,
3653 Snapshot,
3654 Option<&crate::security::CandidateAuthorization<'_>>,
3655 Option<&crate::auth::Principal>,
3656 ) -> Result<T>,
3657 {
3658 self.with_authorized_scored_read_context_at(
3659 table_name,
3660 principal,
3661 catalog_bound,
3662 authorization,
3663 context,
3664 None,
3665 |table, snapshot, authorization, principal| {
3666 let mut table = table.clone();
3667 read(&mut table, snapshot, authorization, principal)
3668 },
3669 )
3670 }
3671
3672 #[allow(clippy::too_many_arguments)]
3673 pub fn with_authorized_scored_read_context_at<T, F>(
3674 &self,
3675 table_name: &str,
3676 principal: Option<&crate::auth::Principal>,
3677 catalog_bound: bool,
3678 authorization: Option<&ReadAuthorization>,
3679 context: Option<&crate::query::AiExecutionContext>,
3680 snapshot_override: Option<Snapshot>,
3681 read: F,
3682 ) -> Result<T>
3683 where
3684 F: FnMut(
3685 &Table,
3686 Snapshot,
3687 Option<&crate::security::CandidateAuthorization<'_>>,
3688 Option<&crate::auth::Principal>,
3689 ) -> Result<T>,
3690 {
3691 self.with_authorized_scored_read_context_at_stamped(
3692 table_name,
3693 principal,
3694 catalog_bound,
3695 authorization,
3696 context,
3697 snapshot_override,
3698 read,
3699 )
3700 .map(|(result, _)| result)
3701 }
3702
3703 #[allow(clippy::too_many_arguments)]
3704 pub fn with_authorized_scored_read_context_at_stamped<T, F>(
3705 &self,
3706 table_name: &str,
3707 principal: Option<&crate::auth::Principal>,
3708 catalog_bound: bool,
3709 authorization: Option<&ReadAuthorization>,
3710 context: Option<&crate::query::AiExecutionContext>,
3711 snapshot_override: Option<Snapshot>,
3712 mut read: F,
3713 ) -> Result<(T, AuthorizedReadStamp)>
3714 where
3715 F: FnMut(
3716 &Table,
3717 Snapshot,
3718 Option<&crate::security::CandidateAuthorization<'_>>,
3719 Option<&crate::auth::Principal>,
3720 ) -> Result<T>,
3721 {
3722 if principal.is_none() && self.principal.read().is_some() {
3723 self.refresh_principal()?;
3724 }
3725 const RETRIES: usize = 3;
3726 let handle = self.table(table_name)?;
3727 for attempt in 0..RETRIES {
3728 if let Some(context) = context {
3729 context.checkpoint()?;
3730 }
3731 crate::trace::QueryTrace::record(|trace| {
3732 trace.authorization_retries = attempt;
3733 });
3734 let (security, security_version, effective_principal) = {
3735 let catalog = self.catalog.read();
3736 (
3737 catalog.security.clone(),
3738 catalog.security_version,
3739 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
3740 )
3741 };
3742 if let Some(authorization) = authorization {
3743 for permission in &authorization.permissions {
3744 self.require_for(effective_principal.as_ref(), permission)?;
3745 }
3746 self.require_columns_for(
3747 table_name,
3748 authorization.operation,
3749 &authorization.columns,
3750 effective_principal.as_ref(),
3751 )?;
3752 }
3753 let result = {
3754 let (table, snapshot, _snapshot_guard, _run_pins) =
3755 self.scored_read_generation(&handle, context, snapshot_override)?;
3756 let candidate_authorization = if security.rls_enabled(table_name) {
3757 Some(crate::security::CandidateAuthorization {
3758 table: table_name,
3759 security: &security,
3760 principal: effective_principal
3761 .as_ref()
3762 .ok_or(MongrelError::AuthRequired)?,
3763 })
3764 } else {
3765 None
3766 };
3767 let stamp = AuthorizedReadStamp {
3768 table_id: table.table_id(),
3769 schema_id: table.schema().schema_id,
3770 data_generation: table.data_generation(),
3771 security_version,
3772 snapshot,
3773 };
3774 let result = read(
3775 table.as_ref(),
3776 snapshot,
3777 candidate_authorization.as_ref(),
3778 effective_principal.as_ref(),
3779 )?;
3780 (result, stamp)
3781 };
3782 if let Some(context) = context {
3783 context.checkpoint()?;
3784 }
3785 if self.catalog.read().security_version == security_version {
3786 return Ok(result);
3787 }
3788 if attempt + 1 == RETRIES {
3789 return Err(MongrelError::Conflict(
3790 "security policy changed during scored read".into(),
3791 ));
3792 }
3793 }
3794 Err(MongrelError::Conflict(
3795 "scored-read authorization retry loop exhausted".into(),
3796 ))
3797 }
3798
3799 fn scored_read_generation(
3800 &self,
3801 handle: &TableHandle,
3802 context: Option<&crate::query::AiExecutionContext>,
3803 snapshot_override: Option<Snapshot>,
3804 ) -> Result<(
3805 Arc<TableReadGeneration>,
3806 Snapshot,
3807 crate::retention::OwnedSnapshotGuard,
3808 RunPins,
3809 )> {
3810 let mut table = if let Some(context) = context {
3811 loop {
3812 context.checkpoint()?;
3813 let wait = context
3814 .remaining_duration()
3815 .unwrap_or(std::time::Duration::from_millis(5))
3816 .min(std::time::Duration::from_millis(5));
3817 if let Some(table) = handle.try_lock_for(wait) {
3818 break table;
3819 }
3820 }
3821 } else {
3822 handle.lock()
3823 };
3824 let (snapshot, snapshot_guard) = if let Some(snapshot) = snapshot_override {
3825 self.snapshot_at_owned(snapshot.epoch)?
3826 } else {
3827 let snapshot = table.snapshot();
3828 let guard = self.snapshots.register_owned(snapshot.epoch);
3829 (snapshot, guard)
3830 };
3831 let table_id = table.table_id();
3832 let run_keys: Vec<_> = table
3833 .active_run_ids()
3834 .map(|run_id| (table_id, run_id))
3835 .collect();
3836 let generation = handle
3837 .generation_metrics
3838 .activate(table.clone_read_generation()?);
3839 let run_pins = self.pin_runs(&run_keys);
3840 Ok((generation, snapshot, snapshot_guard, run_pins))
3841 }
3842
3843 fn pin_runs(&self, runs: &[(u64, u128)]) -> RunPins {
3844 let mut pins = self.backup_pins.lock();
3845 for run in runs {
3846 *pins.entry(*run).or_insert(0) += 1;
3847 }
3848 drop(pins);
3849 RunPins {
3850 pins: Arc::clone(&self.backup_pins),
3851 runs: runs.to_vec(),
3852 }
3853 }
3854
3855 pub fn query_for_current_principal(
3859 &self,
3860 table_name: &str,
3861 query: &crate::query::Query,
3862 projection: Option<&[u16]>,
3863 ) -> Result<Vec<crate::memtable::Row>> {
3864 let condition_columns = crate::query::condition_columns(&query.conditions);
3865 self.with_authorized_read(
3866 table_name,
3867 None,
3868 true,
3869 |table, snapshot, allowed, principal| {
3870 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
3871 self.require_columns_for(
3872 table_name,
3873 crate::auth::ColumnOperation::Select,
3874 &condition_columns,
3875 principal,
3876 )?;
3877 if let Some(projection) = projection {
3878 self.require_columns_for(
3879 table_name,
3880 crate::auth::ColumnOperation::Select,
3881 projection,
3882 principal,
3883 )?;
3884 }
3885 let mut rows = table.query_at_with_allowed(query, snapshot, allowed)?;
3886 let projection =
3887 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
3888 for row in &mut rows {
3889 row.columns.retain(|column, _| {
3890 allowed_columns.contains(column)
3891 && projection
3892 .as_ref()
3893 .is_none_or(|projection| projection.contains(column))
3894 });
3895 }
3896 self.secure_rows_for(table_name, rows, principal)
3897 },
3898 )
3899 }
3900
3901 pub fn query_for_current_principal_controlled(
3905 &self,
3906 table_name: &str,
3907 query: &crate::query::Query,
3908 projection: Option<&[u16]>,
3909 control: &crate::ExecutionControl,
3910 ) -> Result<Vec<crate::memtable::Row>> {
3911 self.query_for_principal_controlled(table_name, query, projection, None, true, control)
3912 }
3913
3914 fn query_for_principal_controlled(
3915 &self,
3916 table_name: &str,
3917 query: &crate::query::Query,
3918 projection: Option<&[u16]>,
3919 principal: Option<&crate::auth::Principal>,
3920 catalog_bound: bool,
3921 control: &crate::ExecutionControl,
3922 ) -> Result<Vec<crate::memtable::Row>> {
3923 control.checkpoint()?;
3924 let context = crate::query::AiExecutionContext::with_control(
3925 control.clone(),
3926 usize::MAX,
3927 crate::query::MAX_FUSED_CANDIDATES,
3928 );
3929 let condition_columns = crate::query::condition_columns(&query.conditions);
3930 self.with_authorized_read_context(
3931 table_name,
3932 principal,
3933 catalog_bound,
3934 None,
3935 Some(&context),
3936 None,
3937 |table, snapshot, allowed, principal| {
3938 control.checkpoint()?;
3939 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
3940 self.require_columns_for(
3941 table_name,
3942 crate::auth::ColumnOperation::Select,
3943 &condition_columns,
3944 principal,
3945 )?;
3946 if let Some(projection) = projection {
3947 self.require_columns_for(
3948 table_name,
3949 crate::auth::ColumnOperation::Select,
3950 projection,
3951 principal,
3952 )?;
3953 }
3954 let rows =
3955 table.query_at_with_allowed_controlled(query, snapshot, allowed, control)?;
3956 let projection =
3957 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
3958 let mut projected = Vec::with_capacity(rows.len());
3959 for (index, mut row) in rows.into_iter().enumerate() {
3960 if index & 255 == 0 {
3961 control.checkpoint()?;
3962 }
3963 row.columns.retain(|column, _| {
3964 allowed_columns.contains(column)
3965 && projection
3966 .as_ref()
3967 .is_none_or(|projection| projection.contains(column))
3968 });
3969 projected.push(row);
3970 }
3971 self.secure_rows_for_with_context(table_name, projected, principal, Some(&context))
3972 },
3973 )
3974 }
3975
3976 pub fn approx_aggregate_for_current_principal(
3979 &self,
3980 table_name: &str,
3981 conditions: &[crate::query::Condition],
3982 column: Option<u16>,
3983 agg: crate::engine::ApproxAgg,
3984 z: f64,
3985 ) -> Result<Option<crate::engine::ApproxResult>> {
3986 if !z.is_finite() || z <= 0.0 {
3987 return Err(MongrelError::InvalidArgument(
3988 "z must be finite and > 0".into(),
3989 ));
3990 }
3991 let mut columns = crate::query::condition_columns(conditions);
3992 columns.extend(column);
3993 columns.sort_unstable();
3994 columns.dedup();
3995 self.with_authorized_aggregate_table(
3996 table_name,
3997 &columns,
3998 None,
3999 true,
4000 true,
4001 |table, authorization, _, _| {
4002 table.approx_aggregate_with_candidate_authorization(
4003 conditions,
4004 column,
4005 agg,
4006 z,
4007 authorization,
4008 )
4009 },
4010 )
4011 }
4012
4013 pub fn incremental_aggregate_for_current_principal(
4017 &self,
4018 table_name: &str,
4019 conditions: &[crate::query::Condition],
4020 column: Option<u16>,
4021 agg: crate::engine::NativeAgg,
4022 ) -> Result<crate::engine::IncrementalAggResult> {
4023 self.incremental_aggregate_for_principal(table_name, conditions, column, agg, None, true)
4024 }
4025
4026 pub fn incremental_aggregate_for_principal(
4030 &self,
4031 table_name: &str,
4032 conditions: &[crate::query::Condition],
4033 column: Option<u16>,
4034 agg: crate::engine::NativeAgg,
4035 principal: Option<&crate::auth::Principal>,
4036 catalog_bound: bool,
4037 ) -> Result<crate::engine::IncrementalAggResult> {
4038 let mut columns = crate::query::condition_columns(conditions);
4039 columns.extend(column);
4040 columns.sort_unstable();
4041 columns.dedup();
4042 self.with_authorized_aggregate_table(
4043 table_name,
4044 &columns,
4045 principal,
4046 catalog_bound,
4047 false,
4048 |table, _, principal, security_version| {
4049 let cache_key = incremental_aggregate_cache_key(
4050 table_name,
4051 conditions,
4052 column,
4053 agg,
4054 principal,
4055 security_version,
4056 );
4057 table.aggregate_incremental(cache_key, conditions, column, agg)
4058 },
4059 )
4060 }
4061
4062 pub fn get_for_current_principal(
4065 &self,
4066 table_name: &str,
4067 row_id: RowId,
4068 ) -> Result<Option<crate::memtable::Row>> {
4069 self.with_authorized_read(
4070 table_name,
4071 None,
4072 true,
4073 |table, snapshot, allowed, principal| {
4074 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
4075 let Some(row) = table.get(row_id, snapshot) else {
4076 return Ok(None);
4077 };
4078 if allowed.is_some_and(|allowed| !allowed.contains(&row.row_id)) {
4079 return Ok(None);
4080 }
4081 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
4082 if let Some(row) = rows.first_mut() {
4083 row.columns
4084 .retain(|column, _| allowed_columns.contains(column));
4085 }
4086 Ok(rows.pop())
4087 },
4088 )
4089 }
4090
4091 pub fn ann_rerank_for_current_principal(
4094 &self,
4095 table_name: &str,
4096 request: &crate::query::AnnRerankRequest,
4097 ) -> Result<Vec<crate::query::AnnRerankHit>> {
4098 self.with_authorized_scored_read_context_at(
4099 table_name,
4100 None,
4101 true,
4102 Some(&ReadAuthorization {
4103 operation: crate::auth::ColumnOperation::Select,
4104 columns: vec![request.column_id],
4105 permissions: Vec::new(),
4106 }),
4107 None,
4108 None,
4109 |table, snapshot, authorization, principal| {
4110 self.require_columns_for(
4111 table_name,
4112 crate::auth::ColumnOperation::Select,
4113 &[request.column_id],
4114 principal,
4115 )?;
4116 table.ann_rerank_at_with_candidate_authorization_on_generation(
4117 request,
4118 snapshot,
4119 authorization,
4120 None,
4121 )
4122 },
4123 )
4124 }
4125
4126 pub fn authorized_read_snapshot(
4129 &self,
4130 table: &str,
4131 principal: Option<&crate::auth::Principal>,
4132 ) -> Result<AuthorizedReadSnapshot> {
4133 let (security, security_version, effective_principal) = {
4134 let catalog = self.catalog.read();
4135 (
4136 catalog.security.clone(),
4137 catalog.security_version,
4138 self.principal_for_authorized_read(&catalog, principal, false)?,
4139 )
4140 };
4141 let handle = self.table(table)?;
4142 let (table_snapshot, data_generation, allowed_row_ids) = {
4143 let table_handle = handle.lock();
4144 let table_snapshot = table_handle.snapshot();
4145 let data_generation = table_handle.data_generation();
4146 let allowed = self.allowed_row_ids_locked(
4147 table,
4148 &table_handle,
4149 table_snapshot,
4150 (&security, security_version),
4151 effective_principal.as_ref(),
4152 None,
4153 )?;
4154 (
4155 table_snapshot,
4156 data_generation,
4157 allowed.map(|allowed| (*allowed).clone()),
4158 )
4159 };
4160 Ok(AuthorizedReadSnapshot {
4161 table: table.to_string(),
4162 table_snapshot,
4163 data_generation,
4164 security_version,
4165 allowed_row_ids,
4166 })
4167 }
4168
4169 pub fn authorized_read_snapshot_valid(&self, snapshot: &AuthorizedReadSnapshot) -> bool {
4170 if self.catalog.read().security_version != snapshot.security_version {
4171 return false;
4172 }
4173 self.table(&snapshot.table)
4174 .ok()
4175 .is_some_and(|table| table.lock().data_generation() == snapshot.data_generation)
4176 }
4177
4178 pub fn rls_cache_stats(&self) -> RlsCacheStats {
4179 self.rls_cache.lock().stats()
4180 }
4181
4182 pub fn rows_for(
4184 &self,
4185 table: &str,
4186 principal: Option<&crate::auth::Principal>,
4187 ) -> Result<Vec<crate::memtable::Row>> {
4188 if principal.is_none() && self.principal.read().is_some() {
4189 self.refresh_principal()?;
4190 }
4191 let allowed = self.select_column_ids_for(table, principal)?;
4192 let handle = self.table(table)?;
4193 let rows = {
4194 let table = handle.lock();
4195 table.visible_rows(table.snapshot())?
4196 };
4197 let mut rows = self.secure_rows_for(table, rows, principal)?;
4198 for row in &mut rows {
4199 row.columns.retain(|column, _| allowed.contains(column));
4200 }
4201 Ok(rows)
4202 }
4203
4204 pub fn rows_at_epoch_for_current_principal(
4207 &self,
4208 table_name: &str,
4209 snapshot: Snapshot,
4210 ) -> Result<Vec<crate::memtable::Row>> {
4211 self.with_authorized_read_context(
4212 table_name,
4213 None,
4214 true,
4215 Some(&ReadAuthorization {
4216 operation: crate::auth::ColumnOperation::Select,
4217 columns: Vec::new(),
4218 permissions: Vec::new(),
4219 }),
4220 None,
4221 Some(snapshot),
4222 |table, snapshot, allowed, principal| {
4223 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
4224 let mut rows = table.visible_rows(snapshot)?;
4225 if let Some(allowed) = allowed {
4226 rows.retain(|row| allowed.contains(&row.row_id));
4227 }
4228 rows = self.secure_rows_for(table_name, rows, principal)?;
4229 for row in &mut rows {
4230 row.columns
4231 .retain(|column, _| allowed_columns.contains(column));
4232 }
4233 Ok(rows)
4234 },
4235 )
4236 }
4237
4238 pub fn count_for(
4240 &self,
4241 table: &str,
4242 principal: Option<&crate::auth::Principal>,
4243 ) -> Result<u64> {
4244 if principal.is_none() && self.principal.read().is_some() {
4245 self.refresh_principal()?;
4246 }
4247 self.select_column_ids_for(table, principal)?;
4248 if self.security_active_for(table) {
4249 Ok(self.rows_for(table, principal)?.len() as u64)
4250 } else {
4251 Ok(self.table(table)?.lock().count())
4252 }
4253 }
4254
4255 pub fn put_for(
4257 &self,
4258 table: &str,
4259 mut cells: Vec<(u16, crate::memtable::Value)>,
4260 principal: Option<&crate::auth::Principal>,
4261 ) -> Result<RowId> {
4262 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
4263 self.require_columns_for(
4264 table,
4265 crate::auth::ColumnOperation::Insert,
4266 &columns,
4267 principal,
4268 )?;
4269 let handle = self.table(table)?;
4270 let mut table_handle = handle.lock();
4271 table_handle.fill_auto_inc(&mut cells)?;
4272 table_handle.apply_defaults(&mut cells)?;
4273 let mut row = crate::memtable::Row::new(RowId(0), self.epoch.visible());
4274 row.columns.extend(cells.iter().cloned());
4275 self.check_row_policy_for(
4276 table,
4277 crate::security::PolicyCommand::Insert,
4278 &row,
4279 true,
4280 principal,
4281 )?;
4282 table_handle.put(cells)
4283 }
4284
4285 pub fn check_row_policy_for(
4286 &self,
4287 table: &str,
4288 command: crate::security::PolicyCommand,
4289 row: &crate::memtable::Row,
4290 check_new: bool,
4291 principal: Option<&crate::auth::Principal>,
4292 ) -> Result<()> {
4293 let security = self.catalog.read().security.clone();
4294 if !security.rls_enabled(table) {
4295 return Ok(());
4296 }
4297 let cached = self.principal.read().clone();
4298 let principal = principal
4299 .or(cached.as_ref())
4300 .ok_or(MongrelError::AuthRequired)?;
4301 if security.row_allowed(table, command, row, principal, check_new) {
4302 return Ok(());
4303 }
4304 let required = match command {
4305 crate::security::PolicyCommand::Insert => crate::auth::Permission::Insert {
4306 table: table.to_string(),
4307 },
4308 crate::security::PolicyCommand::Update => crate::auth::Permission::Update {
4309 table: table.to_string(),
4310 },
4311 crate::security::PolicyCommand::Select => crate::auth::Permission::Select {
4312 table: table.to_string(),
4313 },
4314 crate::security::PolicyCommand::Delete | crate::security::PolicyCommand::All => {
4315 crate::auth::Permission::Delete {
4316 table: table.to_string(),
4317 }
4318 }
4319 };
4320 Err(MongrelError::PermissionDenied {
4321 required,
4322 principal: principal.username.clone(),
4323 })
4324 }
4325
4326 pub fn set_materialized_view(
4329 &self,
4330 definition: crate::catalog::MaterializedViewEntry,
4331 ) -> Result<()> {
4332 self.set_materialized_view_with_epoch(definition)
4333 .map(|_| ())
4334 }
4335
4336 pub fn set_materialized_view_with_epoch(
4338 &self,
4339 definition: crate::catalog::MaterializedViewEntry,
4340 ) -> Result<Epoch> {
4341 use crate::wal::DdlOp;
4342 use std::sync::atomic::Ordering;
4343
4344 self.require(&crate::auth::Permission::Ddl)?;
4345 if self.poisoned.load(Ordering::Relaxed) {
4346 return Err(MongrelError::Other(
4347 "database poisoned by fsync error".into(),
4348 ));
4349 }
4350 if definition.name.is_empty() || definition.query.trim().is_empty() {
4351 return Err(MongrelError::InvalidArgument(
4352 "materialized view name and query must not be empty".into(),
4353 ));
4354 }
4355
4356 let _ddl = self.ddl_lock.lock();
4357 let _security_write = self.security_write()?;
4358 self.require(&crate::auth::Permission::Ddl)?;
4359 let table_id = self
4360 .catalog
4361 .read()
4362 .live(&definition.name)
4363 .ok_or_else(|| {
4364 MongrelError::NotFound(format!(
4365 "materialized view table {:?} not found",
4366 definition.name
4367 ))
4368 })?
4369 .table_id;
4370 let definition_json = DdlOp::encode_materialized_view(&definition)?;
4371 let _commit = self.commit_lock.lock();
4372 let epoch = self.epoch.bump_assigned();
4373 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
4374 let txn_id = self.alloc_txn_id()?;
4375 let mut next_catalog = self.catalog.read().clone();
4376 if let Some(existing) = next_catalog
4377 .materialized_views
4378 .iter_mut()
4379 .find(|existing| existing.name == definition.name)
4380 {
4381 *existing = definition.clone();
4382 } else {
4383 next_catalog.materialized_views.push(definition.clone());
4384 }
4385 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
4386 let commit_seq = {
4387 let mut wal = self.shared_wal.lock();
4388 let append: Result<u64> = (|| {
4389 wal.append(
4390 txn_id,
4391 table_id,
4392 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
4393 name: definition.name.clone(),
4394 definition_json,
4395 }),
4396 )?;
4397 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
4398 wal.append_commit(txn_id, epoch, &[])
4399 })();
4400 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
4401 };
4402 self.await_durable_commit(commit_seq, epoch)?;
4403
4404 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
4405 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
4406 Ok(epoch)
4407 }
4408
4409 pub fn root(&self) -> &Path {
4411 self.durable_root.canonical_path()
4412 }
4413
4414 pub fn durable_root(&self) -> Arc<crate::durable_file::DurableRoot> {
4417 Arc::clone(&self.durable_root)
4418 }
4419
4420 #[cfg(feature = "encryption")]
4424 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
4425 self.kek
4426 .as_deref()
4427 .map(|kek| kek.derive_subkey(b"mongreldb/server/idempotency/v1"))
4428 }
4429
4430 #[cfg(not(feature = "encryption"))]
4431 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
4432 None
4433 }
4434
4435 pub fn is_read_only_replica(&self) -> bool {
4436 self.read_only
4437 }
4438
4439 pub fn ensure_consistent_read(&self) -> Result<()> {
4443 if self.poisoned.load(Ordering::Relaxed) {
4444 return Err(MongrelError::Other(
4445 "database poisoned by post-commit failure; reopen required".into(),
4446 ));
4447 }
4448 Ok(())
4449 }
4450
4451 pub fn set_replication_wal_retention_segments(&self, segments: usize) {
4452 self.replication_wal_retention_segments
4453 .store(segments, std::sync::atomic::Ordering::Relaxed);
4454 }
4455
4456 pub fn replication_snapshot(&self) -> Result<crate::replication::ReplicationSnapshot> {
4461 let admin = crate::auth::Permission::Admin;
4462 self.require(&admin)?;
4463 let operation_principal = self.principal_snapshot();
4464 let _barrier = self.replication_barrier.write();
4465 let _ddl = self.ddl_lock.lock();
4466 let _security = self.security_coordinator.gate.read();
4467 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4468 let mut handles: Vec<_> = self
4469 .tables
4470 .read()
4471 .iter()
4472 .map(|(id, handle)| (*id, handle.clone()))
4473 .collect();
4474 handles.sort_by_key(|(id, _)| *id);
4475 let _table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
4476 let _commit = self.commit_lock.lock();
4477 let mut wal = self.shared_wal.lock();
4478 wal.group_sync()?;
4479 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
4480 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
4481 let epoch = records
4482 .iter()
4483 .filter_map(|record| match &record.op {
4484 crate::wal::Op::TxnCommit { epoch, .. } => Some(*epoch),
4485 _ => None,
4486 })
4487 .max()
4488 .unwrap_or(0)
4489 .max(self.epoch.committed().0);
4490 let files = crate::replication::capture_files(&self.root)?;
4491 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
4492 drop(wal);
4493 Ok(crate::replication::ReplicationSnapshot::new(
4494 source_id, epoch, files,
4495 ))
4496 }
4497
4498 pub fn hot_backup(&self, destination: impl AsRef<Path>) -> Result<crate::backup::BackupReport> {
4506 let control = crate::ExecutionControl::new(None);
4507 self.hot_backup_controlled(destination, &control, || true)
4508 }
4509
4510 pub(crate) fn hot_backup_to_durable_child(
4511 &self,
4512 parent: &crate::durable_file::DurableRoot,
4513 child: &Path,
4514 control: &crate::ExecutionControl,
4515 ) -> Result<crate::backup::BackupReport> {
4516 let mut components = child.components();
4517 if !matches!(components.next(), Some(std::path::Component::Normal(_)))
4518 || components.next().is_some()
4519 {
4520 return Err(MongrelError::InvalidArgument(
4521 "durable backup child must be one relative path component".into(),
4522 ));
4523 }
4524 let destination_name = child.file_name().ok_or_else(|| {
4525 MongrelError::InvalidArgument("durable backup child has no filename".into())
4526 })?;
4527 let prepared = prepare_backup_destination_in(&self.root, parent, destination_name)?;
4528 self.hot_backup_prepared(prepared, control, || true)
4529 }
4530
4531 #[doc(hidden)]
4534 pub fn hot_backup_controlled<F>(
4535 &self,
4536 destination: impl AsRef<Path>,
4537 control: &crate::ExecutionControl,
4538 before_publish: F,
4539 ) -> Result<crate::backup::BackupReport>
4540 where
4541 F: FnOnce() -> bool,
4542 {
4543 let prepared = prepare_backup_destination(&self.root, destination.as_ref())?;
4544 self.hot_backup_prepared(prepared, control, before_publish)
4545 }
4546
4547 fn hot_backup_prepared<F>(
4548 &self,
4549 mut prepared: PreparedBackupDestination,
4550 control: &crate::ExecutionControl,
4551 before_publish: F,
4552 ) -> Result<crate::backup::BackupReport>
4553 where
4554 F: FnOnce() -> bool,
4555 {
4556 let admin = crate::auth::Permission::Admin;
4557 self.require(&admin)?;
4558 let operation_principal = self.principal_snapshot();
4559 control.checkpoint()?;
4560 let destination = prepared.destination_path.clone();
4561 let mut before_publish = Some(before_publish);
4562
4563 let outcome = (|| {
4564 control.checkpoint()?;
4565 let barrier = self.replication_barrier.write();
4566 let ddl = self.ddl_lock.lock();
4567 let security = self.security_coordinator.gate.read();
4568 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4569 let mut handles: Vec<_> = self
4570 .tables
4571 .read()
4572 .iter()
4573 .map(|(id, handle)| (*id, handle.clone()))
4574 .collect();
4575 handles.sort_by_key(|(id, _)| *id);
4576 let table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
4577 let commit = self.commit_lock.lock();
4578 let mut wal = self.shared_wal.lock();
4579 wal.group_sync()?;
4580 let epoch = self.epoch.committed().0;
4581 let boundary_unix_nanos = current_unix_nanos();
4582
4583 let pin_nonce = std::time::SystemTime::now()
4584 .duration_since(std::time::UNIX_EPOCH)
4585 .unwrap_or_default()
4586 .as_nanos();
4587 let file_pin_root = self
4588 .root
4589 .join(META_DIR)
4590 .join("backup-pins")
4591 .join(format!("{}-{pin_nonce}", std::process::id()));
4592 std::fs::create_dir_all(&file_pin_root)?;
4593 let _file_pins = BackupFilePins {
4594 root: file_pin_root.clone(),
4595 };
4596 let mut run_files = Vec::new();
4597 for (index, (table_id, _)) in handles.iter().enumerate() {
4598 if index % 256 == 0 {
4599 control.checkpoint()?;
4600 }
4601 let table = &table_guards[index];
4602 for (run_index, run) in table.run_refs().iter().enumerate() {
4603 if run_index % 256 == 0 {
4604 control.checkpoint()?;
4605 }
4606 let source = table.run_path(run.run_id as u64);
4607 let relative = Path::new(TABLES_DIR)
4608 .join(table_id.to_string())
4609 .join(crate::engine::RUNS_DIR)
4610 .join(format!("r-{}.sr", run.run_id));
4611 let pinned = file_pin_root.join(format!("{table_id}-{}.sr", run.run_id));
4612 if std::fs::hard_link(&source, &pinned).is_err() {
4613 crate::backup::copy_file_synced(&source, &pinned)?;
4614 }
4615 run_files.push(((*table_id, run.run_id), pinned, relative));
4616 }
4617 }
4618 crate::durable_file::sync_directory(&file_pin_root)?;
4619 let run_keys: Vec<_> = run_files.iter().map(|(key, _, _)| *key).collect();
4620 {
4621 let mut pins = self.backup_pins.lock();
4622 for key in &run_keys {
4623 *pins.entry(*key).or_insert(0) += 1;
4624 }
4625 }
4626 let _run_pins = RunPins {
4627 pins: Arc::clone(&self.backup_pins),
4628 runs: run_keys,
4629 };
4630 let deferred: HashSet<_> = run_files
4631 .iter()
4632 .map(|(_, _, relative)| relative.clone())
4633 .collect();
4634 let mut copied = Vec::new();
4635 copy_backup_boundary(
4636 &self.root,
4637 prepared.stage.as_deref().ok_or_else(|| {
4638 MongrelError::Other("backup staging root was already released".into())
4639 })?,
4640 &deferred,
4641 &mut copied,
4642 Some(control),
4643 )?;
4644
4645 drop(wal);
4646 drop(commit);
4647 drop(table_guards);
4648 drop(security);
4649 drop(ddl);
4650 drop(barrier);
4651
4652 if let Some(hook) = self.backup_hook.lock().as_ref() {
4653 hook();
4654 }
4655 for (index, (_, source, relative)) in run_files.into_iter().enumerate() {
4656 if index % 256 == 0 {
4657 control.checkpoint()?;
4658 }
4659 let mut source = crate::durable_file::open_regular_nofollow(&source)?;
4660 prepared
4661 .stage
4662 .as_deref()
4663 .ok_or_else(|| {
4664 MongrelError::Other("backup staging root was already released".into())
4665 })?
4666 .copy_new_from(&relative, &mut source)?;
4667 copied.push(relative);
4668 }
4669
4670 let manifest = crate::backup::BackupManifest::create_controlled_durable(
4671 prepared.stage.as_deref().ok_or_else(|| {
4672 MongrelError::Other("backup staging root was already released".into())
4673 })?,
4674 epoch,
4675 &copied,
4676 control,
4677 )?;
4678 manifest.write_to_durable(prepared.stage.as_deref().ok_or_else(|| {
4679 MongrelError::Other("backup staging root was already released".into())
4680 })?)?;
4681 control.checkpoint()?;
4682 let publish = before_publish.take().ok_or_else(|| {
4683 MongrelError::Other("backup publication callback already consumed".into())
4684 })?;
4685 if !publish() {
4686 return Err(MongrelError::Cancelled);
4687 }
4688 let final_security = self.security_coordinator.gate.read();
4689 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4690 drop(prepared.stage.take().ok_or_else(|| {
4694 MongrelError::Other("backup staging root was already released".into())
4695 })?);
4696 let published = std::cell::Cell::new(false);
4697 if let Err(error) = prepared.parent.rename_directory_new_with_after(
4698 Path::new(&prepared.stage_name),
4699 &prepared.parent,
4700 Path::new(&prepared.destination_name),
4701 || published.set(true),
4702 ) {
4703 if published.get() {
4704 return Err(MongrelError::CommitOutcomeUnknown {
4705 epoch,
4706 message: format!("backup publication was not durable: {error}"),
4707 });
4708 }
4709 return Err(error.into());
4710 }
4711 drop(final_security);
4712 Ok(crate::backup::BackupReport {
4713 destination,
4714 epoch,
4715 boundary_unix_nanos,
4716 files: manifest.files.len(),
4717 bytes: manifest.total_bytes(),
4718 })
4719 })();
4720
4721 if outcome.is_err() {
4722 drop(prepared.stage.take());
4723 let _ = prepared
4724 .parent
4725 .remove_directory_all(Path::new(&prepared.stage_name));
4726 }
4727 outcome
4728 }
4729
4730 pub fn replication_batch_since(
4733 &self,
4734 since_epoch: u64,
4735 ) -> Result<crate::replication::ReplicationBatch> {
4736 use crate::wal::Op;
4737
4738 let admin = crate::auth::Permission::Admin;
4739 self.require(&admin)?;
4740 let operation_principal = self.principal_snapshot();
4741
4742 let mut wal = self.shared_wal.lock();
4743 wal.group_sync()?;
4744 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
4745 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
4746 drop(wal);
4747
4748 let commits: HashMap<u64, u64> = records
4749 .iter()
4750 .filter_map(|record| match &record.op {
4751 Op::TxnCommit { epoch, .. } => Some((record.txn_id, *epoch)),
4752 _ => None,
4753 })
4754 .collect();
4755 let earliest_epoch = commits.values().copied().min();
4756 let current_epoch = commits
4757 .values()
4758 .copied()
4759 .max()
4760 .unwrap_or(0)
4761 .max(self.epoch.committed().0);
4762 let selected: HashSet<u64> = commits
4763 .iter()
4764 .filter_map(|(txn_id, epoch)| (*epoch > since_epoch).then_some(*txn_id))
4765 .collect();
4766 let retention_gap = since_epoch < current_epoch
4767 && since_epoch < crate::replication::replication_wal_floor(&self.root)?;
4768 let spilled = records.iter().any(|record| {
4769 selected.contains(&record.txn_id)
4770 && matches!(
4771 &record.op,
4772 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty()
4773 )
4774 });
4775 let records = records
4776 .into_iter()
4777 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
4778 .filter(|record| selected.contains(&record.txn_id))
4779 .collect();
4780 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
4781 let batch = crate::replication::ReplicationBatch::complete_for_source(
4782 source_id,
4783 since_epoch,
4784 current_epoch,
4785 earliest_epoch,
4786 retention_gap,
4787 spilled,
4788 records,
4789 )?;
4790 if let Some(hook) = self.replication_hook.lock().as_ref() {
4791 hook();
4792 }
4793 let _security = self.security_coordinator.gate.read();
4794 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4795 Ok(batch)
4796 }
4797
4798 pub fn append_replication_batch(
4802 &self,
4803 batch: &crate::replication::ReplicationBatch,
4804 ) -> Result<u64> {
4805 use crate::wal::Op;
4806
4807 if !self.read_only {
4808 return Err(MongrelError::InvalidArgument(
4809 "replication batches may only target a marked replica".into(),
4810 ));
4811 }
4812 let current = crate::replication::replica_epoch(&self.root)?;
4813 if batch.is_source_bound() {
4814 let source_id = crate::replication::replica_source_id_durable(&self.durable_root)?;
4815 if batch.source_id != source_id {
4816 return Err(MongrelError::Conflict(
4817 "replication batch source does not match follower binding".into(),
4818 ));
4819 }
4820 }
4821 if batch.requires_snapshot {
4822 return Err(MongrelError::Conflict(
4823 "replication snapshot required for this batch".into(),
4824 ));
4825 }
4826 batch.validate_proof()?;
4827 if batch.from_epoch != current {
4828 if batch.from_epoch < current && batch.current_epoch == current {
4829 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
4830 let _wal = self.shared_wal.lock();
4831 let existing: HashSet<(u64, u64)> =
4832 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
4833 .into_iter()
4834 .filter_map(|record| match record.op {
4835 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
4836 _ => None,
4837 })
4838 .collect();
4839 let already_applied = batch.records.iter().all(|record| match &record.op {
4840 Op::TxnCommit { epoch, .. } => existing.contains(&(record.txn_id, *epoch)),
4841 _ => true,
4842 });
4843 if already_applied {
4844 return Ok(current);
4845 }
4846 }
4847 return Err(MongrelError::Conflict(format!(
4848 "replication batch starts at epoch {}, follower is at epoch {current}",
4849 batch.from_epoch
4850 )));
4851 }
4852 if batch.current_epoch < current {
4853 return Err(MongrelError::InvalidArgument(format!(
4854 "replication batch current epoch {} precedes follower epoch {current}",
4855 batch.current_epoch
4856 )));
4857 }
4858 let records = &batch.records;
4859 let mut commits = HashMap::new();
4860 let mut commit_epochs = HashSet::new();
4861 let mut commit_timestamps = HashMap::new();
4862 for record in records {
4863 match &record.op {
4864 Op::TxnCommit { epoch, added_runs } => {
4865 if !added_runs.is_empty() {
4866 return Err(MongrelError::Conflict(
4867 "replication snapshot required for spilled-run transaction".into(),
4868 ));
4869 }
4870 if commits.insert(record.txn_id, *epoch).is_some() {
4871 return Err(MongrelError::InvalidArgument(format!(
4872 "duplicate commit for replication transaction {}",
4873 record.txn_id
4874 )));
4875 }
4876 if *epoch <= current || *epoch > batch.current_epoch {
4877 return Err(MongrelError::InvalidArgument(format!(
4878 "replication commit epoch {epoch} is outside ({current}, {}]",
4879 batch.current_epoch
4880 )));
4881 }
4882 if !commit_epochs.insert(*epoch) {
4883 return Err(MongrelError::InvalidArgument(format!(
4884 "duplicate replication commit epoch {epoch}"
4885 )));
4886 }
4887 }
4888 Op::CommitTimestamp { unix_nanos } => {
4889 commit_timestamps.insert(record.txn_id, *unix_nanos);
4890 }
4891 _ => {}
4892 }
4893 }
4894 for record in records {
4895 if record.txn_id == crate::wal::SYSTEM_TXN_ID
4896 || matches!(&record.op, Op::TxnAbort | Op::Flush { .. })
4897 {
4898 return Err(MongrelError::InvalidArgument(
4899 "replication batch contains a non-committed record".into(),
4900 ));
4901 }
4902 if !commits.contains_key(&record.txn_id) {
4903 return Err(MongrelError::InvalidArgument(format!(
4904 "incomplete replication transaction {}",
4905 record.txn_id
4906 )));
4907 }
4908 }
4909 let target_epoch = commits
4910 .values()
4911 .copied()
4912 .filter(|epoch| *epoch > current)
4913 .max()
4914 .unwrap_or(current);
4915 if target_epoch != batch.current_epoch {
4916 return Err(MongrelError::InvalidArgument(format!(
4917 "replication batch ends at epoch {target_epoch}, expected {}",
4918 batch.current_epoch
4919 )));
4920 }
4921 let mut selected: HashSet<u64> = commits
4922 .iter()
4923 .filter_map(|(txn_id, epoch)| (*epoch > current).then_some(*txn_id))
4924 .collect();
4925 if selected.is_empty() {
4926 return Ok(current);
4927 }
4928 let mut wal = self.shared_wal.lock();
4929 wal.group_sync()?;
4930 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
4931 let existing: HashSet<(u64, u64)> =
4932 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
4933 .into_iter()
4934 .filter_map(|record| match record.op {
4935 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
4936 _ => None,
4937 })
4938 .collect();
4939 selected.retain(|txn_id| {
4940 commits
4941 .get(txn_id)
4942 .is_some_and(|epoch| !existing.contains(&(*txn_id, *epoch)))
4943 });
4944 for record in records {
4945 if !selected.contains(&record.txn_id) {
4946 continue;
4947 }
4948 match &record.op {
4949 Op::TxnCommit { epoch, added_runs } => {
4950 let timestamp = commit_timestamps
4951 .get(&record.txn_id)
4952 .copied()
4953 .unwrap_or_else(current_unix_nanos);
4954 wal.append_commit_at(record.txn_id, Epoch(*epoch), added_runs, timestamp)?;
4955 }
4956 Op::TxnAbort | Op::Flush { .. } | Op::CommitTimestamp { .. } => {}
4957 op => {
4958 wal.append(record.txn_id, 0, op.clone())?;
4959 }
4960 }
4961 }
4962 if !selected.is_empty() {
4963 wal.group_sync()?;
4964 }
4965 drop(wal);
4966
4967 let mut recovered_catalog = self.catalog.read().clone();
4971 if let Err(error) = recover_ddl_from_wal(
4972 &self.root,
4973 Some(&self.durable_root),
4974 &mut recovered_catalog,
4975 self.meta_dek.as_ref(),
4976 wal_dek.as_ref(),
4977 true,
4978 None,
4979 ) {
4980 return Err(MongrelError::DurableCommit {
4981 epoch: target_epoch,
4982 message: format!(
4983 "replication WAL is durable but catalog checkpoint failed: {error}"
4984 ),
4985 });
4986 }
4987 let _security = self.security_coordinator.gate.write();
4988 let old_security_version = self.catalog.read().security_version;
4989 let security_changed = old_security_version != recovered_catalog.security_version
4990 || self.catalog.read().require_auth != recovered_catalog.require_auth;
4991 let require_auth = recovered_catalog.require_auth;
4992 let principal = if security_changed {
4993 None
4994 } else {
4995 self.principal.read().as_ref().and_then(|principal| {
4996 Self::resolve_bound_principal_from_catalog(&recovered_catalog, principal)
4997 })
4998 };
4999 if require_auth {
5000 self.auth_state.set_require_auth(true);
5001 }
5002 self.auth_state.set_principal(principal.clone());
5003 *self.principal.write() = principal;
5004 let security_version = recovered_catalog.security_version;
5005 *self.catalog.write() = recovered_catalog;
5006 self.security_coordinator
5007 .version
5008 .store(security_version, Ordering::Release);
5009 if !require_auth {
5010 self.auth_state.set_require_auth(false);
5011 }
5012 if let Err(error) =
5013 crate::replication::reconcile_replica_epoch_durable(&self.durable_root, target_epoch)
5014 {
5015 return Err(MongrelError::DurableCommit {
5016 epoch: target_epoch,
5017 message: format!(
5018 "replication WAL and catalog are durable but follower watermark failed: {error}"
5019 ),
5020 });
5021 }
5022 Ok(target_epoch)
5023 }
5024
5025 pub fn table_id(&self, name: &str) -> Result<u64> {
5028 let cat = self.catalog.read();
5029 cat.live(name)
5030 .map(|e| e.table_id)
5031 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
5032 }
5033
5034 pub fn table_identity(&self, name: &str) -> Result<(u64, u64)> {
5039 let catalog = self.catalog.read();
5040 catalog
5041 .live(name)
5042 .map(|entry| (entry.table_id, entry.schema.schema_id))
5043 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
5044 }
5045
5046 pub(crate) fn building_table_id(&self, name: &str) -> Result<u64> {
5047 self.catalog
5048 .read()
5049 .building(name)
5050 .map(|entry| entry.table_id)
5051 .ok_or_else(|| MongrelError::NotFound(format!("building table {name:?} not found")))
5052 }
5053
5054 pub fn procedures(&self) -> Vec<StoredProcedure> {
5055 self.catalog
5056 .read()
5057 .procedures
5058 .iter()
5059 .map(|p| p.procedure.clone())
5060 .collect()
5061 }
5062
5063 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
5064 self.catalog
5065 .read()
5066 .procedures
5067 .iter()
5068 .find(|p| p.procedure.name == name)
5069 .map(|p| p.procedure.clone())
5070 }
5071
5072 pub fn create_procedure(&self, procedure: StoredProcedure) -> Result<StoredProcedure> {
5073 self.create_procedure_inner(procedure, None)
5074 }
5075
5076 pub fn create_procedure_controlled<F>(
5077 &self,
5078 procedure: StoredProcedure,
5079 mut before_publish: F,
5080 ) -> Result<StoredProcedure>
5081 where
5082 F: FnMut() -> Result<()>,
5083 {
5084 self.create_procedure_inner(procedure, Some(&mut before_publish))
5085 }
5086
5087 fn create_procedure_inner(
5088 &self,
5089 mut procedure: StoredProcedure,
5090 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5091 ) -> Result<StoredProcedure> {
5092 self.require(&crate::auth::Permission::Ddl)?;
5093 let _g = self.ddl_lock.lock();
5094 let _security_write = self.security_write()?;
5095 self.require(&crate::auth::Permission::Ddl)?;
5096 procedure.validate()?;
5097 self.validate_procedure_references(&procedure)?;
5098 {
5099 let cat = self.catalog.read();
5100 if cat
5101 .procedures
5102 .iter()
5103 .any(|p| p.procedure.name == procedure.name)
5104 {
5105 return Err(MongrelError::InvalidArgument(format!(
5106 "procedure {:?} already exists",
5107 procedure.name
5108 )));
5109 }
5110 }
5111 let commit_lock = Arc::clone(&self.commit_lock);
5112 let _c = commit_lock.lock();
5113 let epoch = self.epoch.bump_assigned();
5114 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5115 procedure.created_epoch = epoch.0;
5116 procedure.updated_epoch = epoch.0;
5117 let mut next_catalog = self.catalog.read().clone();
5118 next_catalog
5119 .procedures
5120 .push(ProcedureEntry::from(procedure.clone()));
5121 next_catalog.db_epoch = epoch.0;
5122 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5123 Ok(procedure)
5124 }
5125
5126 pub fn create_or_replace_procedure(
5127 &self,
5128 procedure: StoredProcedure,
5129 ) -> Result<StoredProcedure> {
5130 self.create_or_replace_procedure_inner(procedure, None)
5131 }
5132
5133 pub fn create_or_replace_procedure_controlled<F>(
5134 &self,
5135 procedure: StoredProcedure,
5136 mut before_publish: F,
5137 ) -> Result<StoredProcedure>
5138 where
5139 F: FnMut() -> Result<()>,
5140 {
5141 self.create_or_replace_procedure_inner(procedure, Some(&mut before_publish))
5142 }
5143
5144 fn create_or_replace_procedure_inner(
5145 &self,
5146 procedure: StoredProcedure,
5147 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5148 ) -> Result<StoredProcedure> {
5149 self.require(&crate::auth::Permission::Ddl)?;
5150 let _g = self.ddl_lock.lock();
5151 let _security_write = self.security_write()?;
5152 self.require(&crate::auth::Permission::Ddl)?;
5153 procedure.validate()?;
5154 self.validate_procedure_references(&procedure)?;
5155 let commit_lock = Arc::clone(&self.commit_lock);
5156 let _c = commit_lock.lock();
5157 let epoch = self.epoch.bump_assigned();
5158 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5159 let mut next_catalog = self.catalog.read().clone();
5160 let replaced = {
5161 let next = match next_catalog
5162 .procedures
5163 .iter()
5164 .position(|p| p.procedure.name == procedure.name)
5165 {
5166 Some(idx) => {
5167 let next = next_catalog.procedures[idx]
5168 .procedure
5169 .replaced(procedure.clone(), epoch.0)?;
5170 next_catalog.procedures[idx] = ProcedureEntry::from(next.clone());
5171 next
5172 }
5173 None => {
5174 let mut next = procedure;
5175 next.created_epoch = epoch.0;
5176 next.updated_epoch = epoch.0;
5177 next_catalog
5178 .procedures
5179 .push(ProcedureEntry::from(next.clone()));
5180 next
5181 }
5182 };
5183 next_catalog.db_epoch = epoch.0;
5184 next
5185 };
5186 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5187 Ok(replaced)
5188 }
5189
5190 pub fn drop_procedure(&self, name: &str) -> Result<()> {
5191 self.drop_procedure_with_epoch(name).map(|_| ())
5192 }
5193
5194 pub fn drop_procedure_with_epoch(&self, name: &str) -> Result<Epoch> {
5195 self.drop_procedure_with_epoch_inner(name, None)
5196 }
5197
5198 pub fn drop_procedure_with_epoch_controlled<F>(
5199 &self,
5200 name: &str,
5201 mut before_publish: F,
5202 ) -> Result<Epoch>
5203 where
5204 F: FnMut() -> Result<()>,
5205 {
5206 self.drop_procedure_with_epoch_inner(name, Some(&mut before_publish))
5207 }
5208
5209 fn drop_procedure_with_epoch_inner(
5210 &self,
5211 name: &str,
5212 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5213 ) -> Result<Epoch> {
5214 self.require(&crate::auth::Permission::Ddl)?;
5215 let _g = self.ddl_lock.lock();
5216 let _security_write = self.security_write()?;
5217 self.require(&crate::auth::Permission::Ddl)?;
5218 let commit_lock = Arc::clone(&self.commit_lock);
5219 let _c = commit_lock.lock();
5220 let epoch = self.epoch.bump_assigned();
5221 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5222 let mut next_catalog = self.catalog.read().clone();
5223 let before = next_catalog.procedures.len();
5224 next_catalog.procedures.retain(|p| p.procedure.name != name);
5225 if next_catalog.procedures.len() == before {
5226 return Err(MongrelError::NotFound(format!(
5227 "procedure {name:?} not found"
5228 )));
5229 }
5230 next_catalog.db_epoch = epoch.0;
5231 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5232 Ok(epoch)
5233 }
5234
5235 pub fn users(&self) -> Vec<crate::auth::UserEntry> {
5240 self.catalog.read().users.clone()
5241 }
5242
5243 pub fn user_identity(&self, username: &str) -> Option<(u64, u64)> {
5246 self.catalog
5247 .read()
5248 .users
5249 .iter()
5250 .find(|user| user.username == username)
5251 .map(|user| (user.id, user.created_epoch))
5252 }
5253
5254 pub fn security_version(&self) -> u64 {
5257 self.catalog.read().security_version
5258 }
5259
5260 pub fn roles(&self) -> Vec<crate::auth::RoleEntry> {
5262 self.catalog.read().roles.clone()
5263 }
5264
5265 pub fn create_user(&self, username: &str, password: &str) -> Result<crate::auth::UserEntry> {
5267 self.require(&crate::auth::Permission::Admin)?;
5268 let hash = crate::auth::hash_password(password).map_err(MongrelError::Other)?;
5269 self.create_user_with_password_hash(username, hash)
5270 }
5271
5272 pub fn create_user_with_password_hash(
5274 &self,
5275 username: &str,
5276 hash: String,
5277 ) -> Result<crate::auth::UserEntry> {
5278 self.create_user_with_password_hash_inner(username, hash, None)
5279 }
5280
5281 pub fn create_user_with_password_hash_controlled<F>(
5282 &self,
5283 username: &str,
5284 hash: String,
5285 mut before_publish: F,
5286 ) -> Result<crate::auth::UserEntry>
5287 where
5288 F: FnMut() -> Result<()>,
5289 {
5290 self.create_user_with_password_hash_inner(username, hash, Some(&mut before_publish))
5291 }
5292
5293 fn create_user_with_password_hash_inner(
5294 &self,
5295 username: &str,
5296 hash: String,
5297 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5298 ) -> Result<crate::auth::UserEntry> {
5299 self.require(&crate::auth::Permission::Admin)?;
5300 let _ddl = self.ddl_lock.lock();
5301 let _security_write = self.security_write()?;
5302 self.require(&crate::auth::Permission::Admin)?;
5303 let _commit = self.commit_lock.lock();
5304 let epoch = self.epoch.bump_assigned();
5305 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5306 let mut next_catalog = self.catalog.read().clone();
5307 if next_catalog.users.iter().any(|u| u.username == username) {
5308 return Err(MongrelError::InvalidArgument(format!(
5309 "user {username:?} already exists"
5310 )));
5311 }
5312 next_catalog.next_user_id = next_catalog.next_user_id.max(1);
5313 let id = next_catalog.next_user_id;
5314 next_catalog.next_user_id = id
5315 .checked_add(1)
5316 .ok_or_else(|| MongrelError::Full("user-id namespace exhausted".into()))?;
5317 let entry = crate::auth::UserEntry {
5318 id,
5319 username: username.into(),
5320 password_hash: hash,
5321 roles: Vec::new(),
5322 is_admin: false,
5323 created_epoch: epoch.0,
5324 };
5325 next_catalog.users.push(entry.clone());
5326 advance_security_version(&mut next_catalog)?;
5327 next_catalog.db_epoch = epoch.0;
5328 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5329 Ok(entry)
5330 }
5331
5332 pub fn drop_user(&self, username: &str) -> Result<()> {
5334 self.drop_user_with_epoch(username).map(|_| ())
5335 }
5336
5337 pub fn drop_user_with_epoch(&self, username: &str) -> Result<Epoch> {
5338 self.drop_user_with_epoch_inner(username, None)
5339 }
5340
5341 pub fn drop_user_with_epoch_controlled<F>(
5342 &self,
5343 username: &str,
5344 mut before_publish: F,
5345 ) -> Result<Epoch>
5346 where
5347 F: FnMut() -> Result<()>,
5348 {
5349 self.drop_user_with_epoch_inner(username, Some(&mut before_publish))
5350 }
5351
5352 fn drop_user_with_epoch_inner(
5353 &self,
5354 username: &str,
5355 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5356 ) -> Result<Epoch> {
5357 self.require(&crate::auth::Permission::Admin)?;
5358 let _ddl = self.ddl_lock.lock();
5359 let _security_write = self.security_write()?;
5360 self.require(&crate::auth::Permission::Admin)?;
5361 let _commit = self.commit_lock.lock();
5362 let epoch = self.epoch.bump_assigned();
5363 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5364 let mut next_catalog = self.catalog.read().clone();
5365 let before = next_catalog.users.len();
5366 next_catalog.users.retain(|u| u.username != username);
5367 if next_catalog.users.len() == before {
5368 return Err(MongrelError::NotFound(format!(
5369 "user {username:?} not found"
5370 )));
5371 }
5372 advance_security_version(&mut next_catalog)?;
5373 next_catalog.db_epoch = epoch.0;
5374 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5375 Ok(epoch)
5376 }
5377
5378 pub fn alter_user_password(&self, username: &str, new_password: &str) -> Result<()> {
5380 self.alter_user_password_with_epoch(username, new_password)
5381 .map(|_| ())
5382 }
5383
5384 pub fn alter_user_password_with_epoch(
5385 &self,
5386 username: &str,
5387 new_password: &str,
5388 ) -> Result<Epoch> {
5389 self.require(&crate::auth::Permission::Admin)?;
5390 let hash = crate::auth::hash_password(new_password).map_err(MongrelError::Other)?;
5391 self.alter_user_password_hash_with_epoch(username, hash)
5392 }
5393
5394 pub fn alter_user_password_hash_with_epoch(
5395 &self,
5396 username: &str,
5397 hash: String,
5398 ) -> Result<Epoch> {
5399 self.alter_user_password_hash_with_epoch_inner(username, hash, None)
5400 }
5401
5402 pub fn alter_user_password_hash_with_epoch_controlled<F>(
5403 &self,
5404 username: &str,
5405 hash: String,
5406 mut before_publish: F,
5407 ) -> Result<Epoch>
5408 where
5409 F: FnMut() -> Result<()>,
5410 {
5411 self.alter_user_password_hash_with_epoch_inner(username, hash, Some(&mut before_publish))
5412 }
5413
5414 fn alter_user_password_hash_with_epoch_inner(
5415 &self,
5416 username: &str,
5417 hash: String,
5418 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5419 ) -> Result<Epoch> {
5420 self.require(&crate::auth::Permission::Admin)?;
5421 let _ddl = self.ddl_lock.lock();
5422 let _security_write = self.security_write()?;
5423 self.require(&crate::auth::Permission::Admin)?;
5424 let _commit = self.commit_lock.lock();
5425 let epoch = self.epoch.bump_assigned();
5426 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5427 let mut next_catalog = self.catalog.read().clone();
5428 let user = next_catalog
5429 .users
5430 .iter_mut()
5431 .find(|u| u.username == username)
5432 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5433 user.password_hash = hash;
5434 advance_security_version(&mut next_catalog)?;
5435 next_catalog.db_epoch = epoch.0;
5436 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5437 Ok(epoch)
5438 }
5439
5440 pub fn verify_user(
5443 &self,
5444 username: &str,
5445 password: &str,
5446 ) -> Result<Option<crate::auth::UserEntry>> {
5447 let cat = self.catalog.read();
5448 let Some(user) = cat.users.iter().find(|u| u.username == username) else {
5449 return Ok(None);
5450 };
5451 if user.password_hash.is_empty() {
5452 return Ok(None);
5453 }
5454 let ok = crate::auth::verify_password(password, &user.password_hash)
5455 .map_err(MongrelError::Other)?;
5456 if ok {
5457 Ok(Some(user.clone()))
5458 } else {
5459 Ok(None)
5460 }
5461 }
5462
5463 pub fn authenticate_principal(
5467 &self,
5468 username: &str,
5469 password: &str,
5470 ) -> Result<Option<crate::auth::Principal>> {
5471 self.authenticate_principal_inner(username, password, || {})
5472 }
5473
5474 fn authenticate_principal_inner<F>(
5475 &self,
5476 username: &str,
5477 password: &str,
5478 after_verify: F,
5479 ) -> Result<Option<crate::auth::Principal>>
5480 where
5481 F: FnOnce(),
5482 {
5483 let catalog = self.catalog.read();
5484 let Some(user) = catalog.users.iter().find(|user| user.username == username) else {
5485 return Ok(None);
5486 };
5487 if user.password_hash.is_empty()
5488 || !crate::auth::verify_password(password, &user.password_hash)
5489 .map_err(MongrelError::Other)?
5490 {
5491 return Ok(None);
5492 }
5493 after_verify();
5494 Ok(Self::resolve_user_principal_from_catalog(&catalog, user))
5495 }
5496
5497 pub fn set_user_admin(&self, username: &str, is_admin: bool) -> Result<()> {
5499 self.set_user_admin_with_epoch(username, is_admin)
5500 .map(|_| ())
5501 }
5502
5503 pub fn set_user_admin_with_epoch(
5504 &self,
5505 username: &str,
5506 is_admin: bool,
5507 ) -> Result<Option<Epoch>> {
5508 self.set_user_admin_with_epoch_inner(username, is_admin, None)
5509 }
5510
5511 pub fn set_user_admin_with_epoch_controlled<F>(
5512 &self,
5513 username: &str,
5514 is_admin: bool,
5515 mut before_publish: F,
5516 ) -> Result<Option<Epoch>>
5517 where
5518 F: FnMut() -> Result<()>,
5519 {
5520 self.set_user_admin_with_epoch_inner(username, is_admin, Some(&mut before_publish))
5521 }
5522
5523 fn set_user_admin_with_epoch_inner(
5524 &self,
5525 username: &str,
5526 is_admin: bool,
5527 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5528 ) -> Result<Option<Epoch>> {
5529 self.require(&crate::auth::Permission::Admin)?;
5530 let _ddl = self.ddl_lock.lock();
5531 let _security_write = self.security_write()?;
5532 self.require(&crate::auth::Permission::Admin)?;
5533 let _commit = self.commit_lock.lock();
5534 let mut next_catalog = self.catalog.read().clone();
5535 let user = next_catalog
5536 .users
5537 .iter_mut()
5538 .find(|u| u.username == username)
5539 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5540 if user.is_admin == is_admin {
5541 return Ok(None);
5542 }
5543 user.is_admin = is_admin;
5544 let epoch = self.epoch.bump_assigned();
5545 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5546 advance_security_version(&mut next_catalog)?;
5547 next_catalog.db_epoch = epoch.0;
5548 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5549 Ok(Some(epoch))
5550 }
5551
5552 pub fn create_role(&self, name: &str) -> Result<crate::auth::RoleEntry> {
5554 self.create_role_inner(name, None)
5555 }
5556
5557 pub fn create_role_controlled<F>(
5558 &self,
5559 name: &str,
5560 mut before_publish: F,
5561 ) -> Result<crate::auth::RoleEntry>
5562 where
5563 F: FnMut() -> Result<()>,
5564 {
5565 self.create_role_inner(name, Some(&mut before_publish))
5566 }
5567
5568 fn create_role_inner(
5569 &self,
5570 name: &str,
5571 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5572 ) -> Result<crate::auth::RoleEntry> {
5573 self.require(&crate::auth::Permission::Admin)?;
5574 let _ddl = self.ddl_lock.lock();
5575 let _security_write = self.security_write()?;
5576 self.require(&crate::auth::Permission::Admin)?;
5577 let _commit = self.commit_lock.lock();
5578 let epoch = self.epoch.bump_assigned();
5579 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5580 let mut next_catalog = self.catalog.read().clone();
5581 if next_catalog.roles.iter().any(|r| r.name == name) {
5582 return Err(MongrelError::InvalidArgument(format!(
5583 "role {name:?} already exists"
5584 )));
5585 }
5586 let entry = crate::auth::RoleEntry {
5587 name: name.into(),
5588 permissions: Vec::new(),
5589 created_epoch: epoch.0,
5590 };
5591 next_catalog.roles.push(entry.clone());
5592 advance_security_version(&mut next_catalog)?;
5593 next_catalog.db_epoch = epoch.0;
5594 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5595 Ok(entry)
5596 }
5597
5598 pub fn drop_role(&self, name: &str) -> Result<()> {
5600 self.drop_role_with_epoch(name).map(|_| ())
5601 }
5602
5603 pub fn drop_role_with_epoch(&self, name: &str) -> Result<Epoch> {
5604 self.drop_role_with_epoch_inner(name, None)
5605 }
5606
5607 pub fn drop_role_with_epoch_controlled<F>(
5608 &self,
5609 name: &str,
5610 mut before_publish: F,
5611 ) -> Result<Epoch>
5612 where
5613 F: FnMut() -> Result<()>,
5614 {
5615 self.drop_role_with_epoch_inner(name, Some(&mut before_publish))
5616 }
5617
5618 fn drop_role_with_epoch_inner(
5619 &self,
5620 name: &str,
5621 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5622 ) -> Result<Epoch> {
5623 self.require(&crate::auth::Permission::Admin)?;
5624 let _ddl = self.ddl_lock.lock();
5625 let _security_write = self.security_write()?;
5626 self.require(&crate::auth::Permission::Admin)?;
5627 let _commit = self.commit_lock.lock();
5628 let epoch = self.epoch.bump_assigned();
5629 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5630 let mut next_catalog = self.catalog.read().clone();
5631 let before = next_catalog.roles.len();
5632 next_catalog.roles.retain(|r| r.name != name);
5633 if next_catalog.roles.len() == before {
5634 return Err(MongrelError::NotFound(format!("role {name:?} not found")));
5635 }
5636 for user in &mut next_catalog.users {
5637 user.roles.retain(|r| r != name);
5638 }
5639 advance_security_version(&mut next_catalog)?;
5640 next_catalog.db_epoch = epoch.0;
5641 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5642 Ok(epoch)
5643 }
5644
5645 pub fn grant_role(&self, username: &str, role_name: &str) -> Result<()> {
5647 self.grant_role_with_epoch(username, role_name).map(|_| ())
5648 }
5649
5650 pub fn grant_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
5651 self.grant_role_with_epoch_inner(username, role_name, None)
5652 }
5653
5654 pub fn grant_role_with_epoch_controlled<F>(
5655 &self,
5656 username: &str,
5657 role_name: &str,
5658 mut before_publish: F,
5659 ) -> Result<Option<Epoch>>
5660 where
5661 F: FnMut() -> Result<()>,
5662 {
5663 self.grant_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
5664 }
5665
5666 fn grant_role_with_epoch_inner(
5667 &self,
5668 username: &str,
5669 role_name: &str,
5670 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5671 ) -> Result<Option<Epoch>> {
5672 self.require(&crate::auth::Permission::Admin)?;
5673 let _ddl = self.ddl_lock.lock();
5674 let _security_write = self.security_write()?;
5675 self.require(&crate::auth::Permission::Admin)?;
5676 let _commit = self.commit_lock.lock();
5677 let mut next_catalog = self.catalog.read().clone();
5678 if !next_catalog.roles.iter().any(|r| r.name == role_name) {
5679 return Err(MongrelError::NotFound(format!(
5680 "role {role_name:?} not found"
5681 )));
5682 }
5683 let user = next_catalog
5684 .users
5685 .iter_mut()
5686 .find(|u| u.username == username)
5687 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5688 if user.roles.iter().any(|role| role == role_name) {
5689 return Ok(None);
5690 }
5691 user.roles.push(role_name.into());
5692 let epoch = self.epoch.bump_assigned();
5693 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5694 advance_security_version(&mut next_catalog)?;
5695 next_catalog.db_epoch = epoch.0;
5696 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5697 Ok(Some(epoch))
5698 }
5699
5700 pub fn revoke_role(&self, username: &str, role_name: &str) -> Result<()> {
5702 self.revoke_role_with_epoch(username, role_name).map(|_| ())
5703 }
5704
5705 pub fn revoke_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
5706 self.revoke_role_with_epoch_inner(username, role_name, None)
5707 }
5708
5709 pub fn revoke_role_with_epoch_controlled<F>(
5710 &self,
5711 username: &str,
5712 role_name: &str,
5713 mut before_publish: F,
5714 ) -> Result<Option<Epoch>>
5715 where
5716 F: FnMut() -> Result<()>,
5717 {
5718 self.revoke_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
5719 }
5720
5721 fn revoke_role_with_epoch_inner(
5722 &self,
5723 username: &str,
5724 role_name: &str,
5725 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5726 ) -> Result<Option<Epoch>> {
5727 self.require(&crate::auth::Permission::Admin)?;
5728 let _ddl = self.ddl_lock.lock();
5729 let _security_write = self.security_write()?;
5730 self.require(&crate::auth::Permission::Admin)?;
5731 let _commit = self.commit_lock.lock();
5732 let mut next_catalog = self.catalog.read().clone();
5733 let user = next_catalog
5734 .users
5735 .iter_mut()
5736 .find(|u| u.username == username)
5737 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5738 let before = user.roles.len();
5739 user.roles.retain(|r| r != role_name);
5740 if user.roles.len() == before {
5741 return Ok(None);
5742 }
5743 let epoch = self.epoch.bump_assigned();
5744 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5745 advance_security_version(&mut next_catalog)?;
5746 next_catalog.db_epoch = epoch.0;
5747 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5748 Ok(Some(epoch))
5749 }
5750
5751 pub fn grant_permission(
5753 &self,
5754 role_name: &str,
5755 permission: crate::auth::Permission,
5756 ) -> Result<()> {
5757 self.grant_permission_with_epoch(role_name, permission)
5758 .map(|_| ())
5759 }
5760
5761 pub fn grant_permission_with_epoch(
5762 &self,
5763 role_name: &str,
5764 permission: crate::auth::Permission,
5765 ) -> Result<Option<Epoch>> {
5766 self.grant_permission_with_epoch_inner(role_name, permission, None)
5767 }
5768
5769 pub fn grant_permission_with_epoch_controlled<F>(
5770 &self,
5771 role_name: &str,
5772 permission: crate::auth::Permission,
5773 mut before_publish: F,
5774 ) -> Result<Option<Epoch>>
5775 where
5776 F: FnMut() -> Result<()>,
5777 {
5778 self.grant_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
5779 }
5780
5781 fn grant_permission_with_epoch_inner(
5782 &self,
5783 role_name: &str,
5784 permission: crate::auth::Permission,
5785 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5786 ) -> Result<Option<Epoch>> {
5787 self.require(&crate::auth::Permission::Admin)?;
5788 let _ddl = self.ddl_lock.lock();
5789 let _security_write = self.security_write()?;
5790 self.require(&crate::auth::Permission::Admin)?;
5791 let _commit = self.commit_lock.lock();
5792 let mut next_catalog = self.catalog.read().clone();
5793 let role = next_catalog
5794 .roles
5795 .iter_mut()
5796 .find(|r| r.name == role_name)
5797 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
5798 let before = role.permissions.clone();
5799 merge_permission(&mut role.permissions, permission);
5800 if role.permissions == before {
5801 return Ok(None);
5802 }
5803 let epoch = self.epoch.bump_assigned();
5804 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5805 advance_security_version(&mut next_catalog)?;
5806 next_catalog.db_epoch = epoch.0;
5807 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5808 Ok(Some(epoch))
5809 }
5810
5811 pub fn revoke_permission(
5813 &self,
5814 role_name: &str,
5815 permission: crate::auth::Permission,
5816 ) -> Result<()> {
5817 self.revoke_permission_with_epoch(role_name, permission)
5818 .map(|_| ())
5819 }
5820
5821 pub fn revoke_permission_with_epoch(
5822 &self,
5823 role_name: &str,
5824 permission: crate::auth::Permission,
5825 ) -> Result<Option<Epoch>> {
5826 self.revoke_permission_with_epoch_inner(role_name, permission, None)
5827 }
5828
5829 pub fn revoke_permission_with_epoch_controlled<F>(
5830 &self,
5831 role_name: &str,
5832 permission: crate::auth::Permission,
5833 mut before_publish: F,
5834 ) -> Result<Option<Epoch>>
5835 where
5836 F: FnMut() -> Result<()>,
5837 {
5838 self.revoke_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
5839 }
5840
5841 fn revoke_permission_with_epoch_inner(
5842 &self,
5843 role_name: &str,
5844 permission: crate::auth::Permission,
5845 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5846 ) -> Result<Option<Epoch>> {
5847 self.require(&crate::auth::Permission::Admin)?;
5848 let _ddl = self.ddl_lock.lock();
5849 let _security_write = self.security_write()?;
5850 self.require(&crate::auth::Permission::Admin)?;
5851 let _commit = self.commit_lock.lock();
5852 let mut next_catalog = self.catalog.read().clone();
5853 let role = next_catalog
5854 .roles
5855 .iter_mut()
5856 .find(|r| r.name == role_name)
5857 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
5858 let before = role.permissions.clone();
5859 revoke_permission_from(&mut role.permissions, &permission);
5860 if role.permissions == before {
5861 return Ok(None);
5862 }
5863 let epoch = self.epoch.bump_assigned();
5864 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5865 advance_security_version(&mut next_catalog)?;
5866 next_catalog.db_epoch = epoch.0;
5867 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5868 Ok(Some(epoch))
5869 }
5870
5871 pub fn resolve_principal(&self, username: &str) -> Option<crate::auth::Principal> {
5874 let cat = self.catalog.read();
5875 Self::resolve_principal_from_catalog(&cat, username)
5876 }
5877
5878 pub fn resolve_current_principal(
5881 &self,
5882 principal: &crate::auth::Principal,
5883 ) -> Option<crate::auth::Principal> {
5884 Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
5885 }
5886
5887 fn resolve_principal_from_catalog(
5892 cat: &Catalog,
5893 username: &str,
5894 ) -> Option<crate::auth::Principal> {
5895 let user = cat.users.iter().find(|u| u.username == username)?;
5896 Self::resolve_user_principal_from_catalog(cat, user)
5897 }
5898
5899 fn resolve_bound_principal_from_catalog(
5900 cat: &Catalog,
5901 principal: &crate::auth::Principal,
5902 ) -> Option<crate::auth::Principal> {
5903 let user = cat.users.iter().find(|user| {
5904 user.id == principal.user_id
5905 && user.created_epoch == principal.created_epoch
5906 && user.username == principal.username
5907 })?;
5908 Self::resolve_user_principal_from_catalog(cat, user)
5909 }
5910
5911 fn resolve_user_principal_from_catalog(
5912 cat: &Catalog,
5913 user: &crate::auth::UserEntry,
5914 ) -> Option<crate::auth::Principal> {
5915 let mut permissions = Vec::new();
5916 for role_name in &user.roles {
5917 if let Some(role) = cat.roles.iter().find(|r| &r.name == role_name) {
5918 permissions.extend(role.permissions.iter().cloned());
5919 }
5920 }
5921 Some(crate::auth::Principal {
5922 user_id: user.id,
5923 created_epoch: user.created_epoch,
5924 username: user.username.clone(),
5925 is_admin: user.is_admin,
5926 roles: user.roles.clone(),
5927 permissions,
5928 })
5929 }
5930
5931 pub fn check_permission(&self, username: &str, permission: &crate::auth::Permission) -> bool {
5933 match self.resolve_principal(username) {
5934 Some(p) => p.has_permission(permission),
5935 None => false,
5936 }
5937 }
5938
5939 pub fn require_auth_enabled(&self) -> bool {
5943 self.catalog.read().require_auth
5944 }
5945
5946 pub fn principal(&self) -> Option<crate::auth::Principal> {
5950 self.principal.read().clone()
5951 }
5952
5953 fn table_auth_checker(&self) -> Option<Arc<dyn crate::auth_state::TableAuthChecker>> {
5959 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
5960 self.auth_state.clone(),
5961 )))
5962 }
5963
5964 pub fn refresh_principal(&self) -> Result<()> {
5978 let previous = match self.principal.read().clone() {
5979 Some(principal) => principal,
5980 None => return Ok(()),
5981 };
5982 let observed_version = self.security_coordinator.version.load(Ordering::Acquire);
5983 self.refresh_security_catalog_if_stale(observed_version)?;
5984 let cat = self.catalog.read();
5985 match Self::resolve_bound_principal_from_catalog(&cat, &previous) {
5986 Some(p) => {
5987 *self.principal.write() = Some(p.clone());
5988 self.auth_state.set_principal(Some(p));
5992 Ok(())
5993 }
5994 None => Err(MongrelError::InvalidCredentials {
5995 username: previous.username,
5996 }),
5997 }
5998 }
5999
6000 pub fn security_catalog_disk_read_count(&self) -> u64 {
6003 self.security_catalog_disk_reads.load(Ordering::Relaxed)
6004 }
6005
6006 pub fn enable_auth(&self, admin_username: &str, admin_password: &str) -> Result<()> {
6018 let password_hash =
6019 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
6020 let _ddl = self.ddl_lock.lock();
6021 let _security_write = self.security_write()?;
6022 let _commit = self.commit_lock.lock();
6023 let epoch = self.epoch.bump_assigned();
6024 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6025 let mut next_catalog = self.catalog.read().clone();
6026 if next_catalog.require_auth {
6027 return Err(MongrelError::InvalidArgument(
6028 "database already has require_auth enabled".into(),
6029 ));
6030 }
6031 if next_catalog
6032 .users
6033 .iter()
6034 .any(|u| u.username == admin_username)
6035 {
6036 return Err(MongrelError::InvalidArgument(format!(
6037 "user {admin_username:?} already exists"
6038 )));
6039 }
6040 next_catalog.next_user_id = next_catalog.next_user_id.max(1);
6041 let id = next_catalog.next_user_id;
6042 next_catalog.next_user_id = id
6043 .checked_add(1)
6044 .ok_or_else(|| MongrelError::Full("user-id namespace exhausted".into()))?;
6045 next_catalog.users.push(crate::auth::UserEntry {
6046 id,
6047 username: admin_username.to_string(),
6048 password_hash,
6049 roles: Vec::new(),
6050 is_admin: true,
6051 created_epoch: epoch.0,
6052 });
6053 next_catalog.require_auth = true;
6054 advance_security_version(&mut next_catalog)?;
6055 next_catalog.db_epoch = epoch.0;
6056 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
6057 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
6061 let principal = crate::auth::Principal {
6062 user_id: id,
6063 created_epoch: epoch.0,
6064 username: admin_username.to_string(),
6065 is_admin: true,
6066 roles: Vec::new(),
6067 permissions: Vec::new(),
6068 };
6069 *self.principal.write() = Some(principal.clone());
6070 self.auth_state.set_principal(Some(principal));
6071 }
6072 publish
6073 }
6074
6075 pub fn disable_auth(&self) -> Result<()> {
6092 let _ddl = self.ddl_lock.lock();
6093 let _security_write = self.security_write()?;
6094 let _commit = self.commit_lock.lock();
6095 let epoch = self.epoch.bump_assigned();
6096 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6097 let mut next_catalog = self.catalog.read().clone();
6098 if !next_catalog.require_auth {
6099 return Err(MongrelError::InvalidArgument(
6100 "database does not have require_auth enabled".into(),
6101 ));
6102 }
6103 next_catalog.require_auth = false;
6104 advance_security_version(&mut next_catalog)?;
6105 next_catalog.db_epoch = epoch.0;
6106 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
6107 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
6109 *self.principal.write() = None;
6110 }
6111 publish
6112 }
6113
6114 pub fn require(&self, perm: &crate::auth::Permission) -> Result<()> {
6121 if self.read_only && !matches!(perm, crate::auth::Permission::Select { .. }) {
6122 return Err(MongrelError::ReadOnlyReplica);
6123 }
6124 if self.principal.read().is_some() {
6125 self.refresh_principal().map_err(|error| match error {
6126 MongrelError::InvalidCredentials { .. } => MongrelError::AuthRequired,
6127 error => error,
6128 })?;
6129 }
6130 if !self.catalog.read().require_auth {
6131 return Ok(());
6132 }
6133 let guard = self.principal.read();
6134 let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
6135 if p.has_permission(perm) {
6136 Ok(())
6137 } else {
6138 Err(MongrelError::PermissionDenied {
6139 required: perm.clone(),
6140 principal: p.username.clone(),
6141 })
6142 }
6143 }
6144
6145 pub fn require_table(
6150 &self,
6151 table: &str,
6152 perm: crate::auth_state::RequiredPermission,
6153 ) -> Result<()> {
6154 self.require(&perm.into_permission(table))
6155 }
6156
6157 pub fn triggers(&self) -> Vec<StoredTrigger> {
6158 self.catalog
6159 .read()
6160 .triggers
6161 .iter()
6162 .map(|t| t.trigger.clone())
6163 .collect()
6164 }
6165
6166 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
6167 self.catalog
6168 .read()
6169 .triggers
6170 .iter()
6171 .find(|t| t.trigger.name == name)
6172 .map(|t| t.trigger.clone())
6173 }
6174
6175 pub fn create_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
6176 self.create_trigger_inner(trigger, None, None)
6177 }
6178
6179 pub fn create_trigger_controlled<F>(
6180 &self,
6181 trigger: StoredTrigger,
6182 mut before_publish: F,
6183 ) -> Result<StoredTrigger>
6184 where
6185 F: FnMut() -> Result<()>,
6186 {
6187 self.create_trigger_inner(trigger, None, Some(&mut before_publish))
6188 }
6189
6190 pub fn create_trigger_as_controlled<F>(
6191 &self,
6192 trigger: StoredTrigger,
6193 principal: Option<&crate::auth::Principal>,
6194 mut before_publish: F,
6195 ) -> Result<StoredTrigger>
6196 where
6197 F: FnMut() -> Result<()>,
6198 {
6199 self.create_trigger_inner(trigger, principal, Some(&mut before_publish))
6200 }
6201
6202 fn create_trigger_inner(
6203 &self,
6204 mut trigger: StoredTrigger,
6205 principal: Option<&crate::auth::Principal>,
6206 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6207 ) -> Result<StoredTrigger> {
6208 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6209 let _g = self.ddl_lock.lock();
6210 let _security_write = self.security_write()?;
6211 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6212 trigger.validate()?;
6213 self.validate_trigger_references(&trigger)
6214 .map_err(trigger_validation_error)?;
6215 {
6216 let cat = self.catalog.read();
6217 if cat.triggers.iter().any(|t| t.trigger.name == trigger.name) {
6218 return Err(MongrelError::TriggerValidation(format!(
6219 "trigger {:?} already exists",
6220 trigger.name
6221 )));
6222 }
6223 }
6224 let commit_lock = Arc::clone(&self.commit_lock);
6225 let _c = commit_lock.lock();
6226 let epoch = self.epoch.bump_assigned();
6227 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6228 trigger.created_epoch = epoch.0;
6229 trigger.updated_epoch = epoch.0;
6230 let mut next_catalog = self.catalog.read().clone();
6231 next_catalog
6232 .triggers
6233 .push(TriggerEntry::from(trigger.clone()));
6234 next_catalog.db_epoch = epoch.0;
6235 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6236 Ok(trigger)
6237 }
6238
6239 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
6240 self.create_or_replace_trigger_inner(trigger, None, None)
6241 }
6242
6243 pub fn create_or_replace_trigger_controlled<F>(
6244 &self,
6245 trigger: StoredTrigger,
6246 mut before_publish: F,
6247 ) -> Result<StoredTrigger>
6248 where
6249 F: FnMut() -> Result<()>,
6250 {
6251 self.create_or_replace_trigger_inner(trigger, None, Some(&mut before_publish))
6252 }
6253
6254 pub fn create_or_replace_trigger_as_controlled<F>(
6255 &self,
6256 trigger: StoredTrigger,
6257 principal: Option<&crate::auth::Principal>,
6258 mut before_publish: F,
6259 ) -> Result<StoredTrigger>
6260 where
6261 F: FnMut() -> Result<()>,
6262 {
6263 self.create_or_replace_trigger_inner(trigger, principal, Some(&mut before_publish))
6264 }
6265
6266 fn create_or_replace_trigger_inner(
6267 &self,
6268 trigger: StoredTrigger,
6269 principal: Option<&crate::auth::Principal>,
6270 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6271 ) -> Result<StoredTrigger> {
6272 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6273 let _g = self.ddl_lock.lock();
6274 let _security_write = self.security_write()?;
6275 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6276 trigger.validate()?;
6277 self.validate_trigger_references(&trigger)
6278 .map_err(trigger_validation_error)?;
6279 let commit_lock = Arc::clone(&self.commit_lock);
6280 let _c = commit_lock.lock();
6281 let epoch = self.epoch.bump_assigned();
6282 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6283 let mut next_catalog = self.catalog.read().clone();
6284 let replaced = {
6285 let next = match next_catalog
6286 .triggers
6287 .iter()
6288 .position(|t| t.trigger.name == trigger.name)
6289 {
6290 Some(idx) => {
6291 let next = next_catalog.triggers[idx]
6292 .trigger
6293 .replaced(trigger.clone(), epoch.0)?;
6294 next_catalog.triggers[idx] = TriggerEntry::from(next.clone());
6295 next
6296 }
6297 None => {
6298 let mut next = trigger;
6299 next.created_epoch = epoch.0;
6300 next.updated_epoch = epoch.0;
6301 next_catalog.triggers.push(TriggerEntry::from(next.clone()));
6302 next
6303 }
6304 };
6305 next_catalog.db_epoch = epoch.0;
6306 next
6307 };
6308 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6309 Ok(replaced)
6310 }
6311
6312 pub fn drop_trigger(&self, name: &str) -> Result<()> {
6313 self.drop_trigger_with_epoch(name).map(|_| ())
6314 }
6315
6316 pub fn drop_trigger_with_epoch(&self, name: &str) -> Result<Epoch> {
6318 self.drop_triggers_with_epoch(&[name.to_string()])
6319 }
6320
6321 pub fn drop_trigger_with_epoch_controlled<F>(
6322 &self,
6323 name: &str,
6324 before_publish: F,
6325 ) -> Result<Epoch>
6326 where
6327 F: FnMut() -> Result<()>,
6328 {
6329 self.drop_triggers_with_epoch_controlled(&[name.to_string()], before_publish)
6330 }
6331
6332 pub fn drop_triggers_with_epoch(&self, names: &[String]) -> Result<Epoch> {
6334 self.drop_triggers_with_epoch_inner(names, None, None)
6335 }
6336
6337 pub fn drop_triggers_with_epoch_controlled<F>(
6338 &self,
6339 names: &[String],
6340 mut before_publish: F,
6341 ) -> Result<Epoch>
6342 where
6343 F: FnMut() -> Result<()>,
6344 {
6345 self.drop_triggers_with_epoch_inner(names, None, Some(&mut before_publish))
6346 }
6347
6348 pub fn drop_triggers_with_epoch_as_controlled<F>(
6349 &self,
6350 names: &[String],
6351 principal: Option<&crate::auth::Principal>,
6352 mut before_publish: F,
6353 ) -> Result<Epoch>
6354 where
6355 F: FnMut() -> Result<()>,
6356 {
6357 self.drop_triggers_with_epoch_inner(names, principal, Some(&mut before_publish))
6358 }
6359
6360 fn drop_triggers_with_epoch_inner(
6361 &self,
6362 names: &[String],
6363 principal: Option<&crate::auth::Principal>,
6364 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6365 ) -> Result<Epoch> {
6366 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6367 if names.is_empty() {
6368 return Err(MongrelError::InvalidArgument(
6369 "at least one trigger name is required".into(),
6370 ));
6371 }
6372 let _g = self.ddl_lock.lock();
6373 let _security_write = self.security_write()?;
6374 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6375 {
6376 let cat = self.catalog.read();
6377 for name in names {
6378 if !cat.triggers.iter().any(|t| t.trigger.name == *name) {
6379 return Err(MongrelError::NotFound(format!(
6380 "trigger {name:?} not found"
6381 )));
6382 }
6383 }
6384 }
6385 let commit_lock = Arc::clone(&self.commit_lock);
6386 let _c = commit_lock.lock();
6387 let epoch = self.epoch.bump_assigned();
6388 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6389 let mut next_catalog = self.catalog.read().clone();
6390 next_catalog
6391 .triggers
6392 .retain(|trigger| !names.contains(&trigger.trigger.name));
6393 next_catalog.db_epoch = epoch.0;
6394 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6395 Ok(epoch)
6396 }
6397
6398 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
6399 self.catalog.read().external_tables.clone()
6400 }
6401
6402 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
6403 self.catalog
6404 .read()
6405 .external_tables
6406 .iter()
6407 .find(|t| t.name == name)
6408 .cloned()
6409 }
6410
6411 pub fn create_external_table(&self, entry: ExternalTableEntry) -> Result<ExternalTableEntry> {
6412 self.create_external_table_inner(entry, None)
6413 }
6414
6415 pub fn create_external_table_controlled<F>(
6416 &self,
6417 entry: ExternalTableEntry,
6418 mut before_publish: F,
6419 ) -> Result<ExternalTableEntry>
6420 where
6421 F: FnMut() -> Result<()>,
6422 {
6423 self.create_external_table_inner(entry, Some(&mut before_publish))
6424 }
6425
6426 fn create_external_table_inner(
6427 &self,
6428 mut entry: ExternalTableEntry,
6429 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6430 ) -> Result<ExternalTableEntry> {
6431 self.require(&crate::auth::Permission::Ddl)?;
6432 let _g = self.ddl_lock.lock();
6433 let _security_write = self.security_write()?;
6434 self.require(&crate::auth::Permission::Ddl)?;
6435 entry.validate()?;
6436 {
6437 let cat = self.catalog.read();
6438 if cat.live(&entry.name).is_some()
6439 || cat.external_tables.iter().any(|t| t.name == entry.name)
6440 {
6441 return Err(MongrelError::InvalidArgument(format!(
6442 "table {:?} already exists",
6443 entry.name
6444 )));
6445 }
6446 }
6447 let commit_lock = Arc::clone(&self.commit_lock);
6448 let _c = commit_lock.lock();
6449 crate::durable_file::create_directory(&self.root.join(VTAB_DIR))?;
6453 crate::durable_file::remove_directory_all(&self.root.join(VTAB_DIR).join(&entry.name))?;
6454 let epoch = self.epoch.bump_assigned();
6455 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6456 entry.created_epoch = epoch.0;
6457 let mut next_catalog = self.catalog.read().clone();
6458 next_catalog.external_tables.push(entry.clone());
6459 next_catalog.db_epoch = epoch.0;
6460 self.publish_catalog_candidate_with_prelude(
6461 next_catalog,
6462 epoch,
6463 &mut _epoch_guard,
6464 before_publish,
6465 vec![(
6466 EXTERNAL_TABLE_ID,
6467 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
6468 name: entry.name.clone(),
6469 generation_epoch: epoch.0,
6470 }),
6471 )],
6472 )?;
6473 Ok(entry)
6474 }
6475
6476 pub fn drop_external_table(&self, name: &str) -> Result<()> {
6477 self.drop_external_table_with_epoch(name).map(|_| ())
6478 }
6479
6480 pub fn drop_external_table_with_epoch(&self, name: &str) -> Result<Epoch> {
6482 self.drop_external_table_with_epoch_inner(name, None)
6483 }
6484
6485 pub fn drop_external_table_with_epoch_controlled<F>(
6486 &self,
6487 name: &str,
6488 mut before_publish: F,
6489 ) -> Result<Epoch>
6490 where
6491 F: FnMut() -> Result<()>,
6492 {
6493 self.drop_external_table_with_epoch_inner(name, Some(&mut before_publish))
6494 }
6495
6496 fn drop_external_table_with_epoch_inner(
6497 &self,
6498 name: &str,
6499 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6500 ) -> Result<Epoch> {
6501 self.require(&crate::auth::Permission::Ddl)?;
6502 let _g = self.ddl_lock.lock();
6503 let _security_write = self.security_write()?;
6504 self.require(&crate::auth::Permission::Ddl)?;
6505 let commit_lock = Arc::clone(&self.commit_lock);
6506 let _c = commit_lock.lock();
6507 let epoch = self.epoch.bump_assigned();
6508 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6509 let mut next_catalog = self.catalog.read().clone();
6510 let before = next_catalog.external_tables.len();
6511 next_catalog.external_tables.retain(|t| t.name != name);
6512 if next_catalog.external_tables.len() == before {
6513 return Err(MongrelError::NotFound(format!(
6514 "external table {name:?} not found"
6515 )));
6516 }
6517 next_catalog.db_epoch = epoch.0;
6518 self.publish_catalog_candidate_with_prelude(
6519 next_catalog,
6520 epoch,
6521 &mut _epoch_guard,
6522 before_publish,
6523 vec![(
6524 EXTERNAL_TABLE_ID,
6525 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
6526 name: name.to_string(),
6527 generation_epoch: epoch.0,
6528 }),
6529 )],
6530 )?;
6531 let state_dir = self.root.join(VTAB_DIR).join(name);
6532 if let Err(error) = crate::durable_file::remove_directory_all(&state_dir) {
6533 return Err(MongrelError::DurableCommit {
6534 epoch: epoch.0,
6535 message: format!(
6536 "external table was dropped but connector-state cleanup failed: {error}"
6537 ),
6538 });
6539 }
6540 Ok(epoch)
6541 }
6542
6543 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
6544 let txn_id = self.alloc_txn_id()?;
6545 let (principal, catalog_bound) = self.transaction_principal_snapshot();
6546 self.commit_transaction_with_external_states(
6547 txn_id,
6548 self.epoch.visible(),
6549 Vec::new(),
6550 vec![(name.to_string(), state.to_vec())],
6551 Vec::new(),
6552 principal,
6553 catalog_bound,
6554 None,
6555 )
6556 .map(|(epoch, _)| epoch)
6557 }
6558
6559 pub fn trigger_config(&self) -> TriggerConfig {
6560 use std::sync::atomic::Ordering;
6561 TriggerConfig {
6562 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
6563 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
6564 max_loop_iterations: self.trigger_max_loop_iterations.load(Ordering::Relaxed),
6565 }
6566 }
6567
6568 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
6569 use std::sync::atomic::Ordering;
6570 if config.max_depth == 0 {
6571 return Err(MongrelError::InvalidArgument(
6572 "trigger max_depth must be greater than 0".into(),
6573 ));
6574 }
6575 self.trigger_recursive
6576 .store(config.recursive_triggers, Ordering::Relaxed);
6577 self.trigger_max_depth
6578 .store(config.max_depth, Ordering::Relaxed);
6579 self.trigger_max_loop_iterations
6580 .store(config.max_loop_iterations, Ordering::Relaxed);
6581 Ok(())
6582 }
6583
6584 pub fn set_recursive_triggers(&self, recursive: bool) {
6585 use std::sync::atomic::Ordering;
6586 self.trigger_recursive.store(recursive, Ordering::Relaxed);
6587 }
6588
6589 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent> {
6593 self.notify.subscribe()
6594 }
6595
6596 pub fn subscribe_change_commits(&self) -> tokio::sync::broadcast::Receiver<()> {
6597 self.change_wake.subscribe()
6598 }
6599
6600 pub fn change_events_since(&self, last_event_id: Option<&str>) -> Result<CdcBatch> {
6605 let control = crate::ExecutionControl::new(None);
6606 self.change_events_since_controlled(last_event_id, &control)
6607 }
6608
6609 pub fn change_events_since_controlled(
6611 &self,
6612 last_event_id: Option<&str>,
6613 control: &crate::ExecutionControl,
6614 ) -> Result<CdcBatch> {
6615 use crate::wal::Op;
6616
6617 control.checkpoint()?;
6618 let resume = match last_event_id {
6619 Some(id) => {
6620 let (epoch, index) = id.split_once(':').ok_or_else(|| {
6621 MongrelError::InvalidArgument(format!(
6622 "invalid CDC event id {id:?}; expected <epoch>:<index>"
6623 ))
6624 })?;
6625 Some((
6626 epoch.parse::<u64>().map_err(|error| {
6627 MongrelError::InvalidArgument(format!("invalid CDC epoch: {error}"))
6628 })?,
6629 index.parse::<u32>().map_err(|error| {
6630 MongrelError::InvalidArgument(format!("invalid CDC index: {error}"))
6631 })?,
6632 ))
6633 }
6634 None => None,
6635 };
6636
6637 let mut wal = self.shared_wal.lock();
6638 wal.group_sync()?;
6639 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
6640 let records = crate::wal::SharedWal::replay_with_dek_controlled(
6641 &self.root,
6642 wal_dek.as_ref(),
6643 control,
6644 CDC_MAX_WAL_RECORDS,
6645 CDC_MAX_WAL_REPLAY_BYTES,
6646 )?;
6647 drop(wal);
6648 control.checkpoint()?;
6649
6650 let mut commits: HashMap<u64, (u64, Vec<crate::wal::AddedRun>)> = HashMap::new();
6651 let mut spilled_payloads: HashMap<(u64, u64), Vec<&[u8]>> = HashMap::new();
6652 for (index, record) in records.iter().enumerate() {
6653 if index % 256 == 0 {
6654 control.checkpoint()?;
6655 }
6656 if let Op::TxnCommit { epoch, added_runs } = &record.op {
6657 commits.insert(record.txn_id, (*epoch, added_runs.clone()));
6658 }
6659 if let Op::SpilledRows { table_id, rows } = &record.op {
6660 spilled_payloads
6661 .entry((record.txn_id, *table_id))
6662 .or_default()
6663 .push(rows);
6664 }
6665 }
6666 let earliest_epoch = commits.values().map(|(epoch, _)| *epoch).min();
6667 let current_epoch = self.epoch.committed().0;
6668 let retention_floor = crate::replication::replication_wal_floor(&self.root)?;
6669 let gap = resume.is_some_and(|(epoch, _)| {
6670 retention_floor != 0 && epoch <= retention_floor && epoch <= current_epoch
6671 });
6672 if gap {
6673 return Ok(CdcBatch {
6674 events: Vec::new(),
6675 current_epoch,
6676 earliest_epoch,
6677 gap: true,
6678 });
6679 }
6680
6681 let table_names: HashMap<u64, String> = self
6682 .catalog
6683 .read()
6684 .tables
6685 .iter()
6686 .map(|entry| (entry.table_id, entry.name.clone()))
6687 .collect();
6688 let mut before_images: HashMap<(u64, u64, u64), crate::memtable::Row> = HashMap::new();
6689 let mut retained_bytes = 0_usize;
6690 for (index, record) in records.iter().enumerate() {
6691 if index % 256 == 0 {
6692 control.checkpoint()?;
6693 }
6694 if !commits.contains_key(&record.txn_id) {
6695 continue;
6696 }
6697 let Op::BeforeImage {
6698 table_id,
6699 row_id,
6700 row,
6701 } = &record.op
6702 else {
6703 continue;
6704 };
6705 if row.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
6706 return Err(MongrelError::ResourceLimitExceeded {
6707 resource: "CDC before-image bytes",
6708 requested: row.len(),
6709 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
6710 });
6711 }
6712 let before: crate::memtable::Row = bincode::deserialize(row)?;
6713 if before_images.len() >= CDC_MAX_ROWS {
6714 return Err(MongrelError::ResourceLimitExceeded {
6715 resource: "CDC before-image rows",
6716 requested: before_images.len().saturating_add(1),
6717 limit: CDC_MAX_ROWS,
6718 });
6719 }
6720 charge_cdc_bytes(
6721 &mut retained_bytes,
6722 cdc_row_storage_bytes(&before),
6723 "CDC retained bytes",
6724 )?;
6725 before_images.insert((record.txn_id, *table_id, row_id.0), before);
6726 }
6727 let mut operation_indices: HashMap<u64, u32> = HashMap::new();
6728 let mut events = Vec::new();
6729 let mut decoded_rows = before_images.len();
6730 for (record_index, record) in records.iter().enumerate() {
6731 if record_index % 256 == 0 {
6732 control.checkpoint()?;
6733 }
6734 let Some((commit_epoch, _)) = commits.get(&record.txn_id) else {
6735 continue;
6736 };
6737 let event = match &record.op {
6738 Op::Put { table_id, rows } => {
6739 if rows.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
6740 return Err(MongrelError::ResourceLimitExceeded {
6741 resource: "CDC inline row bytes",
6742 requested: rows.len(),
6743 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
6744 });
6745 }
6746 let rows: Vec<crate::memtable::Row> = bincode::deserialize(rows)?;
6747 decoded_rows = decoded_rows.saturating_add(rows.len());
6748 if decoded_rows > CDC_MAX_ROWS {
6749 return Err(MongrelError::ResourceLimitExceeded {
6750 resource: "CDC decoded rows",
6751 requested: decoded_rows,
6752 limit: CDC_MAX_ROWS,
6753 });
6754 }
6755 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(512);
6756 let mut peak_bytes = retained_bytes;
6757 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
6758 let data = serde_json::to_value(rows)
6759 .map_err(|error| MongrelError::Other(format!("CDC JSON: {error}")))?;
6760 Some((*table_id, "put", data, event_bytes))
6761 }
6762 Op::Delete { table_id, row_ids } => {
6763 let before = row_ids
6764 .iter()
6765 .filter_map(|row_id| {
6766 before_images
6767 .get(&(record.txn_id, *table_id, row_id.0))
6768 .cloned()
6769 })
6770 .collect::<Vec<_>>();
6771 let event_bytes = cdc_rows_json_bytes(&before)
6772 .saturating_add(
6773 row_ids
6774 .len()
6775 .saturating_mul(std::mem::size_of::<serde_json::Value>()),
6776 )
6777 .saturating_add(512);
6778 let mut peak_bytes = retained_bytes;
6779 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
6780 Some((
6781 *table_id,
6782 "delete",
6783 serde_json::json!({
6784 "row_ids": row_ids.iter().map(|row_id| row_id.0).collect::<Vec<_>>(),
6785 "before": before,
6786 }),
6787 event_bytes,
6788 ))
6789 }
6790 Op::TruncateTable { table_id } => {
6791 Some((*table_id, "truncate", serde_json::Value::Null, 512))
6792 }
6793 _ => None,
6794 };
6795 if let Some((table_id, op, data, event_bytes)) = event {
6796 let index = operation_indices.entry(record.txn_id).or_insert(0);
6797 let event_position = (*commit_epoch, *index);
6798 *index = index.saturating_add(1);
6799 if resume.is_some_and(|position| event_position <= position) {
6800 continue;
6801 }
6802 if events.len() >= CDC_MAX_EVENTS {
6803 return Err(MongrelError::ResourceLimitExceeded {
6804 resource: "CDC events",
6805 requested: events.len().saturating_add(1),
6806 limit: CDC_MAX_EVENTS,
6807 });
6808 }
6809 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
6810 events.push(ChangeEvent {
6811 id: Some(format!("{}:{}", event_position.0, event_position.1)),
6812 channel: "changes".into(),
6813 table_id: Some(table_id),
6814 table: table_names.get(&table_id).cloned().unwrap_or_default(),
6815 op: op.into(),
6816 epoch: *commit_epoch,
6817 txn_id: Some(record.txn_id),
6818 message: None,
6819 data: Some(data),
6820 });
6821 }
6822 if let Op::TxnCommit { added_runs, .. } = &record.op {
6823 for run in added_runs {
6824 control.checkpoint()?;
6825 let index = operation_indices.entry(record.txn_id).or_insert(0);
6826 let event_position = (*commit_epoch, *index);
6827 *index = index.saturating_add(1);
6828 if resume.is_some_and(|position| event_position <= position) {
6829 continue;
6830 }
6831 let mut rows = if let Some(payloads) =
6832 spilled_payloads.get(&(record.txn_id, run.table_id))
6833 {
6834 let mut rows = Vec::new();
6835 for payload in payloads {
6836 control.checkpoint()?;
6837 if payload.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
6838 return Err(MongrelError::ResourceLimitExceeded {
6839 resource: "CDC spilled row bytes",
6840 requested: payload.len(),
6841 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
6842 });
6843 }
6844 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(payload)?;
6845 if decoded_rows
6846 .saturating_add(rows.len())
6847 .saturating_add(chunk.len())
6848 > CDC_MAX_ROWS
6849 {
6850 return Err(MongrelError::ResourceLimitExceeded {
6851 resource: "CDC decoded rows",
6852 requested: decoded_rows
6853 .saturating_add(rows.len())
6854 .saturating_add(chunk.len()),
6855 limit: CDC_MAX_ROWS,
6856 });
6857 }
6858 rows.extend(chunk);
6859 }
6860 rows
6861 } else {
6862 let Some(handle) = self.tables.read().get(&run.table_id).cloned() else {
6863 return Ok(CdcBatch {
6864 events: Vec::new(),
6865 current_epoch,
6866 earliest_epoch,
6867 gap: true,
6868 });
6869 };
6870 let table = handle.lock();
6871 let mut reader = match table.open_reader(run.run_id) {
6872 Ok(reader) => reader,
6873 Err(_) => {
6874 return Ok(CdcBatch {
6875 events: Vec::new(),
6876 current_epoch,
6877 earliest_epoch,
6878 gap: true,
6879 })
6880 }
6881 };
6882 let remaining = CDC_MAX_ROWS.saturating_sub(decoded_rows);
6883 let rows = reader.all_rows_controlled(control, remaining)?;
6884 drop(reader);
6885 drop(table);
6886 rows
6887 };
6888 for row in &mut rows {
6889 row.committed_epoch = Epoch(*commit_epoch);
6890 }
6891 decoded_rows = decoded_rows.saturating_add(rows.len());
6892 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(768);
6893 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
6894 if events.len() >= CDC_MAX_EVENTS {
6895 return Err(MongrelError::ResourceLimitExceeded {
6896 resource: "CDC events",
6897 requested: events.len().saturating_add(1),
6898 limit: CDC_MAX_EVENTS,
6899 });
6900 }
6901 events.push(ChangeEvent {
6902 id: Some(format!("{}:{}", event_position.0, event_position.1)),
6903 channel: "changes".into(),
6904 table_id: Some(run.table_id),
6905 table: table_names.get(&run.table_id).cloned().unwrap_or_default(),
6906 op: "put_run".into(),
6907 epoch: *commit_epoch,
6908 txn_id: Some(record.txn_id),
6909 message: None,
6910 data: Some(serde_json::json!({
6911 "run_id": run.run_id.to_string(),
6912 "row_count": run.row_count,
6913 "min_row_id": run.min_row_id,
6914 "max_row_id": run.max_row_id,
6915 "rows": rows,
6916 })),
6917 });
6918 }
6919 }
6920 }
6921 control.checkpoint()?;
6922 Ok(CdcBatch {
6923 events,
6924 current_epoch,
6925 earliest_epoch,
6926 gap: false,
6927 })
6928 }
6929
6930 pub fn notify(&self, channel: &str, message: Option<String>) {
6933 let _ = self.notify.send(ChangeEvent {
6934 id: None,
6935 channel: channel.to_string(),
6936 table_id: None,
6937 table: String::new(),
6938 op: "notify".into(),
6939 epoch: self.epoch.visible().0,
6940 txn_id: None,
6941 message,
6942 data: None,
6943 });
6944 }
6945
6946 pub fn call_procedure(
6947 &self,
6948 name: &str,
6949 args: HashMap<String, crate::Value>,
6950 ) -> Result<ProcedureCallResult> {
6951 self.call_procedure_as(name, args, None)
6952 }
6953
6954 pub fn call_procedure_as(
6955 &self,
6956 name: &str,
6957 args: HashMap<String, crate::Value>,
6958 principal: Option<&crate::auth::Principal>,
6959 ) -> Result<ProcedureCallResult> {
6960 let control = crate::ExecutionControl::new(None);
6961 self.call_procedure_as_controlled(name, args, principal, &control, || true)
6962 }
6963
6964 #[doc(hidden)]
6967 pub fn call_procedure_as_bound(
6968 &self,
6969 expected: &StoredProcedure,
6970 args: HashMap<String, crate::Value>,
6971 principal: Option<&crate::auth::Principal>,
6972 ) -> Result<ProcedureCallResult> {
6973 self.require_for(principal, &crate::auth::Permission::All)?;
6974 let procedure = self.procedure(&expected.name).ok_or_else(|| {
6975 MongrelError::NotFound(format!("procedure {:?} not found", expected.name))
6976 })?;
6977 if &procedure != expected {
6978 return Err(MongrelError::Conflict(format!(
6979 "procedure {:?} changed after request authorization",
6980 expected.name
6981 )));
6982 }
6983 let control = crate::ExecutionControl::new(None);
6984 self.execute_procedure_as_controlled(procedure, args, principal, &control, || true)
6985 }
6986
6987 #[doc(hidden)]
6992 pub fn call_procedure_as_controlled<F>(
6993 &self,
6994 name: &str,
6995 args: HashMap<String, crate::Value>,
6996 principal: Option<&crate::auth::Principal>,
6997 control: &crate::ExecutionControl,
6998 before_commit: F,
6999 ) -> Result<ProcedureCallResult>
7000 where
7001 F: FnOnce() -> bool,
7002 {
7003 self.require_for(principal, &crate::auth::Permission::All)?;
7007 let procedure = self
7008 .procedure(name)
7009 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
7010 self.execute_procedure_as_controlled(procedure, args, principal, control, before_commit)
7011 }
7012
7013 fn execute_procedure_as_controlled<F>(
7014 &self,
7015 procedure: StoredProcedure,
7016 args: HashMap<String, crate::Value>,
7017 principal: Option<&crate::auth::Principal>,
7018 control: &crate::ExecutionControl,
7019 before_commit: F,
7020 ) -> Result<ProcedureCallResult>
7021 where
7022 F: FnOnce() -> bool,
7023 {
7024 let args = bind_procedure_args(&procedure, args)?;
7025 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
7026 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
7027 if has_writes {
7028 let mut tx = self.begin_as(principal.cloned());
7029 let run = (|| {
7030 for (step_index, step) in procedure.body.steps.iter().enumerate() {
7031 if step_index % 256 == 0 {
7032 control.checkpoint()?;
7033 }
7034 let output = self.execute_procedure_step(
7035 step,
7036 &args,
7037 &outputs,
7038 Some(&mut tx),
7039 principal,
7040 Some(control),
7041 )?;
7042 outputs.insert(step.id().to_string(), output);
7043 }
7044 control.checkpoint()?;
7045 eval_return_output(&procedure.body.return_value, &args, &outputs)
7046 })();
7047 match run {
7048 Ok(output) => {
7049 control.checkpoint()?;
7050 if !before_commit() {
7051 tx.rollback();
7052 return Err(MongrelError::Cancelled);
7053 }
7054 let epoch = tx.commit()?.0;
7055 Ok(ProcedureCallResult {
7056 epoch: Some(epoch),
7057 output,
7058 })
7059 }
7060 Err(e) => {
7061 tx.rollback();
7062 Err(e)
7063 }
7064 }
7065 } else {
7066 for (step_index, step) in procedure.body.steps.iter().enumerate() {
7067 if step_index % 256 == 0 {
7068 control.checkpoint()?;
7069 }
7070 let output = self.execute_procedure_step(
7071 step,
7072 &args,
7073 &outputs,
7074 None,
7075 principal,
7076 Some(control),
7077 )?;
7078 outputs.insert(step.id().to_string(), output);
7079 }
7080 control.checkpoint()?;
7081 Ok(ProcedureCallResult {
7082 epoch: None,
7083 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
7084 })
7085 }
7086 }
7087
7088 fn execute_procedure_step(
7089 &self,
7090 step: &ProcedureStep,
7091 args: &HashMap<String, crate::Value>,
7092 outputs: &HashMap<String, ProcedureCallOutput>,
7093 tx: Option<&mut crate::txn::Transaction<'_>>,
7094 principal: Option<&crate::auth::Principal>,
7095 control: Option<&crate::ExecutionControl>,
7096 ) -> Result<ProcedureCallOutput> {
7097 if let Some(control) = control {
7098 control.checkpoint()?;
7099 }
7100 match step {
7101 ProcedureStep::NativeQuery {
7102 table,
7103 conditions,
7104 projection,
7105 limit,
7106 ..
7107 } => {
7108 let mut q = crate::Query::new();
7109 for condition in conditions {
7110 q = q.and(eval_condition(condition, args, outputs)?);
7111 }
7112 let fallback_control = crate::ExecutionControl::new(None);
7113 let query_control = control.unwrap_or(&fallback_control);
7114 let mut rows = self.query_for_principal_controlled(
7115 table,
7116 &q,
7117 projection.as_deref(),
7118 principal,
7119 false,
7120 query_control,
7121 )?;
7122 if let Some(limit) = limit {
7123 rows.truncate(*limit);
7124 }
7125 let mut output = Vec::with_capacity(rows.len());
7126 for (row_index, row) in rows.into_iter().enumerate() {
7127 if row_index % 256 == 0 {
7128 if let Some(control) = control {
7129 control.checkpoint()?;
7130 }
7131 }
7132 output.push(ProcedureCallRow {
7133 row_id: Some(row.row_id),
7134 columns: row.columns,
7135 });
7136 }
7137 Ok(ProcedureCallOutput::Rows(output))
7138 }
7139 ProcedureStep::Put {
7140 table,
7141 cells,
7142 returning,
7143 ..
7144 } => {
7145 let tx = tx.ok_or_else(|| {
7146 MongrelError::InvalidArgument(
7147 "write procedure step requires a transaction".into(),
7148 )
7149 })?;
7150 let cells = eval_cells(cells, args, outputs)?;
7151 if *returning {
7152 let out = tx.put_returning(table, cells)?;
7153 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
7154 row_id: None,
7155 columns: out.row.columns.into_iter().collect(),
7156 }))
7157 } else {
7158 tx.put(table, cells)?;
7159 Ok(ProcedureCallOutput::Null)
7160 }
7161 }
7162 ProcedureStep::Upsert {
7163 table,
7164 cells,
7165 update_cells,
7166 returning,
7167 ..
7168 } => {
7169 let tx = tx.ok_or_else(|| {
7170 MongrelError::InvalidArgument(
7171 "write procedure step requires a transaction".into(),
7172 )
7173 })?;
7174 let cells = eval_cells(cells, args, outputs)?;
7175 let action = match update_cells {
7176 Some(update_cells) => {
7177 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
7178 }
7179 None => crate::UpsertAction::DoNothing,
7180 };
7181 let out = tx.upsert(table, cells, action)?;
7182 if *returning {
7183 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
7184 row_id: None,
7185 columns: out.row.columns.into_iter().collect(),
7186 }))
7187 } else {
7188 Ok(ProcedureCallOutput::Null)
7189 }
7190 }
7191 ProcedureStep::DeleteByPk { table, pk, .. } => {
7192 let tx = tx.ok_or_else(|| {
7193 MongrelError::InvalidArgument(
7194 "write procedure step requires a transaction".into(),
7195 )
7196 })?;
7197 let pk = eval_value(pk, args, outputs)?;
7198 let handle = self.table(table)?;
7199 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
7200 MongrelError::NotFound("procedure delete_by_pk target not found".into())
7201 })?;
7202 tx.delete(table, row_id)?;
7203 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
7204 }
7205 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
7206 "DeleteRows procedure step is not supported by the core executor yet".into(),
7207 )),
7208 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
7209 "SqlQuery procedure step must be executed by mongreldb-query".into(),
7210 )),
7211 }
7212 }
7213
7214 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
7215 let cat = self.catalog.read();
7216 for step in &procedure.body.steps {
7217 let Some(table_name) = step.table() else {
7218 continue;
7219 };
7220 let schema = &cat
7221 .live(table_name)
7222 .ok_or_else(|| {
7223 MongrelError::InvalidArgument(format!(
7224 "procedure {:?} references unknown table {table_name:?}",
7225 procedure.name
7226 ))
7227 })?
7228 .schema;
7229 match step {
7230 ProcedureStep::NativeQuery {
7231 conditions,
7232 projection,
7233 ..
7234 } => {
7235 for condition in conditions {
7236 validate_condition_columns(condition, schema)?;
7237 }
7238 if let Some(projection) = projection {
7239 for id in projection {
7240 validate_column_id(*id, schema)?;
7241 }
7242 }
7243 }
7244 ProcedureStep::Put { cells, .. } => {
7245 for cell in cells {
7246 validate_column_id(cell.column_id, schema)?;
7247 }
7248 }
7249 ProcedureStep::Upsert {
7250 cells,
7251 update_cells,
7252 ..
7253 } => {
7254 for cell in cells {
7255 validate_column_id(cell.column_id, schema)?;
7256 }
7257 if let Some(update_cells) = update_cells {
7258 for cell in update_cells {
7259 validate_column_id(cell.column_id, schema)?;
7260 }
7261 }
7262 }
7263 ProcedureStep::DeleteByPk { .. } => {
7264 if schema.primary_key().is_none() {
7265 return Err(MongrelError::InvalidArgument(format!(
7266 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
7267 procedure.name
7268 )));
7269 }
7270 }
7271 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
7272 }
7273 }
7274 Ok(())
7275 }
7276
7277 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
7278 let cat = self.catalog.read();
7279 let target_schema = match &trigger.target {
7280 TriggerTarget::Table(target_name) => cat
7281 .live(target_name)
7282 .ok_or_else(|| {
7283 MongrelError::InvalidArgument(format!(
7284 "trigger {:?} references unknown target table {target_name:?}",
7285 trigger.name
7286 ))
7287 })?
7288 .schema
7289 .clone(),
7290 TriggerTarget::View(_) => Schema {
7291 columns: trigger.target_columns.clone(),
7292 ..Schema::default()
7293 },
7294 };
7295 for col in &trigger.update_of {
7296 if target_schema.column(col).is_none() {
7297 return Err(MongrelError::InvalidArgument(format!(
7298 "trigger {:?} UPDATE OF references unknown column {col:?}",
7299 trigger.name
7300 )));
7301 }
7302 }
7303 if let Some(expr) = &trigger.when {
7304 validate_trigger_expr(expr, &target_schema, trigger.event)?;
7305 }
7306 let mut select_schemas: HashMap<String, &Schema> = HashMap::new();
7307 for step in &trigger.program.steps {
7308 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
7309 {
7310 return Err(MongrelError::InvalidArgument(
7311 "SetNew trigger steps are only valid in BEFORE triggers".into(),
7312 ));
7313 }
7314 validate_trigger_step(
7315 step,
7316 &cat,
7317 &target_schema,
7318 trigger.event,
7319 &mut select_schemas,
7320 )?;
7321 }
7322 Ok(())
7323 }
7324
7325 pub fn begin(&self) -> crate::txn::Transaction<'_> {
7327 self.begin_with_isolation(crate::txn::IsolationLevel::default())
7328 }
7329
7330 fn transaction_principal_snapshot(&self) -> (Option<crate::auth::Principal>, bool) {
7331 let principal = self.principal.read().clone();
7332 let catalog_bound = principal.as_ref().is_some_and(|principal| {
7333 let catalog = self.catalog.read();
7334 catalog.require_auth || principal.user_id != 0
7335 });
7336 (principal, catalog_bound)
7337 }
7338
7339 pub fn begin_as(
7340 &self,
7341 principal: Option<crate::auth::Principal>,
7342 ) -> crate::txn::Transaction<'_> {
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 let txn_id = self.alloc_txn_id();
7348 let read = Snapshot::at(self.epoch.visible());
7349 crate::txn::Transaction::new(self, txn_id, read).with_principal(principal, catalog_bound)
7350 }
7351
7352 pub fn begin_with_isolation(
7354 &self,
7355 level: crate::txn::IsolationLevel,
7356 ) -> crate::txn::Transaction<'_> {
7357 let txn_id = self.alloc_txn_id();
7358 let epoch = match level {
7359 crate::txn::IsolationLevel::ReadCommitted => self.epoch.visible(),
7360 _ => self.epoch.visible(),
7361 };
7362 let read = Snapshot::at(epoch);
7363 let (principal, catalog_bound) = self.transaction_principal_snapshot();
7364 crate::txn::Transaction::new(self, txn_id, read).with_principal(principal, catalog_bound)
7365 }
7366
7367 pub fn begin_with_external_trigger_bridge<'a>(
7370 &'a self,
7371 bridge: &'a dyn ExternalTriggerBridge,
7372 ) -> crate::txn::Transaction<'a> {
7373 let txn_id = self.alloc_txn_id();
7374 let read = Snapshot::at(self.epoch.visible());
7375 let (principal, catalog_bound) = self.transaction_principal_snapshot();
7376 crate::txn::Transaction::new(self, txn_id, read)
7377 .with_external_trigger_bridge(bridge)
7378 .with_principal(principal, catalog_bound)
7379 }
7380
7381 pub fn begin_with_external_trigger_bridge_as<'a>(
7382 &'a self,
7383 bridge: &'a dyn ExternalTriggerBridge,
7384 principal: Option<crate::auth::Principal>,
7385 ) -> crate::txn::Transaction<'a> {
7386 let catalog_bound = principal.as_ref().is_some_and(|principal| {
7387 let catalog = self.catalog.read();
7388 catalog.require_auth || principal.user_id != 0
7389 });
7390 let txn_id = self.alloc_txn_id();
7391 let read = Snapshot::at(self.epoch.visible());
7392 crate::txn::Transaction::new(self, txn_id, read)
7393 .with_external_trigger_bridge(bridge)
7394 .with_principal(principal, catalog_bound)
7395 }
7396
7397 pub fn transaction<T>(
7399 &self,
7400 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7401 ) -> Result<T> {
7402 let mut tx = self.begin();
7403 match f(&mut tx) {
7404 Ok(out) => {
7405 tx.commit()?;
7406 Ok(out)
7407 }
7408 Err(e) => {
7409 tx.rollback();
7410 Err(e)
7411 }
7412 }
7413 }
7414
7415 pub fn transaction_with_row_ids<T>(
7416 &self,
7417 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7418 ) -> Result<(T, Vec<RowId>)> {
7419 let mut tx = self.begin();
7420 match f(&mut tx) {
7421 Ok(output) => {
7422 let (_, row_ids) = tx.commit_with_row_ids()?;
7423 Ok((output, row_ids))
7424 }
7425 Err(error) => {
7426 tx.rollback();
7427 Err(error)
7428 }
7429 }
7430 }
7431
7432 pub fn transaction_for_current_principal<T>(
7433 &self,
7434 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7435 ) -> Result<T> {
7436 if self.principal.read().is_some() {
7437 self.refresh_principal()?;
7438 }
7439 let mut transaction = self.begin_as(self.principal.read().clone());
7440 match f(&mut transaction) {
7441 Ok(output) => {
7442 transaction.commit()?;
7443 Ok(output)
7444 }
7445 Err(error) => {
7446 transaction.rollback();
7447 Err(error)
7448 }
7449 }
7450 }
7451
7452 pub fn transaction_for_current_principal_with_epoch<T>(
7453 &self,
7454 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7455 ) -> Result<(Epoch, T)> {
7456 if self.principal.read().is_some() {
7457 self.refresh_principal()?;
7458 }
7459 let mut transaction = self.begin_as(self.principal.read().clone());
7460 match f(&mut transaction) {
7461 Ok(output) => {
7462 let epoch = transaction.commit()?;
7463 Ok((epoch, output))
7464 }
7465 Err(error) => {
7466 transaction.rollback();
7467 Err(error)
7468 }
7469 }
7470 }
7471
7472 pub fn transaction_with_row_ids_for_current_principal<T>(
7473 &self,
7474 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7475 ) -> Result<(T, Vec<RowId>)> {
7476 if self.principal.read().is_some() {
7477 self.refresh_principal()?;
7478 }
7479 let mut transaction = self.begin_as(self.principal.read().clone());
7480 match f(&mut transaction) {
7481 Ok(output) => {
7482 let (_, row_ids) = transaction.commit_with_row_ids()?;
7483 Ok((output, row_ids))
7484 }
7485 Err(error) => {
7486 transaction.rollback();
7487 Err(error)
7488 }
7489 }
7490 }
7491
7492 pub fn transaction_with_external_trigger_bridge<'a, T>(
7495 &'a self,
7496 bridge: &'a dyn ExternalTriggerBridge,
7497 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7498 ) -> Result<T> {
7499 let mut tx = self.begin_with_external_trigger_bridge(bridge);
7500 match f(&mut tx) {
7501 Ok(out) => {
7502 tx.commit()?;
7503 Ok(out)
7504 }
7505 Err(e) => {
7506 tx.rollback();
7507 Err(e)
7508 }
7509 }
7510 }
7511
7512 pub fn transaction_with_external_trigger_bridge_as<'a, T>(
7513 &'a self,
7514 bridge: &'a dyn ExternalTriggerBridge,
7515 principal: Option<crate::auth::Principal>,
7516 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7517 ) -> Result<T> {
7518 let mut tx = self.begin_with_external_trigger_bridge_as(bridge, principal);
7519 match f(&mut tx) {
7520 Ok(output) => {
7521 tx.commit()?;
7522 Ok(output)
7523 }
7524 Err(error) => {
7525 tx.rollback();
7526 Err(error)
7527 }
7528 }
7529 }
7530
7531 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
7534 self.active_txns.register(epoch)
7535 }
7536
7537 fn fill_auto_increment_for_staging(
7538 &self,
7539 staging: &mut [(u64, crate::txn::Staged)],
7540 control: Option<&crate::ExecutionControl>,
7541 ) -> Result<()> {
7542 let mut puts_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
7543 for (index, (table_id, staged)) in staging.iter().enumerate() {
7544 commit_prepare_checkpoint(control, index)?;
7545 if matches!(staged, crate::txn::Staged::Put(_)) {
7546 puts_by_table.entry(*table_id).or_default().push(index);
7547 }
7548 }
7549
7550 let tables = self.tables.read();
7551 for (table_index, (table_id, indexes)) in puts_by_table.into_iter().enumerate() {
7552 commit_prepare_checkpoint(control, table_index)?;
7553 if let Some(handle) = tables.get(&table_id) {
7554 #[cfg(test)]
7555 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
7556 let mut t = handle.lock();
7557 for (fill_index, index) in indexes.into_iter().enumerate() {
7558 commit_prepare_checkpoint(control, fill_index)?;
7559 if let crate::txn::Staged::Put(cells) = &mut staging[index].1 {
7560 t.fill_auto_inc(cells)?;
7561 }
7562 }
7563 }
7564 }
7565 Ok(())
7566 }
7567
7568 fn expand_table_triggers(
7569 &self,
7570 staging: &mut Vec<(u64, crate::txn::Staged)>,
7571 read_epoch: Epoch,
7572 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
7573 external_states: &mut Vec<(String, Vec<u8>)>,
7574 control: Option<&crate::ExecutionControl>,
7575 ) -> Result<()> {
7576 commit_prepare_checkpoint(control, 0)?;
7577 let mut external_writes = Vec::new();
7578 let config = self.trigger_config();
7579 if config.recursive_triggers {
7580 let chunk = std::mem::take(staging);
7581 let stacks = vec![Vec::new(); chunk.len()];
7582 *staging = self.expand_trigger_chunk(
7583 chunk,
7584 stacks,
7585 read_epoch,
7586 0,
7587 config.max_depth,
7588 &mut external_writes,
7589 &config,
7590 control,
7591 )?;
7592 self.apply_external_trigger_writes(
7593 external_writes,
7594 external_trigger_bridge,
7595 external_states,
7596 staging,
7597 control,
7598 )?;
7599 return Ok(());
7600 }
7601
7602 let mut expansion =
7603 self.expand_table_triggers_once(staging, read_epoch, None, &config, control)?;
7604 if !expansion.before.is_empty() {
7605 let mut final_staging = expansion.before;
7606 final_staging.extend(filter_ignored_staging(
7607 std::mem::take(staging),
7608 &expansion.ignored_indices,
7609 ));
7610 *staging = final_staging;
7611 } else if !expansion.ignored_indices.is_empty() {
7612 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
7613 }
7614 staging.append(&mut expansion.after);
7615 external_writes.append(&mut expansion.before_external);
7616 external_writes.append(&mut expansion.after_external);
7617 self.apply_external_trigger_writes(
7618 external_writes,
7619 external_trigger_bridge,
7620 external_states,
7621 staging,
7622 control,
7623 )?;
7624 Ok(())
7625 }
7626
7627 #[allow(clippy::too_many_arguments)]
7628 fn expand_trigger_chunk(
7629 &self,
7630 mut chunk: Vec<(u64, crate::txn::Staged)>,
7631 stacks: Vec<Vec<String>>,
7632 read_epoch: Epoch,
7633 depth: u32,
7634 max_depth: u32,
7635 external_writes: &mut Vec<ExternalTriggerWrite>,
7636 config: &TriggerConfig,
7637 control: Option<&crate::ExecutionControl>,
7638 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
7639 if chunk.is_empty() {
7640 return Ok(Vec::new());
7641 }
7642 commit_prepare_checkpoint(control, 0)?;
7643 self.fill_auto_increment_for_staging(&mut chunk, control)?;
7644 let expansion = self.expand_table_triggers_once(
7645 &mut chunk,
7646 read_epoch,
7647 Some(&stacks),
7648 config,
7649 control,
7650 )?;
7651 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
7652 let stack = expansion
7653 .before_stacks
7654 .first()
7655 .or_else(|| expansion.after_stacks.first())
7656 .cloned()
7657 .unwrap_or_default();
7658 return Err(MongrelError::TriggerValidation(format!(
7659 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
7660 Self::format_trigger_stack(&stack)
7661 )));
7662 }
7663
7664 let mut out = Vec::new();
7665 external_writes.extend(expansion.before_external);
7666 out.extend(self.expand_trigger_chunk(
7667 expansion.before,
7668 expansion.before_stacks,
7669 read_epoch,
7670 depth + 1,
7671 max_depth,
7672 external_writes,
7673 config,
7674 control,
7675 )?);
7676 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
7677 external_writes.extend(expansion.after_external);
7678 out.extend(self.expand_trigger_chunk(
7679 expansion.after,
7680 expansion.after_stacks,
7681 read_epoch,
7682 depth + 1,
7683 max_depth,
7684 external_writes,
7685 config,
7686 control,
7687 )?);
7688 Ok(out)
7689 }
7690
7691 fn apply_external_trigger_writes(
7692 &self,
7693 writes: Vec<ExternalTriggerWrite>,
7694 bridge: Option<&dyn ExternalTriggerBridge>,
7695 external_states: &mut Vec<(String, Vec<u8>)>,
7696 staging: &mut Vec<(u64, crate::txn::Staged)>,
7697 control: Option<&crate::ExecutionControl>,
7698 ) -> Result<()> {
7699 if writes.is_empty() {
7700 return Ok(());
7701 }
7702 let bridge = bridge.ok_or_else(|| {
7703 MongrelError::TriggerValidation(
7704 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
7705 )
7706 })?;
7707 for (write_index, write) in writes.into_iter().enumerate() {
7708 commit_prepare_checkpoint(control, write_index)?;
7709 let table = write.table().to_string();
7710 let entry = self.external_table(&table).ok_or_else(|| {
7711 MongrelError::NotFound(format!("external table {table:?} not found"))
7712 })?;
7713 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
7714 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
7715 external_states.push((table, result.state));
7716 for (base_index, base_write) in result.base_writes.into_iter().enumerate() {
7717 commit_prepare_checkpoint(control, base_index)?;
7718 match base_write {
7719 ExternalTriggerBaseWrite::Put { table, cells } => {
7720 let table_id = self.table_id(&table)?;
7721 staging.push((table_id, crate::txn::Staged::Put(cells)));
7722 }
7723 ExternalTriggerBaseWrite::Delete { table, row_id } => {
7724 let table_id = self.table_id(&table)?;
7725 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
7726 }
7727 }
7728 }
7729 }
7730 dedup_external_states_in_place(external_states);
7731 Ok(())
7732 }
7733
7734 fn expand_table_triggers_once(
7735 &self,
7736 staging: &mut Vec<(u64, crate::txn::Staged)>,
7737 read_epoch: Epoch,
7738 trigger_stacks: Option<&[Vec<String>]>,
7739 config: &TriggerConfig,
7740 control: Option<&crate::ExecutionControl>,
7741 ) -> Result<TriggerExpansion> {
7742 commit_prepare_checkpoint(control, 0)?;
7743 let triggers: Vec<StoredTrigger> = self
7744 .catalog
7745 .read()
7746 .triggers
7747 .iter()
7748 .filter(|entry| {
7749 entry.trigger.enabled
7750 && matches!(
7751 entry.trigger.timing,
7752 TriggerTiming::Before | TriggerTiming::After
7753 )
7754 && matches!(entry.trigger.target, TriggerTarget::Table(_))
7755 })
7756 .map(|entry| entry.trigger.clone())
7757 .collect();
7758 if triggers.is_empty() || staging.is_empty() {
7759 return Ok(TriggerExpansion::default());
7760 }
7761
7762 let before_triggers = triggers
7763 .iter()
7764 .filter(|trigger| trigger.timing == TriggerTiming::Before)
7765 .cloned()
7766 .collect::<Vec<_>>();
7767 let after_triggers = triggers
7768 .iter()
7769 .filter(|trigger| trigger.timing == TriggerTiming::After)
7770 .cloned()
7771 .collect::<Vec<_>>();
7772
7773 let mut before_added = Vec::new();
7774 let mut before_stacks = Vec::new();
7775 let mut before_external = Vec::new();
7776 let mut ignored_indices = std::collections::BTreeSet::new();
7777 if !before_triggers.is_empty() {
7778 let before_events =
7779 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?;
7780 let mut out = TriggerProgramOutput {
7781 added: &mut before_added,
7782 added_stacks: &mut before_stacks,
7783 added_external: &mut before_external,
7784 ignored_indices: &mut ignored_indices,
7785 };
7786 self.execute_triggers_for_events(
7787 &before_triggers,
7788 &before_events,
7789 Some(staging),
7790 &mut out,
7791 config,
7792 read_epoch,
7793 control,
7794 )?;
7795 }
7796
7797 let after_events = if after_triggers.is_empty() {
7798 Vec::new()
7799 } else {
7800 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?
7801 .into_iter()
7802 .filter(|event| {
7803 !event
7804 .op_indices
7805 .iter()
7806 .any(|idx| ignored_indices.contains(idx))
7807 })
7808 .collect()
7809 };
7810
7811 let mut after_added = Vec::new();
7812 let mut after_stacks = Vec::new();
7813 let mut after_external = Vec::new();
7814 let mut out = TriggerProgramOutput {
7815 added: &mut after_added,
7816 added_stacks: &mut after_stacks,
7817 added_external: &mut after_external,
7818 ignored_indices: &mut ignored_indices,
7819 };
7820 self.execute_triggers_for_events(
7821 &after_triggers,
7822 &after_events,
7823 None,
7824 &mut out,
7825 config,
7826 read_epoch,
7827 control,
7828 )?;
7829 Ok(TriggerExpansion {
7830 before: before_added,
7831 before_stacks,
7832 before_external,
7833 after: after_added,
7834 after_stacks,
7835 after_external,
7836 ignored_indices,
7837 })
7838 }
7839
7840 #[allow(clippy::too_many_arguments)]
7841 fn execute_triggers_for_events(
7842 &self,
7843 triggers: &[StoredTrigger],
7844 events: &[WriteEvent],
7845 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
7846 out: &mut TriggerProgramOutput<'_>,
7847 config: &TriggerConfig,
7848 read_epoch: Epoch,
7849 control: Option<&crate::ExecutionControl>,
7850 ) -> Result<()> {
7851 let mut checkpoint_index = 0_usize;
7852 for event in events {
7853 for trigger in triggers {
7854 commit_prepare_checkpoint(control, checkpoint_index)?;
7855 checkpoint_index += 1;
7856 if event
7857 .op_indices
7858 .iter()
7859 .any(|idx| out.ignored_indices.contains(idx))
7860 {
7861 break;
7862 }
7863 let matches = {
7864 let cat = self.catalog.read();
7865 trigger_matches_event(trigger, event, &cat)?
7866 };
7867 if !matches {
7868 continue;
7869 }
7870 if let Some(when) = &trigger.when {
7871 if !eval_trigger_expr(when, event)? {
7872 continue;
7873 }
7874 }
7875 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
7876 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
7877 return Err(MongrelError::TriggerValidation(format!(
7878 "trigger recursion cycle detected; trigger stack: {}",
7879 Self::format_trigger_stack(&trigger_stack)
7880 )));
7881 }
7882 let outcome = match staging.as_mut() {
7883 Some(staging) => self.execute_trigger_program(
7884 trigger,
7885 event,
7886 Some(&mut **staging),
7887 out,
7888 &trigger_stack,
7889 config,
7890 read_epoch,
7891 control,
7892 )?,
7893 None => self.execute_trigger_program(
7894 trigger,
7895 event,
7896 None,
7897 out,
7898 &trigger_stack,
7899 config,
7900 read_epoch,
7901 control,
7902 )?,
7903 };
7904 if outcome == TriggerProgramOutcome::Ignore {
7905 out.ignored_indices.extend(event.op_indices.iter().copied());
7906 break;
7907 }
7908 }
7909 }
7910 Ok(())
7911 }
7912
7913 fn trigger_events_for_staging(
7914 &self,
7915 staging: &[(u64, crate::txn::Staged)],
7916 read_epoch: Epoch,
7917 trigger_stacks: Option<&[Vec<String>]>,
7918 control: Option<&crate::ExecutionControl>,
7919 ) -> Result<Vec<WriteEvent>> {
7920 use crate::txn::Staged;
7921 use std::collections::{HashMap, VecDeque};
7922
7923 let snapshot = Snapshot::at(read_epoch);
7924 let cat = self.catalog.read();
7925 let mut table_names = HashMap::new();
7926 let mut table_schemas = HashMap::new();
7927 for entry in cat
7928 .tables
7929 .iter()
7930 .filter(|entry| matches!(entry.state, TableState::Live))
7931 {
7932 table_names.insert(entry.table_id, entry.name.clone());
7933 table_schemas.insert(entry.table_id, entry.schema.clone());
7934 }
7935 drop(cat);
7936
7937 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
7938 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
7939 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
7940
7941 for (idx, (table_id, staged)) in staging.iter().enumerate() {
7942 commit_prepare_checkpoint(control, idx)?;
7943 let Some(schema) = table_schemas.get(table_id) else {
7944 continue;
7945 };
7946 let Some(pk) = schema.primary_key() else {
7947 continue;
7948 };
7949 match staged {
7950 Staged::Delete(row_id) => {
7951 let handle = self.table_by_id(*table_id)?;
7952 let Some(row) = handle.lock().get(*row_id, snapshot) else {
7953 continue;
7954 };
7955 let Some(pk_value) = row.columns.get(&pk.id) else {
7956 continue;
7957 };
7958 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
7959 delete_by_key
7960 .entry((*table_id, pk_value.encode_key()))
7961 .or_default()
7962 .push_back(idx);
7963 }
7964 Staged::Put(cells) => {
7965 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
7966 put_by_key
7967 .entry((*table_id, value.encode_key()))
7968 .or_default()
7969 .push_back(idx);
7970 }
7971 }
7972 Staged::Update { row_id, .. } => {
7973 let handle = self.table_by_id(*table_id)?;
7974 let row = handle.lock().get(*row_id, snapshot);
7975 if let Some(row) = row {
7976 old_rows.insert(idx, TriggerRowImage::from_row(row));
7977 }
7978 }
7979 Staged::Truncate => {}
7980 }
7981 }
7982
7983 let mut paired_delete = std::collections::HashSet::new();
7984 let mut paired_put = std::collections::HashSet::new();
7985 let mut events = Vec::new();
7986
7987 for (pair_index, (key, deletes)) in delete_by_key.iter_mut().enumerate() {
7988 commit_prepare_checkpoint(control, pair_index)?;
7989 let Some(puts) = put_by_key.get_mut(key) else {
7990 continue;
7991 };
7992 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
7993 paired_delete.insert(delete_idx);
7994 paired_put.insert(put_idx);
7995 let (table_id, _) = &staging[put_idx];
7996 let Some(table_name) = table_names.get(table_id).cloned() else {
7997 continue;
7998 };
7999 let old = old_rows.get(&delete_idx).cloned();
8000 let new = match &staging[put_idx].1 {
8001 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
8002 _ => None,
8003 };
8004 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
8005 events.push(WriteEvent {
8006 table: table_name,
8007 kind: TriggerEvent::Update,
8008 old,
8009 new,
8010 changed_columns,
8011 op_indices: vec![delete_idx, put_idx],
8012 put_idx: Some(put_idx),
8013 trigger_stack: Self::trigger_stack_for_indices(
8014 trigger_stacks,
8015 &[delete_idx, put_idx],
8016 ),
8017 });
8018 }
8019 }
8020
8021 for (idx, (table_id, staged)) in staging.iter().enumerate() {
8022 commit_prepare_checkpoint(control, idx)?;
8023 let Some(table_name) = table_names.get(table_id).cloned() else {
8024 continue;
8025 };
8026 match staged {
8027 Staged::Put(cells) if !paired_put.contains(&idx) => {
8028 let new = Some(TriggerRowImage::from_cells(cells));
8029 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
8030 events.push(WriteEvent {
8031 table: table_name,
8032 kind: TriggerEvent::Insert,
8033 old: None,
8034 new,
8035 changed_columns,
8036 op_indices: vec![idx],
8037 put_idx: Some(idx),
8038 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
8039 });
8040 }
8041 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
8042 let old = match old_rows.get(&idx).cloned() {
8043 Some(old) => Some(old),
8044 None => {
8045 let handle = self.table_by_id(*table_id)?;
8046 let row = handle.lock().get(*row_id, snapshot);
8047 row.map(TriggerRowImage::from_row)
8048 }
8049 };
8050 let Some(old) = old else {
8051 continue;
8052 };
8053 let changed_columns = old.columns.keys().copied().collect();
8054 events.push(WriteEvent {
8055 table: table_name,
8056 kind: TriggerEvent::Delete,
8057 old: Some(old),
8058 new: None,
8059 changed_columns,
8060 op_indices: vec![idx],
8061 put_idx: None,
8062 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
8063 });
8064 }
8065 Staged::Update { new_row: cells, .. } => {
8066 let old = old_rows.get(&idx).cloned();
8067 let new = Some(TriggerRowImage::from_cells(cells));
8068 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
8069 events.push(WriteEvent {
8070 table: table_name,
8071 kind: TriggerEvent::Update,
8072 old,
8073 new,
8074 changed_columns,
8075 op_indices: vec![idx],
8076 put_idx: Some(idx),
8077 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
8078 });
8079 }
8080 Staged::Truncate => {}
8081 _ => {}
8082 }
8083 }
8084
8085 Ok(events)
8086 }
8087
8088 #[allow(clippy::too_many_arguments)]
8089 fn execute_trigger_program(
8090 &self,
8091 trigger: &StoredTrigger,
8092 event: &WriteEvent,
8093 staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
8094 out: &mut TriggerProgramOutput<'_>,
8095 trigger_stack: &[String],
8096 config: &TriggerConfig,
8097 read_epoch: Epoch,
8098 control: Option<&crate::ExecutionControl>,
8099 ) -> Result<TriggerProgramOutcome> {
8100 let mut event = event.clone();
8101 let mut select_results: HashMap<String, Vec<TriggerRowImage>> = HashMap::new();
8102 self.execute_trigger_steps(
8103 trigger,
8104 &trigger.program.steps,
8105 &mut event,
8106 staging,
8107 out,
8108 trigger_stack,
8109 config,
8110 &mut select_results,
8111 0,
8112 None,
8113 read_epoch,
8114 control,
8115 )
8116 }
8117
8118 #[allow(clippy::too_many_arguments)]
8119 fn execute_trigger_steps(
8120 &self,
8121 trigger: &StoredTrigger,
8122 steps: &[TriggerStep],
8123 event: &mut WriteEvent,
8124 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
8125 out: &mut TriggerProgramOutput<'_>,
8126 trigger_stack: &[String],
8127 config: &TriggerConfig,
8128 select_results: &mut HashMap<String, Vec<TriggerRowImage>>,
8129 depth: u32,
8130 selected: Option<&TriggerRowImage>,
8131 read_epoch: Epoch,
8132 control: Option<&crate::ExecutionControl>,
8133 ) -> Result<TriggerProgramOutcome> {
8134 let _ = depth;
8135 for (step_index, step) in steps.iter().enumerate() {
8136 commit_prepare_checkpoint(control, step_index)?;
8137 match step {
8138 TriggerStep::SetNew { cells } => {
8139 if trigger.timing != TriggerTiming::Before {
8140 return Err(MongrelError::InvalidArgument(
8141 "SetNew trigger step is only valid in BEFORE triggers".into(),
8142 ));
8143 }
8144 let put_idx = event.put_idx.ok_or_else(|| {
8145 MongrelError::InvalidArgument(
8146 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
8147 )
8148 })?;
8149 let staging = staging.as_deref_mut().ok_or_else(|| {
8150 MongrelError::InvalidArgument(
8151 "SetNew trigger step requires mutable trigger staging".into(),
8152 )
8153 })?;
8154 let mut update_changed_columns = None;
8155 let row_cells = match staging.get_mut(put_idx).map(|(_, op)| op) {
8156 Some(crate::txn::Staged::Put(cells)) => cells,
8157 Some(crate::txn::Staged::Update {
8158 new_row,
8159 changed_columns,
8160 ..
8161 }) => {
8162 update_changed_columns = Some(changed_columns);
8163 new_row
8164 }
8165 _ => {
8166 return Err(MongrelError::InvalidArgument(
8167 "SetNew trigger step target row is not mutable".into(),
8168 ))
8169 }
8170 };
8171 for (column_id, value) in eval_trigger_cells(cells, event, selected)? {
8172 row_cells.retain(|(id, _)| *id != column_id);
8173 row_cells.push((column_id, value.clone()));
8174 if let Some(changed_columns) = &mut update_changed_columns {
8175 changed_columns.push(column_id);
8176 }
8177 if let Some(new) = &mut event.new {
8178 new.columns.insert(column_id, value);
8179 }
8180 }
8181 row_cells.sort_by_key(|(id, _)| *id);
8182 if let Some(changed_columns) = update_changed_columns {
8183 changed_columns.sort_unstable();
8184 changed_columns.dedup();
8185 }
8186 }
8187 TriggerStep::Insert { table, cells } => {
8188 let cells = eval_trigger_cells(cells, event, selected)?;
8189 if let Ok(table_id) = self.table_id(table) {
8190 out.added.push((table_id, crate::txn::Staged::Put(cells)));
8191 out.added_stacks.push(trigger_stack.to_vec());
8192 } else if self.external_table(table).is_some() {
8193 out.added_external.push(ExternalTriggerWrite::Insert {
8194 table: table.clone(),
8195 cells,
8196 });
8197 } else {
8198 return Err(MongrelError::NotFound(format!(
8199 "trigger {:?} insert target {table:?} not found",
8200 trigger.name
8201 )));
8202 }
8203 }
8204 TriggerStep::UpdateByPk { table, pk, cells } => {
8205 let pk = eval_trigger_value(pk, event, selected)?;
8206 let cells = eval_trigger_cells(cells, event, selected)?;
8207 if self.external_table(table).is_some() {
8208 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
8209 table: table.clone(),
8210 pk,
8211 cells,
8212 });
8213 } else {
8214 let row_id = self
8215 .table(table)?
8216 .lock()
8217 .lookup_pk(&pk.encode_key())
8218 .ok_or_else(|| {
8219 MongrelError::NotFound(format!(
8220 "trigger {:?} update target not found",
8221 trigger.name
8222 ))
8223 })?;
8224 let handle = self.table(table)?;
8225 let snapshot = Snapshot::at(self.epoch.visible());
8226 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
8227 MongrelError::NotFound(format!(
8228 "trigger {:?} update target not visible",
8229 trigger.name
8230 ))
8231 })?;
8232 let mut changed_columns = cells
8233 .iter()
8234 .map(|(column_id, _)| *column_id)
8235 .collect::<Vec<_>>();
8236 changed_columns.sort_unstable();
8237 changed_columns.dedup();
8238 let mut merged = old.columns;
8239 for (column_id, value) in cells {
8240 merged.insert(column_id, value);
8241 }
8242 out.added.push((
8243 self.table_id(table)?,
8244 crate::txn::Staged::Update {
8245 row_id,
8246 new_row: merged.into_iter().collect(),
8247 changed_columns,
8248 },
8249 ));
8250 out.added_stacks.push(trigger_stack.to_vec());
8251 }
8252 }
8253 TriggerStep::DeleteByPk { table, pk } => {
8254 let pk = eval_trigger_value(pk, event, selected)?;
8255 if self.external_table(table).is_some() {
8256 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
8257 table: table.clone(),
8258 pk,
8259 });
8260 } else {
8261 let row_id = self
8262 .table(table)?
8263 .lock()
8264 .lookup_pk(&pk.encode_key())
8265 .ok_or_else(|| {
8266 MongrelError::NotFound(format!(
8267 "trigger {:?} delete target not found",
8268 trigger.name
8269 ))
8270 })?;
8271 out.added
8272 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
8273 out.added_stacks.push(trigger_stack.to_vec());
8274 }
8275 }
8276 TriggerStep::Select {
8277 id,
8278 table,
8279 conditions,
8280 } => {
8281 let schema = self.table(table)?.lock().schema().clone();
8282 let snapshot = Snapshot::at(read_epoch);
8283 let handle = self.table(table)?;
8284 let rows = match control {
8285 Some(control) => {
8286 handle.lock().visible_rows_controlled(snapshot, control)?
8287 }
8288 None => handle.lock().visible_rows(snapshot)?,
8289 };
8290 let mut matched = Vec::new();
8291 for (row_index, row) in rows.into_iter().enumerate() {
8292 commit_prepare_checkpoint(control, row_index)?;
8293 let image = TriggerRowImage::from_row(row);
8294 let passes = conditions
8295 .iter()
8296 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
8297 .collect::<Result<Vec<_>>>()?
8298 .into_iter()
8299 .all(|b| b);
8300 if passes {
8301 matched.push(image);
8302 }
8303 }
8304 if let Some(pk) = schema.primary_key() {
8305 matched.sort_by(|a, b| {
8306 let av = a.columns.get(&pk.id).unwrap_or(&Value::Null);
8307 let bv = b.columns.get(&pk.id).unwrap_or(&Value::Null);
8308 value_order(av, bv).unwrap_or(std::cmp::Ordering::Equal)
8309 });
8310 }
8311 select_results.insert(id.clone(), matched);
8312 }
8313 TriggerStep::Foreach { id, steps } => {
8314 let rows = select_results.get(id).ok_or_else(|| {
8315 MongrelError::InvalidArgument(format!(
8316 "trigger {:?} foreach references unknown select id {id:?}",
8317 trigger.name
8318 ))
8319 })?;
8320 if rows.len() > config.max_loop_iterations as usize {
8321 return Err(MongrelError::InvalidArgument(format!(
8322 "trigger {:?} foreach exceeded max_loop_iterations ({})",
8323 trigger.name, config.max_loop_iterations
8324 )));
8325 }
8326 for (row_index, row) in rows.clone().into_iter().enumerate() {
8327 commit_prepare_checkpoint(control, row_index)?;
8328 let result = self.execute_trigger_steps(
8329 trigger,
8330 steps,
8331 event,
8332 staging.as_deref_mut(),
8333 out,
8334 trigger_stack,
8335 config,
8336 select_results,
8337 depth + 1,
8338 Some(&row),
8339 read_epoch,
8340 control,
8341 )?;
8342 if result == TriggerProgramOutcome::Ignore {
8343 return Ok(TriggerProgramOutcome::Ignore);
8344 }
8345 }
8346 }
8347 TriggerStep::DeleteWhere { table, conditions } => {
8348 let schema = self.table(table)?.lock().schema().clone();
8349 let snapshot = Snapshot::at(read_epoch);
8350 let handle = self.table(table)?;
8351 let rows = match control {
8352 Some(control) => {
8353 handle.lock().visible_rows_controlled(snapshot, control)?
8354 }
8355 None => handle.lock().visible_rows(snapshot)?,
8356 };
8357 let table_id = self.table_id(table)?;
8358 let mut to_delete = Vec::new();
8359 for (row_index, row) in rows.into_iter().enumerate() {
8360 commit_prepare_checkpoint(control, row_index)?;
8361 let image = TriggerRowImage::from_row(row.clone());
8362 let passes = conditions
8363 .iter()
8364 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
8365 .collect::<Result<Vec<_>>>()?
8366 .into_iter()
8367 .all(|b| b);
8368 if passes {
8369 to_delete.push((table_id, row.row_id));
8370 }
8371 }
8372 for (row_index, (table_id, row_id)) in to_delete.into_iter().enumerate() {
8373 commit_prepare_checkpoint(control, row_index)?;
8374 out.added
8375 .push((table_id, crate::txn::Staged::Delete(row_id)));
8376 out.added_stacks.push(trigger_stack.to_vec());
8377 }
8378 }
8379 TriggerStep::UpdateWhere {
8380 table,
8381 conditions,
8382 cells,
8383 } => {
8384 let schema = self.table(table)?.lock().schema().clone();
8385 let snapshot = Snapshot::at(read_epoch);
8386 let handle = self.table(table)?;
8387 let rows = match control {
8388 Some(control) => {
8389 handle.lock().visible_rows_controlled(snapshot, control)?
8390 }
8391 None => handle.lock().visible_rows(snapshot)?,
8392 };
8393 let table_id = self.table_id(table)?;
8394 let mut changed_columns =
8395 cells.iter().map(|cell| cell.column_id).collect::<Vec<_>>();
8396 changed_columns.sort_unstable();
8397 changed_columns.dedup();
8398 let mut to_update = Vec::new();
8399 for (row_index, row) in rows.into_iter().enumerate() {
8400 commit_prepare_checkpoint(control, row_index)?;
8401 let image = TriggerRowImage::from_row(row.clone());
8402 let passes = conditions
8403 .iter()
8404 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
8405 .collect::<Result<Vec<_>>>()?
8406 .into_iter()
8407 .all(|b| b);
8408 if passes {
8409 let new_cells = cells
8410 .iter()
8411 .map(|cell| {
8412 Ok((
8413 cell.column_id,
8414 eval_trigger_value(&cell.value, event, Some(&image))?,
8415 ))
8416 })
8417 .collect::<Result<Vec<_>>>()?;
8418 let mut merged = row.columns.clone();
8419 for (column_id, value) in new_cells {
8420 merged.insert(column_id, value);
8421 }
8422 to_update.push((table_id, row.row_id, merged));
8423 }
8424 }
8425 for (row_index, (table_id, row_id, merged)) in to_update.into_iter().enumerate()
8426 {
8427 commit_prepare_checkpoint(control, row_index)?;
8428 out.added.push((
8429 table_id,
8430 crate::txn::Staged::Update {
8431 row_id,
8432 new_row: merged.into_iter().collect(),
8433 changed_columns: changed_columns.clone(),
8434 },
8435 ));
8436 out.added_stacks.push(trigger_stack.to_vec());
8437 }
8438 }
8439 TriggerStep::Raise { action, message } => match action {
8440 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
8441 TriggerRaiseAction::Abort
8442 | TriggerRaiseAction::Fail
8443 | TriggerRaiseAction::Rollback => {
8444 let message = eval_trigger_value(message, event, selected)?;
8445 return Err(MongrelError::TriggerValidation(format!(
8446 "trigger {:?} raised: {}; trigger stack: {}",
8447 trigger.name,
8448 trigger_message(message),
8449 Self::format_trigger_stack(trigger_stack)
8450 )));
8451 }
8452 },
8453 }
8454 }
8455 Ok(TriggerProgramOutcome::Continue)
8456 }
8457
8458 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
8459 let Some(stacks) = stacks else {
8460 return Vec::new();
8461 };
8462 let mut out = Vec::new();
8463 for idx in indices {
8464 let Some(stack) = stacks.get(*idx) else {
8465 continue;
8466 };
8467 for name in stack {
8468 if !out.iter().any(|existing| existing == name) {
8469 out.push(name.clone());
8470 }
8471 }
8472 }
8473 out
8474 }
8475
8476 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
8477 let mut out = stack.to_vec();
8478 out.push(trigger_name.to_string());
8479 out
8480 }
8481
8482 fn format_trigger_stack(stack: &[String]) -> String {
8483 if stack.is_empty() {
8484 "<root>".into()
8485 } else {
8486 stack.join(" -> ")
8487 }
8488 }
8489
8490 fn validate_constraints(
8505 &self,
8506 staging: &mut Vec<(u64, crate::txn::Staged)>,
8507 read_epoch: Epoch,
8508 control: Option<&crate::ExecutionControl>,
8509 ) -> Result<()> {
8510 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
8511 use crate::memtable::Row;
8512 use crate::txn::Staged;
8513 use std::collections::HashSet;
8514
8515 commit_prepare_checkpoint(control, 0)?;
8516 let snapshot = Snapshot::at(read_epoch);
8517 let cat = self.catalog.read();
8518
8519 let live: Vec<(u64, &str, &crate::schema::Schema)> = cat
8521 .tables
8522 .iter()
8523 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
8524 .map(|e| (e.table_id, e.name.as_str(), &e.schema))
8525 .collect();
8526
8527 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
8529 if !any_constraints {
8530 return Ok(());
8531 }
8532
8533 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
8535 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
8536 if let Some(r) = rows_cache.get(&table_id) {
8537 return Ok(r.clone());
8538 }
8539 let handle = self.table_by_id(table_id)?;
8540 let rows = match control {
8541 Some(control) => handle.lock().visible_rows_controlled(snapshot, control)?,
8542 None => handle.lock().visible_rows(snapshot)?,
8543 };
8544 rows_cache.insert(table_id, rows.clone());
8545 Ok(rows)
8546 };
8547
8548 let mut processed_updates = HashSet::new();
8553 type PendingUpdate = (usize, u64, crate::rowid::RowId, Vec<(u16, Value)>);
8554 let mut update_pass = 0_usize;
8555 loop {
8556 commit_prepare_checkpoint(control, update_pass)?;
8557 update_pass += 1;
8558 let updates: Vec<PendingUpdate> = staging
8559 .iter()
8560 .enumerate()
8561 .filter_map(|(index, (table_id, op))| match op {
8562 Staged::Update {
8563 row_id,
8564 new_row: cells,
8565 ..
8566 } if !processed_updates.contains(&index) => {
8567 Some((index, *table_id, *row_id, cells.clone()))
8568 }
8569 _ => None,
8570 })
8571 .collect();
8572 if updates.is_empty() {
8573 break;
8574 }
8575 let mut new_ops = Vec::new();
8576 for (update_index, (index, table_id, row_id, new_cells)) in
8577 updates.into_iter().enumerate()
8578 {
8579 commit_prepare_checkpoint(control, update_index)?;
8580 processed_updates.insert(index);
8581 let Some(tname) = live
8582 .iter()
8583 .find(|(id, _, _)| *id == table_id)
8584 .map(|(_, name, _)| *name)
8585 else {
8586 continue;
8587 };
8588 let Some(old_row) = self.table_by_id(table_id)?.lock().get(row_id, snapshot) else {
8589 continue;
8590 };
8591 let new_map: HashMap<u16, Value> = new_cells.iter().cloned().collect();
8592 for (child_id, _child_name, child_schema) in &live {
8593 for fk in &child_schema.constraints.foreign_keys {
8594 if fk.ref_table != tname {
8595 continue;
8596 }
8597 let Some(old_key) = encode_composite_key(&fk.ref_columns, &old_row.columns)
8598 else {
8599 continue;
8600 };
8601 if encode_composite_key(&fk.ref_columns, &new_map).as_deref()
8602 == Some(old_key.as_slice())
8603 {
8604 continue;
8605 }
8606 if fk.on_update == FkAction::Restrict {
8607 continue;
8608 }
8609 let child_rows = load_rows(*child_id)?;
8610 for (child_index, child) in child_rows.into_iter().enumerate() {
8611 commit_prepare_checkpoint(control, child_index)?;
8612 if encode_composite_key(&fk.columns, &child.columns).as_deref()
8613 != Some(old_key.as_slice())
8614 {
8615 continue;
8616 }
8617 if staging.iter().any(|(id, op)| {
8618 *id == *child_id
8619 && matches!(op, Staged::Delete(id) if *id == child.row_id)
8620 }) {
8621 continue;
8622 }
8623 let mut cells: Vec<(u16, Value)> = child
8624 .columns
8625 .iter()
8626 .map(|(column_id, value)| (*column_id, value.clone()))
8627 .collect();
8628 for (child_column, parent_column) in
8629 fk.columns.iter().zip(&fk.ref_columns)
8630 {
8631 cells.retain(|(column_id, _)| column_id != child_column);
8632 let value = match fk.on_update {
8633 FkAction::Cascade => {
8634 new_map.get(parent_column).cloned().unwrap_or(Value::Null)
8635 }
8636 FkAction::SetNull => Value::Null,
8637 FkAction::Restrict => {
8638 return Err(MongrelError::Other(
8639 "restricted foreign-key update reached cascade preparation"
8640 .into(),
8641 ));
8642 }
8643 };
8644 cells.push((*child_column, value));
8645 }
8646 cells.sort_by_key(|(column_id, _)| *column_id);
8647 if let Some(existing_index) = staging.iter().position(|(id, op)| {
8648 *id == *child_id
8649 && matches!(op, Staged::Update { row_id, .. } if *row_id == child.row_id)
8650 }) {
8651 if let Staged::Update {
8652 new_row: existing,
8653 changed_columns,
8654 ..
8655 } = &mut staging[existing_index].1 {
8656 changed_columns.extend(fk.columns.iter().copied());
8657 changed_columns.sort_unstable();
8658 changed_columns.dedup();
8659 if *existing != cells {
8660 *existing = cells;
8661 processed_updates.remove(&existing_index);
8662 }
8663 }
8664 } else {
8665 new_ops.push((
8666 *child_id,
8667 Staged::Update {
8668 row_id: child.row_id,
8669 new_row: cells,
8670 changed_columns: fk.columns.clone(),
8671 },
8672 ));
8673 }
8674 }
8675 }
8676 }
8677 }
8678 staging.extend(new_ops);
8679 }
8680
8681 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
8686 let mut cascade_pass = 0_usize;
8687 loop {
8688 commit_prepare_checkpoint(control, cascade_pass)?;
8689 cascade_pass += 1;
8690 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
8691 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
8692 .iter()
8693 .filter_map(|(t, op)| match op {
8694 Staged::Delete(rid) => Some((*t, *rid)),
8695 _ => None,
8696 })
8697 .collect();
8698 for (delete_index, (table_id, rid)) in deletes.into_iter().enumerate() {
8699 commit_prepare_checkpoint(control, delete_index)?;
8700 if !cascaded.insert((table_id, rid.0)) {
8701 continue;
8702 }
8703 let Some(tname) = live
8704 .iter()
8705 .find(|(t, _, _)| *t == table_id)
8706 .map(|(_, n, _)| *n)
8707 else {
8708 continue;
8709 };
8710 let parent_handle = self.table_by_id(table_id)?;
8711 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
8712 continue;
8713 };
8714 for (child_id, _child_name, child_schema) in &live {
8715 for fk in &child_schema.constraints.foreign_keys {
8716 if fk.ref_table != tname {
8717 continue;
8718 }
8719 let Some(parent_key) =
8720 encode_composite_key(&fk.ref_columns, &parent_row.columns)
8721 else {
8722 continue;
8723 };
8724 let key_preserved = staging.iter().any(|(t, op)| {
8733 if *t != table_id {
8734 return false;
8735 }
8736 let Staged::Put(cells) = op else {
8737 return false;
8738 };
8739 let map: HashMap<u16, crate::memtable::Value> =
8740 cells.iter().cloned().collect();
8741 encode_composite_key(&fk.ref_columns, &map).as_deref()
8742 == Some(parent_key.as_slice())
8743 });
8744 if key_preserved {
8745 continue;
8746 }
8747 match fk.on_delete {
8748 FkAction::Restrict => continue,
8749 FkAction::Cascade => {
8750 let child_rows = load_rows(*child_id)?;
8751 for (child_index, cr) in child_rows.iter().enumerate() {
8752 commit_prepare_checkpoint(control, child_index)?;
8753 if !cascaded.contains(&(*child_id, cr.row_id.0))
8754 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
8755 == Some(parent_key.as_slice())
8756 {
8757 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
8758 }
8759 }
8760 }
8761 FkAction::SetNull => {
8762 let child_rows = load_rows(*child_id)?;
8763 for (child_index, cr) in child_rows.iter().enumerate() {
8764 commit_prepare_checkpoint(control, child_index)?;
8765 if !cascaded.contains(&(*child_id, cr.row_id.0))
8766 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
8767 == Some(parent_key.as_slice())
8768 {
8769 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
8772 .columns
8773 .iter()
8774 .map(|(k, v)| (*k, v.clone()))
8775 .collect();
8776 for cid in &fk.columns {
8777 cells.retain(|(k, _)| k != cid);
8778 cells.push((*cid, crate::memtable::Value::Null));
8779 }
8780 new_ops.push((
8781 *child_id,
8782 Staged::Update {
8783 row_id: cr.row_id,
8784 new_row: cells,
8785 changed_columns: fk.columns.clone(),
8786 },
8787 ));
8788 }
8789 }
8790 }
8791 }
8792 }
8793 }
8794 }
8795 if new_ops.is_empty() {
8796 break;
8797 }
8798 staging.extend(new_ops);
8799 }
8800
8801 let staged_deletes: HashSet<(u64, u64)> = staging
8805 .iter()
8806 .filter_map(|(t, op)| match op {
8807 Staged::Delete(rid) | Staged::Update { row_id: rid, .. } => Some((*t, rid.0)),
8808 _ => None,
8809 })
8810 .collect();
8811
8812 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
8814
8815 for (operation_index, (table_id, op)) in staging.iter().enumerate() {
8817 commit_prepare_checkpoint(control, operation_index)?;
8818 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id).copied()
8819 else {
8820 continue;
8821 };
8822 let cells_map: HashMap<u16, crate::memtable::Value>;
8823 match op {
8824 Staged::Put(cells) | Staged::Update { new_row: cells, .. } => {
8825 cells_map = cells.iter().cloned().collect();
8826
8827 if !schema.constraints.checks.is_empty() {
8829 validate_checks(&schema.constraints.checks, &cells_map)?;
8830 }
8831
8832 for uc in &schema.constraints.uniques {
8834 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
8835 continue; };
8837 let marker = (*table_id, uc.id, key.clone());
8838 if !seen_unique.insert(marker) {
8839 return Err(MongrelError::Conflict(format!(
8840 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
8841 uc.name
8842 )));
8843 }
8844 let rows = load_rows(*table_id)?;
8845 for (row_index, r) in rows.iter().enumerate() {
8846 commit_prepare_checkpoint(control, row_index)?;
8847 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
8850 continue;
8851 }
8852 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
8853 if theirs == key {
8854 return Err(MongrelError::Conflict(format!(
8855 "UNIQUE constraint '{}' on table '{tname}' violated",
8856 uc.name
8857 )));
8858 }
8859 }
8860 }
8861 }
8862
8863 for fk in &schema.constraints.foreign_keys {
8865 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
8866 continue; };
8868 let Some(parent_id) = cat
8869 .tables
8870 .iter()
8871 .find(|t| t.name == fk.ref_table)
8872 .map(|t| t.table_id)
8873 else {
8874 return Err(MongrelError::InvalidArgument(format!(
8875 "FOREIGN KEY '{}' references unknown table '{}'",
8876 fk.name, fk.ref_table
8877 )));
8878 };
8879 let parent_rows = load_rows(parent_id)?;
8880 let mut found = false;
8881 for (row_index, r) in parent_rows.iter().enumerate() {
8882 commit_prepare_checkpoint(control, row_index)?;
8883 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
8884 continue;
8885 }
8886 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
8887 if pkey == child_key {
8888 found = true;
8889 break;
8890 }
8891 }
8892 }
8893 if !found {
8900 for (staged_index, (st_table, st_op)) in staging.iter().enumerate() {
8901 commit_prepare_checkpoint(control, staged_index)?;
8902 if *st_table != parent_id {
8903 continue;
8904 }
8905 if let Staged::Put(pcells)
8906 | Staged::Update {
8907 new_row: pcells, ..
8908 } = st_op
8909 {
8910 let pmap: HashMap<u16, crate::memtable::Value> =
8911 pcells.iter().cloned().collect();
8912 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &pmap)
8913 {
8914 if pkey == child_key {
8915 found = true;
8916 break;
8917 }
8918 }
8919 }
8920 }
8921 }
8922 if !found {
8923 return Err(MongrelError::Conflict(format!(
8924 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
8925 fk.name, fk.ref_table
8926 )));
8927 }
8928 }
8929
8930 if let Staged::Update { row_id, .. } = op {
8935 let parent_handle = self.table_by_id(*table_id)?;
8936 let Some(old_parent) = parent_handle.lock().get(*row_id, snapshot) else {
8937 continue;
8938 };
8939 for (child_id, child_name, child_schema) in &live {
8940 for fk in &child_schema.constraints.foreign_keys {
8941 if fk.ref_table != tname || fk.on_update != FkAction::Restrict {
8942 continue;
8943 }
8944 let Some(old_key) =
8945 encode_composite_key(&fk.ref_columns, &old_parent.columns)
8946 else {
8947 continue;
8948 };
8949 if encode_composite_key(&fk.ref_columns, &cells_map).as_deref()
8950 == Some(old_key.as_slice())
8951 {
8952 continue;
8953 }
8954 for (child_index, child) in
8955 load_rows(*child_id)?.into_iter().enumerate()
8956 {
8957 commit_prepare_checkpoint(control, child_index)?;
8958 if encode_composite_key(&fk.columns, &child.columns).as_deref()
8959 != Some(old_key.as_slice())
8960 {
8961 continue;
8962 }
8963 let replacement = staging.iter().find_map(|(id, op)| {
8964 if *id != *child_id {
8965 return None;
8966 }
8967 match op {
8968 Staged::Delete(id) if *id == child.row_id => Some(None),
8969 Staged::Update {
8970 row_id,
8971 new_row: cells,
8972 ..
8973 } if *row_id == child.row_id => {
8974 let map: HashMap<u16, Value> =
8975 cells.iter().cloned().collect();
8976 Some(encode_composite_key(&fk.columns, &map))
8977 }
8978 _ => None,
8979 }
8980 });
8981 if replacement.is_some_and(|key| {
8982 key.as_deref() != Some(old_key.as_slice())
8983 }) {
8984 continue;
8985 }
8986 return Err(MongrelError::Conflict(format!(
8987 "FOREIGN KEY '{}' on table '{child_name}' restricts update (parent key referenced)",
8988 fk.name
8989 )));
8990 }
8991 }
8992 }
8993 }
8994 }
8995 Staged::Delete(rid) => {
8996 let parent_handle = self.table_by_id(*table_id)?;
9000 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
9001 continue;
9002 };
9003 for (child_id, child_name, child_schema) in &live {
9004 for fk in &child_schema.constraints.foreign_keys {
9005 if fk.ref_table != tname || fk.on_delete != FkAction::Restrict {
9006 continue;
9007 }
9008 let Some(parent_key) =
9009 encode_composite_key(&fk.ref_columns, &parent_row.columns)
9010 else {
9011 continue;
9012 };
9013 let child_rows = load_rows(*child_id)?;
9014 for (row_index, r) in child_rows.iter().enumerate() {
9015 commit_prepare_checkpoint(control, row_index)?;
9016 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
9019 continue;
9020 }
9021 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
9022 if ck == parent_key {
9023 return Err(MongrelError::Conflict(format!(
9024 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
9025 fk.name
9026 )));
9027 }
9028 }
9029 }
9030 }
9031 }
9032 }
9033 Staged::Truncate => {
9034 for (child_id, child_name, child_schema) in &live {
9038 for fk in &child_schema.constraints.foreign_keys {
9039 if fk.ref_table != tname {
9040 continue;
9041 }
9042 let child_rows = load_rows(*child_id)?;
9043 if child_rows
9044 .iter()
9045 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
9046 {
9047 return Err(MongrelError::Conflict(format!(
9048 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
9049 fk.name
9050 )));
9051 }
9052 }
9053 }
9054 }
9055 }
9056 }
9057 Ok(())
9058 }
9059
9060 fn validate_write_permissions(
9061 &self,
9062 staging: &[(u64, crate::txn::Staged)],
9063 principal: Option<&crate::auth::Principal>,
9064 control: Option<&crate::ExecutionControl>,
9065 ) -> Result<()> {
9066 commit_prepare_checkpoint(control, 0)?;
9067 if principal.is_none() && !self.auth_state.require_auth() {
9068 return Ok(());
9069 }
9070 let principal = principal.ok_or(MongrelError::AuthRequired)?;
9071 let needs = summarize_write_permissions(staging);
9072 let catalog = self.catalog.read();
9073
9074 if needs.values().any(|need| need.truncate) {
9075 self.require_for(Some(principal), &crate::auth::Permission::Admin)?;
9076 }
9077 for (need_index, (table_id, need)) in needs.into_iter().enumerate() {
9078 commit_prepare_checkpoint(control, need_index)?;
9079 let entry = catalog
9080 .tables
9081 .iter()
9082 .find(|entry| {
9083 entry.table_id == table_id
9084 && matches!(entry.state, TableState::Live | TableState::Building { .. })
9085 })
9086 .ok_or_else(|| {
9087 MongrelError::NotFound(format!(
9088 "live table {table_id} not found during write validation"
9089 ))
9090 })?;
9091 if matches!(entry.state, TableState::Building { .. }) {
9092 self.require_for(Some(principal), &crate::auth::Permission::Ddl)?;
9093 continue;
9094 }
9095 if need.insert {
9096 Self::require_columns_for_principal(
9097 &entry.name,
9098 &entry.schema,
9099 crate::auth::ColumnOperation::Insert,
9100 &need.insert_columns,
9101 principal,
9102 )?;
9103 }
9104 if need.update {
9105 Self::require_columns_for_principal(
9106 &entry.name,
9107 &entry.schema,
9108 crate::auth::ColumnOperation::Update,
9109 &need.update_columns,
9110 principal,
9111 )?;
9112 }
9113 if need.delete {
9114 self.require_for(
9115 Some(principal),
9116 &crate::auth::Permission::Delete {
9117 table: entry.name.clone(),
9118 },
9119 )?;
9120 }
9121 }
9122 Ok(())
9123 }
9124
9125 fn validate_security_writes(
9126 &self,
9127 staging: &[(u64, crate::txn::Staged)],
9128 read_epoch: Epoch,
9129 explicit_principal: Option<&crate::auth::Principal>,
9130 control: Option<&crate::ExecutionControl>,
9131 ) -> Result<()> {
9132 commit_prepare_checkpoint(control, 0)?;
9133 use crate::security::PolicyCommand;
9134 use crate::txn::Staged;
9135
9136 let catalog = self.catalog.read();
9137 if catalog.security.rls_tables.is_empty() {
9138 return Ok(());
9139 }
9140 let security = catalog.security.clone();
9141 let table_names = catalog
9142 .tables
9143 .iter()
9144 .filter(|entry| matches!(entry.state, TableState::Live))
9145 .map(|entry| (entry.table_id, entry.name.clone()))
9146 .collect::<HashMap<_, _>>();
9147 drop(catalog);
9148 if !staging.iter().any(|(table_id, _)| {
9149 table_names
9150 .get(table_id)
9151 .is_some_and(|table| security.rls_enabled(table))
9152 }) {
9153 return Ok(());
9154 }
9155 let principal = explicit_principal.ok_or(MongrelError::AuthRequired)?;
9156
9157 for (operation_index, (table_id, operation)) in staging.iter().enumerate() {
9158 commit_prepare_checkpoint(control, operation_index)?;
9159 let Some(table) = table_names.get(table_id) else {
9160 continue;
9161 };
9162 if !security.rls_enabled(table) || principal.is_admin {
9163 continue;
9164 }
9165 let denied = |command| MongrelError::PermissionDenied {
9166 required: match command {
9167 PolicyCommand::Insert => crate::auth::Permission::Insert {
9168 table: table.clone(),
9169 },
9170 PolicyCommand::Update => crate::auth::Permission::Update {
9171 table: table.clone(),
9172 },
9173 PolicyCommand::Delete | PolicyCommand::All | PolicyCommand::Select => {
9174 crate::auth::Permission::Delete {
9175 table: table.clone(),
9176 }
9177 }
9178 },
9179 principal: principal.username.clone(),
9180 };
9181 match operation {
9182 Staged::Put(cells) => {
9183 let mut row = crate::memtable::Row::new(RowId(0), Epoch(read_epoch.0));
9184 row.columns.extend(cells.iter().cloned());
9185 if !security.row_allowed(table, PolicyCommand::Insert, &row, principal, true) {
9186 return Err(denied(PolicyCommand::Insert));
9187 }
9188 }
9189 Staged::Update {
9190 row_id,
9191 new_row: cells,
9192 ..
9193 } => {
9194 let old = self
9195 .table_by_id(*table_id)?
9196 .lock()
9197 .get(*row_id, Snapshot::at(read_epoch))
9198 .ok_or_else(|| {
9199 MongrelError::NotFound(format!("row {} not found", row_id.0))
9200 })?;
9201 if !security.row_allowed(table, PolicyCommand::Update, &old, principal, false) {
9202 return Err(denied(PolicyCommand::Update));
9203 }
9204 let mut new = crate::memtable::Row::new(*row_id, Epoch(read_epoch.0));
9205 new.columns.extend(cells.iter().cloned());
9206 if !security.row_allowed(table, PolicyCommand::Update, &new, principal, true) {
9207 return Err(denied(PolicyCommand::Update));
9208 }
9209 }
9210 Staged::Delete(row_id) => {
9211 let old = self
9212 .table_by_id(*table_id)?
9213 .lock()
9214 .get(*row_id, Snapshot::at(read_epoch))
9215 .ok_or_else(|| {
9216 MongrelError::NotFound(format!("row {} not found", row_id.0))
9217 })?;
9218 if !security.row_allowed(table, PolicyCommand::Delete, &old, principal, false) {
9219 return Err(denied(PolicyCommand::Delete));
9220 }
9221 }
9222 Staged::Truncate => return Err(denied(PolicyCommand::Delete)),
9223 }
9224 }
9225 Ok(())
9226 }
9227
9228 #[allow(clippy::too_many_arguments)]
9235 pub(crate) fn commit_transaction_with_external_states(
9236 &self,
9237 txn_id: u64,
9238 read_epoch: Epoch,
9239 staging: Vec<(u64, crate::txn::Staged)>,
9240 external_states: Vec<(String, Vec<u8>)>,
9241 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
9242 security_principal: Option<crate::auth::Principal>,
9243 principal_catalog_bound: bool,
9244 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9245 ) -> Result<(Epoch, Vec<RowId>)> {
9246 self.commit_transaction_with_external_states_inner(
9247 txn_id,
9248 read_epoch,
9249 staging,
9250 external_states,
9251 materialized_view_updates,
9252 security_principal,
9253 principal_catalog_bound,
9254 external_trigger_bridge,
9255 None,
9256 None,
9257 )
9258 }
9259
9260 #[allow(clippy::too_many_arguments)]
9261 pub(crate) fn commit_transaction_with_external_states_controlled(
9262 &self,
9263 txn_id: u64,
9264 read_epoch: Epoch,
9265 staging: Vec<(u64, crate::txn::Staged)>,
9266 external_states: Vec<(String, Vec<u8>)>,
9267 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
9268 security_principal: Option<crate::auth::Principal>,
9269 principal_catalog_bound: bool,
9270 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9271 control: &crate::ExecutionControl,
9272 before_commit: &mut dyn FnMut() -> Result<()>,
9273 ) -> Result<(Epoch, Vec<RowId>)> {
9274 self.commit_transaction_with_external_states_inner(
9275 txn_id,
9276 read_epoch,
9277 staging,
9278 external_states,
9279 materialized_view_updates,
9280 security_principal,
9281 principal_catalog_bound,
9282 external_trigger_bridge,
9283 Some(control),
9284 Some(before_commit),
9285 )
9286 }
9287
9288 #[allow(clippy::too_many_arguments)]
9289 fn commit_transaction_with_external_states_inner(
9290 &self,
9291 txn_id: u64,
9292 read_epoch: Epoch,
9293 mut staging: Vec<(u64, crate::txn::Staged)>,
9294 external_states: Vec<(String, Vec<u8>)>,
9295 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
9296 mut security_principal: Option<crate::auth::Principal>,
9297 principal_catalog_bound: bool,
9298 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9299 control: Option<&crate::ExecutionControl>,
9300 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
9301 ) -> Result<(Epoch, Vec<RowId>)> {
9302 use crate::memtable::Row;
9303 use crate::txn::{Staged, StagedOp, WriteKey};
9304 use crate::wal::Op;
9305 use std::collections::hash_map::DefaultHasher;
9306 use std::hash::{Hash, Hasher};
9307 use std::sync::atomic::Ordering;
9308
9309 if txn_id == crate::wal::SYSTEM_TXN_ID {
9310 return Err(MongrelError::Full(
9311 "per-open transaction id namespace exhausted; reopen the database".into(),
9312 ));
9313 }
9314 if self.read_only {
9315 return Err(MongrelError::ReadOnlyReplica);
9316 }
9317 commit_prepare_checkpoint(control, 0)?;
9318 let observed_security_version = self.security_coordinator.version.load(Ordering::Acquire);
9319 self.refresh_security_catalog_if_stale(observed_security_version)?;
9320 let trigger_binding = trigger_catalog_binding(&self.catalog.read());
9321 if self.auth_state.require_auth() && security_principal.is_none() {
9322 return Err(MongrelError::AuthRequired);
9323 }
9324 {
9325 let catalog = self.catalog.read();
9326 if catalog.require_auth
9327 || principal_catalog_bound
9328 || security_principal
9329 .as_ref()
9330 .is_some_and(|principal| principal.user_id != 0)
9331 {
9332 let principal = security_principal
9333 .as_ref()
9334 .ok_or(MongrelError::AuthRequired)?;
9335 security_principal =
9336 Self::resolve_bound_principal_from_catalog(&catalog, principal);
9337 if security_principal.is_none() {
9338 return Err(MongrelError::AuthRequired);
9339 }
9340 }
9341 }
9342 let _replication_guard = self.replication_barrier.read();
9343 if self.poisoned.load(Ordering::Relaxed) {
9344 return Err(MongrelError::Other(
9345 "database poisoned by fsync error".into(),
9346 ));
9347 }
9348 let mut external_states = dedup_external_states(external_states);
9349 if !external_states.is_empty() {
9350 let cat = self.catalog.read();
9351 for (name, _) in &external_states {
9352 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
9353 return Err(MongrelError::NotFound(format!(
9354 "external table {name:?} not found"
9355 )));
9356 }
9357 }
9358 }
9359 let prepared_materialized_views = {
9360 let mut deduplicated = HashMap::new();
9361 for (definition_index, definition) in materialized_view_updates.into_iter().enumerate()
9362 {
9363 commit_prepare_checkpoint(control, definition_index)?;
9364 if definition.name.is_empty() || definition.query.trim().is_empty() {
9365 return Err(MongrelError::InvalidArgument(
9366 "materialized view name and query must not be empty".into(),
9367 ));
9368 }
9369 deduplicated.insert(definition.name.clone(), definition);
9370 }
9371 let catalog = self.catalog.read();
9372 let mut prepared = Vec::with_capacity(deduplicated.len());
9373 for (definition_index, definition) in deduplicated.into_values().enumerate() {
9374 commit_prepare_checkpoint(control, definition_index)?;
9375 let table_id = catalog
9376 .live(&definition.name)
9377 .ok_or_else(|| {
9378 MongrelError::NotFound(format!(
9379 "materialized view table {:?} not found",
9380 definition.name
9381 ))
9382 })?
9383 .table_id;
9384 prepared.push((table_id, definition));
9385 }
9386 prepared.sort_by(|left, right| left.1.name.cmp(&right.1.name));
9387 prepared
9388 };
9389
9390 self.fill_auto_increment_for_staging(&mut staging, control)?;
9393 self.expand_table_triggers(
9394 &mut staging,
9395 read_epoch,
9396 external_trigger_bridge,
9397 &mut external_states,
9398 control,
9399 )?;
9400 self.fill_auto_increment_for_staging(&mut staging, control)?;
9401 external_states = dedup_external_states(external_states);
9402 let expected_external_generations = {
9403 let catalog = self.catalog.read();
9404 let mut generations = HashMap::with_capacity(external_states.len());
9405 for (name, _) in &external_states {
9406 let entry = catalog
9407 .external_tables
9408 .iter()
9409 .find(|entry| entry.name == *name)
9410 .ok_or_else(|| {
9411 MongrelError::NotFound(format!("external table {name:?} not found"))
9412 })?;
9413 generations.insert(name.clone(), entry.created_epoch);
9414 }
9415 generations
9416 };
9417
9418 self.validate_constraints(&mut staging, read_epoch, control)?;
9423 self.validate_write_permissions(&staging, security_principal.as_ref(), control)?;
9424 self.validate_security_writes(&staging, read_epoch, security_principal.as_ref(), control)?;
9425 let mut normalized = Vec::with_capacity(staging.len() * 2);
9426 for (staged_index, (table_id, op)) in staging.into_iter().enumerate() {
9427 commit_prepare_checkpoint(control, staged_index)?;
9428 match op {
9429 crate::txn::Staged::Update {
9430 row_id,
9431 new_row: cells,
9432 ..
9433 } => {
9434 normalized.push((table_id, crate::txn::Staged::Delete(row_id)));
9435 normalized.push((table_id, crate::txn::Staged::Put(cells)));
9436 }
9437 op => normalized.push((table_id, op)),
9438 }
9439 }
9440 staging = normalized;
9441 let has_changes = !staging.is_empty()
9442 || !external_states.is_empty()
9443 || !prepared_materialized_views.is_empty();
9444 let truncated_tables: HashSet<u64> = staging
9445 .iter()
9446 .filter_map(|(table_id, op)| matches!(op, Staged::Truncate).then_some(*table_id))
9447 .collect();
9448
9449 let write_keys = {
9450 let cat = self.catalog.read();
9451 let mut keys: Vec<WriteKey> = Vec::new();
9452 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
9453 commit_prepare_checkpoint(control, staged_index)?;
9454 match staged {
9455 Staged::Put(cells) => {
9456 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
9457 for col in &entry.schema.columns {
9458 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
9459 if let Some((_, val)) =
9460 cells.iter().find(|(id, _)| *id == col.id)
9461 {
9462 let mut h = DefaultHasher::new();
9463 val.encode_key().hash(&mut h);
9464 keys.push(WriteKey::Unique {
9465 table_id: *table_id,
9466 index_id: 0,
9467 key_hash: h.finish(),
9468 });
9469 }
9470 }
9471 }
9472 for uc in &entry.schema.constraints.uniques {
9479 if let Some(key_bytes) = crate::constraint::encode_composite_key(
9480 &uc.columns,
9481 &cells.iter().cloned().collect(),
9482 ) {
9483 let mut h = DefaultHasher::new();
9484 key_bytes.hash(&mut h);
9485 keys.push(WriteKey::Unique {
9486 table_id: *table_id,
9487 index_id: uc.id | 0x8000,
9488 key_hash: h.finish(),
9489 });
9490 }
9491 }
9492 }
9493 }
9494 Staged::Delete(rid) => keys.push(WriteKey::Row {
9495 table_id: *table_id,
9496 row_id: rid.0,
9497 }),
9498 Staged::Truncate => keys.push(WriteKey::Table {
9499 table_id: *table_id,
9500 }),
9501 Staged::Update { .. } => {
9502 return Err(MongrelError::Other(
9503 "transaction contains an unnormalized update during preparation".into(),
9504 ));
9505 }
9506 }
9507 }
9508 for (external_index, (name, _)) in external_states.iter().enumerate() {
9509 commit_prepare_checkpoint(control, external_index)?;
9510 let mut h = DefaultHasher::new();
9511 name.hash(&mut h);
9512 keys.push(WriteKey::Unique {
9513 table_id: EXTERNAL_TABLE_ID,
9514 index_id: 0,
9515 key_hash: h.finish(),
9516 });
9517 }
9518 keys
9519 };
9520
9521 let min_active = self.active_txns.min_read_epoch();
9523 if min_active < u64::MAX {
9524 self.conflicts.prune_below(Epoch(min_active));
9525 }
9526
9527 if self.conflicts.conflicts(&write_keys, read_epoch) {
9531 return Err(MongrelError::Conflict(
9532 "write-write conflict (pre-validate, first-committer-wins)".into(),
9533 ));
9534 }
9535 let pre_validate_version = self.conflicts.version();
9536
9537 let mut spilled: Vec<SpilledRun> = Vec::new();
9541 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
9542 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
9546 {
9547 let mut table_bytes: HashMap<u64, u64> = HashMap::new();
9548 let mut put_indexes: HashMap<u64, Vec<usize>> = HashMap::new();
9549 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
9550 commit_prepare_checkpoint(control, staged_index)?;
9551 if let Staged::Put(cells) = staged {
9552 let bytes = cells.iter().fold(32_u64, |bytes, (_, value)| {
9553 bytes.saturating_add(value.estimated_bytes())
9554 });
9555 let table_bytes = table_bytes.entry(*table_id).or_default();
9556 *table_bytes = table_bytes.saturating_add(bytes);
9557 put_indexes.entry(*table_id).or_default().push(staged_index);
9558 }
9559 }
9560 let tables = self.tables.read();
9561 for (table_index, (&table_id, &bytes)) in table_bytes.iter().enumerate() {
9562 commit_prepare_checkpoint(control, table_index)?;
9563 if bytes
9564 <= self
9565 .spill_threshold
9566 .load(std::sync::atomic::Ordering::Relaxed)
9567 {
9568 continue;
9569 }
9570 let Some(handle) = tables.get(&table_id) else {
9571 continue;
9572 };
9573 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
9574 let mut t = handle.lock();
9575 let tdir = t.table_dir().to_path_buf();
9576 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
9577 std::fs::create_dir_all(&txn_dir)?;
9578 let run_id = t.alloc_run_id()? as u128;
9579 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
9580 let final_path = t.run_path(run_id as u64);
9581
9582 let mut rows: Vec<Row> = Vec::new();
9583 for (put_index, staged_index) in put_indexes[&table_id].iter().enumerate() {
9584 commit_prepare_checkpoint(control, put_index)?;
9585 let Staged::Put(cells) = &mut staging[*staged_index].1 else {
9586 return Err(MongrelError::Other(
9587 "transaction put index no longer references a put".into(),
9588 ));
9589 };
9590 t.validate_cells_not_null(cells)?;
9591 let row_id = t.alloc_row_id()?;
9592 let mut row = Row::new(row_id, Epoch(0));
9593 row.columns.extend(std::mem::take(cells));
9594 rows.push(row);
9595 }
9596 let schema = t.schema_ref().clone();
9597 let kek = t.kek_ref().cloned();
9598 let specs = t.indexable_column_specs();
9599 drop(t);
9600
9601 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
9602 .uniform_epoch(true);
9603 if let Some(ref kek) = kek {
9604 writer = writer.with_encryption(kek.as_ref(), specs);
9605 }
9606 commit_prepare_checkpoint(control, 0)?;
9607 let header = writer.write(&pending_path, &rows)?;
9608 commit_prepare_checkpoint(control, 0)?;
9609 let row_count = header.row_count;
9610 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
9611 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
9612
9613 spilled.push(SpilledRun {
9614 table_id,
9615 run_id,
9616 pending_path,
9617 final_path,
9618 rows,
9619 row_count,
9620 min_rid,
9621 max_rid,
9622 content_hash: header.content_hash,
9623 });
9624 spilled_tables.insert(table_id);
9625 }
9626 }
9627
9628 if spill_guard.is_some() {
9630 if let Some(hook) = self.spill_hook.lock().as_ref() {
9631 hook();
9632 }
9633 }
9634
9635 let mut prebuilt: Vec<Option<Row>> = std::iter::repeat_with(|| None)
9646 .take(staging.len())
9647 .collect();
9648 let mut delete_images: Vec<Option<Row>> = std::iter::repeat_with(|| None)
9649 .take(staging.len())
9650 .collect();
9651 {
9652 let mut indexes_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
9653 for (index, (table_id, staged)) in staging.iter().enumerate() {
9654 commit_prepare_checkpoint(control, index)?;
9655 if matches!(staged, Staged::Delete(_))
9656 || matches!(staged, Staged::Put(_) if !spilled_tables.contains(table_id))
9657 {
9658 indexes_by_table.entry(*table_id).or_default().push(index);
9659 }
9660 }
9661 let tables = self.tables.read();
9662 for (table_index, (table_id, indexes)) in indexes_by_table.into_iter().enumerate() {
9663 commit_prepare_checkpoint(control, table_index)?;
9664 let handle = tables.get(&table_id).ok_or_else(|| {
9665 MongrelError::NotFound(format!("table {table_id} not mounted"))
9666 })?;
9667 #[cfg(test)]
9668 PREBUILD_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
9669 let mut t = handle.lock();
9670 for (prepare_index, index) in indexes.into_iter().enumerate() {
9671 commit_prepare_checkpoint(control, prepare_index)?;
9672 match &staging[index].1 {
9673 Staged::Put(cells) if !spilled_tables.contains(&table_id) => {
9674 t.validate_cells_not_null(cells)?;
9675 let mut row = Row::new(t.alloc_row_id()?, Epoch(0));
9676 for (column, value) in cells {
9677 row.columns.insert(*column, value.clone());
9678 }
9679 prebuilt[index] = Some(row);
9680 }
9681 Staged::Delete(row_id) => {
9682 delete_images[index] = t.get(*row_id, Snapshot::at(read_epoch));
9683 }
9684 Staged::Put(_) | Staged::Truncate => {}
9685 Staged::Update { .. } => {
9686 return Err(MongrelError::Other(
9687 "transaction contains an unnormalized update during row preparation"
9688 .into(),
9689 ));
9690 }
9691 }
9692 }
9693 }
9694 }
9695
9696 let prepared_table_handles = {
9700 let table_ids: HashSet<u64> = staging.iter().map(|(table_id, _)| *table_id).collect();
9701 let put_table_ids: HashSet<u64> = staging
9702 .iter()
9703 .filter_map(|(table_id, staged)| {
9704 matches!(staged, Staged::Put(_)).then_some(*table_id)
9705 })
9706 .collect();
9707 let tables = self.tables.read();
9708 let mut handles = HashMap::with_capacity(table_ids.len());
9709 for (table_index, table_id) in table_ids.into_iter().enumerate() {
9710 commit_prepare_checkpoint(control, table_index)?;
9711 let handle = tables.get(&table_id).ok_or_else(|| {
9712 MongrelError::NotFound(format!("table {table_id} not mounted"))
9713 })?;
9714 if put_table_ids.contains(&table_id) {
9715 match control {
9716 Some(control) => {
9717 handle.lock().prepare_durable_publish_controlled(control)?
9718 }
9719 None => handle.lock().prepare_durable_publish()?,
9720 }
9721 }
9722 handles.insert(table_id, handle.clone());
9723 }
9724 handles
9725 };
9726
9727 let mut prepared_run_links = PreparedRunLinks::prepare(&spilled)?;
9731
9732 let mut spilled_row_ids: HashMap<u64, VecDeque<RowId>> = spilled
9733 .iter()
9734 .map(|run| {
9735 (
9736 run.table_id,
9737 run.rows.iter().map(|row| row.row_id).collect(),
9738 )
9739 })
9740 .collect();
9741 let committed_row_ids = staging
9742 .iter()
9743 .enumerate()
9744 .filter_map(|(index, (table_id, staged))| {
9745 if !matches!(staged, Staged::Put(_)) {
9746 return None;
9747 }
9748 prebuilt[index].as_ref().map(|row| row.row_id).or_else(|| {
9749 spilled_row_ids
9750 .get_mut(table_id)
9751 .and_then(VecDeque::pop_front)
9752 })
9753 })
9754 .collect();
9755
9756 let mut prepared_external = Vec::with_capacity(external_states.len());
9757 for (external_index, (name, state)) in external_states.iter().enumerate() {
9758 commit_prepare_checkpoint(control, external_index)?;
9759 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
9760 prepared_external.push((name.clone(), state.clone(), pending));
9761 }
9762
9763 let added_runs: Vec<crate::wal::AddedRun> = spilled
9765 .iter()
9766 .map(|s| crate::wal::AddedRun {
9767 table_id: s.table_id,
9768 run_id: s.run_id,
9769 row_count: s.row_count,
9770 level: 0,
9771 min_row_id: s.min_rid,
9772 max_row_id: s.max_rid,
9773 content_hash: s.content_hash,
9774 })
9775 .collect();
9776 if let Some(hook) = self.catalog_commit_hook.lock().as_ref() {
9777 hook();
9778 }
9779 let security_guard = self.security_coordinator.gate.read();
9783 if self.security_coordinator.version.load(Ordering::Acquire) != observed_security_version {
9784 return Err(MongrelError::Conflict(
9785 "security policy changed during write".into(),
9786 ));
9787 }
9788 if spill_guard.is_some() {
9789 if let Some(hook) = self.security_commit_hook.lock().as_ref() {
9790 hook();
9791 }
9792 }
9793 let commit_guard = self.commit_lock.lock();
9794 let catalog_generation_result = (|| {
9795 {
9796 let catalog = self.catalog.read();
9797 for table_id in prepared_table_handles.keys() {
9798 let is_current = catalog.tables.iter().any(|entry| {
9799 entry.table_id == *table_id
9800 && matches!(entry.state, TableState::Live | TableState::Building { .. })
9801 });
9802 if !is_current {
9803 return Err(MongrelError::Conflict(format!(
9804 "table {table_id} changed during transaction preparation"
9805 )));
9806 }
9807 }
9808 for (name, created_epoch) in &expected_external_generations {
9809 let current = catalog
9810 .external_tables
9811 .iter()
9812 .find(|entry| entry.name == *name)
9813 .map(|entry| entry.created_epoch);
9814 if current != Some(*created_epoch) {
9815 return Err(MongrelError::Conflict(format!(
9816 "external table {name:?} changed during transaction preparation"
9817 )));
9818 }
9819 }
9820 for (table_id, definition) in &prepared_materialized_views {
9821 let current = catalog.live(&definition.name).map(|entry| entry.table_id);
9822 if current != Some(*table_id) {
9823 return Err(MongrelError::Conflict(format!(
9824 "materialized view {:?} changed during transaction preparation",
9825 definition.name
9826 )));
9827 }
9828 }
9829 if trigger_catalog_binding(&catalog) != trigger_binding {
9830 return Err(MongrelError::Conflict(
9831 "trigger or referenced table generation changed during transaction preparation"
9832 .into(),
9833 ));
9834 }
9835 }
9836 let tables = self.tables.read();
9837 for (table_id, prepared) in &prepared_table_handles {
9838 if !tables
9839 .get(table_id)
9840 .is_some_and(|current| current.ptr_eq(prepared))
9841 {
9842 return Err(MongrelError::Conflict(format!(
9843 "table {table_id} mount changed during transaction preparation"
9844 )));
9845 }
9846 }
9847 Ok(())
9848 })();
9849 if let Err(error) = catalog_generation_result {
9850 drop(commit_guard);
9851 for (_, _, pending) in &prepared_external {
9852 let _ = std::fs::remove_file(pending);
9853 }
9854 return Err(error);
9855 }
9856 let new_epoch = self.epoch.assigned().next();
9860 let mut spilled_wal_bytes = 0;
9861 let mut spilled_wal_records = Vec::<(u64, Op)>::new();
9862 let spill_prepare = (|| {
9863 for run in &mut spilled {
9864 for row in &mut run.rows {
9865 row.committed_epoch = new_epoch;
9866 }
9867 for rows in encode_spilled_row_chunks(
9868 &run.rows,
9869 &mut spilled_wal_bytes,
9870 SPILLED_WAL_TOTAL_MAX_BYTES,
9871 control,
9872 )? {
9873 spilled_wal_records.push((
9874 run.table_id,
9875 Op::SpilledRows {
9876 table_id: run.table_id,
9877 rows,
9878 },
9879 ));
9880 }
9881 }
9882 Result::<()>::Ok(())
9883 })();
9884 if let Err(error) = spill_prepare {
9885 for (_, _, pending) in &prepared_external {
9886 let _ = std::fs::remove_file(pending);
9887 }
9888 return Err(error);
9889 }
9890 let (new_epoch, mut _epoch_guard, applies, committed_materialized_views, commit_seq) = {
9891 let mut wal = self.shared_wal.lock();
9892
9893 if self.conflicts.version() != pre_validate_version
9898 && self.conflicts.conflicts(&write_keys, read_epoch)
9899 {
9900 drop(wal);
9903 for (_, _, pending) in &prepared_external {
9904 let _ = std::fs::remove_file(pending);
9905 }
9906 return Err(MongrelError::Conflict(
9907 "write-write conflict (sequencer delta re-check)".into(),
9908 ));
9909 }
9910
9911 if let Some(control) = control {
9912 if let Err(error) = control.checkpoint() {
9913 drop(wal);
9914 for (_, _, pending) in &prepared_external {
9915 let _ = std::fs::remove_file(pending);
9916 }
9917 return Err(error);
9918 }
9919 }
9920 let mut applies = Vec::<TableApplyBatch>::new();
9921 let mut apply_indexes = HashMap::<u64, usize>::new();
9922 let mut committed_materialized_views = Vec::new();
9923 let mut wal_records = spilled_wal_records;
9924
9925 let mut index = 0;
9926 while index < staging.len() {
9927 let table_id = staging[index].0;
9928 let handle = prepared_table_handles
9929 .get(&table_id)
9930 .cloned()
9931 .ok_or_else(|| {
9932 MongrelError::NotFound(format!("table {table_id} not prepared"))
9933 })?;
9934 let batch_index = *apply_indexes.entry(table_id).or_insert_with(|| {
9935 let index = applies.len();
9936 applies.push(TableApplyBatch {
9937 table_id,
9938 handle,
9939 ops: Vec::new(),
9940 });
9941 index
9942 });
9943
9944 if spilled_tables.contains(&table_id) && matches!(&staging[index].1, Staged::Put(_))
9947 {
9948 index += 1;
9949 continue;
9950 }
9951
9952 match &staging[index].1 {
9953 Staged::Put(_) => {
9954 let mut rows = Vec::new();
9955 while index < staging.len()
9956 && staging[index].0 == table_id
9957 && matches!(&staging[index].1, Staged::Put(_))
9958 {
9959 let mut row = prebuilt[index].take().ok_or_else(|| {
9960 MongrelError::Other(
9961 "transaction prepare lost a prebuilt put row".into(),
9962 )
9963 })?;
9964 row.committed_epoch = new_epoch;
9965 rows.push(row);
9966 index += 1;
9967 }
9968 let payload = bincode::serialize(&rows)
9969 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
9970 wal_records.push((
9971 table_id,
9972 Op::Put {
9973 table_id,
9974 rows: payload,
9975 },
9976 ));
9977 applies[batch_index].ops.push(StagedOp::Put(rows));
9978 }
9979 Staged::Delete(_) => {
9980 let mut row_ids = Vec::new();
9981 while index < staging.len()
9982 && staging[index].0 == table_id
9983 && matches!(&staging[index].1, Staged::Delete(_))
9984 {
9985 let Staged::Delete(row_id) = &staging[index].1 else {
9986 return Err(MongrelError::Other(
9987 "transaction delete batch changed during WAL preparation"
9988 .into(),
9989 ));
9990 };
9991 if let Some(before) = &delete_images[index] {
9992 wal_records.push((
9993 table_id,
9994 Op::BeforeImage {
9995 table_id,
9996 row_id: *row_id,
9997 row: bincode::serialize(before).map_err(|error| {
9998 MongrelError::Other(format!(
9999 "before-image serialize: {error}"
10000 ))
10001 })?,
10002 },
10003 ));
10004 }
10005 row_ids.push(*row_id);
10006 index += 1;
10007 }
10008 wal_records.push((
10009 table_id,
10010 Op::Delete {
10011 table_id,
10012 row_ids: row_ids.clone(),
10013 },
10014 ));
10015 applies[batch_index].ops.push(StagedOp::Delete(row_ids));
10016 }
10017 Staged::Truncate => {
10018 wal_records.push((table_id, Op::TruncateTable { table_id }));
10019 applies[batch_index].ops.push(StagedOp::Truncate);
10020 index += 1;
10021 }
10022 Staged::Update { .. } => {
10023 return Err(MongrelError::Other(
10024 "transaction contains an unnormalized update at the sequencer".into(),
10025 ));
10026 }
10027 }
10028 }
10029
10030 for (name, state, _) in &prepared_external {
10031 wal_records.push((
10032 EXTERNAL_TABLE_ID,
10033 Op::ExternalTableState {
10034 name: name.clone(),
10035 state: state.clone(),
10036 },
10037 ));
10038 }
10039
10040 for (table_id, definition) in &prepared_materialized_views {
10041 let mut definition = definition.clone();
10042 definition.last_refresh_epoch = new_epoch.0;
10043 wal_records.push((
10044 *table_id,
10045 Op::Ddl(crate::wal::DdlOp::SetMaterializedView {
10046 name: definition.name.clone(),
10047 definition_json: crate::wal::DdlOp::encode_materialized_view(&definition)?,
10048 }),
10049 ));
10050 committed_materialized_views.push(definition);
10051 }
10052 if !committed_materialized_views.is_empty() {
10053 let mut next_catalog = self.catalog.read().clone();
10054 for definition in &committed_materialized_views {
10055 if let Some(existing) = next_catalog
10056 .materialized_views
10057 .iter_mut()
10058 .find(|existing| existing.name == definition.name)
10059 {
10060 *existing = definition.clone();
10061 } else {
10062 next_catalog.materialized_views.push(definition.clone());
10063 }
10064 }
10065 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
10066 wal_records.push((
10067 WAL_TABLE_ID,
10068 Op::Ddl(crate::wal::DdlOp::CatalogSnapshot {
10069 catalog_json: crate::wal::DdlOp::encode_catalog(&next_catalog)?,
10070 }),
10071 ));
10072 }
10073
10074 if let Some(control) = control {
10075 if let Err(error) = control.checkpoint() {
10076 drop(wal);
10077 for (_, _, pending) in &prepared_external {
10078 let _ = std::fs::remove_file(pending);
10079 }
10080 return Err(error);
10081 }
10082 }
10083 if let Some(before_commit) = before_commit.as_mut() {
10084 if let Err(error) = before_commit() {
10085 drop(wal);
10086 for (_, _, pending) in &prepared_external {
10087 let _ = std::fs::remove_file(pending);
10088 }
10089 return Err(error);
10090 }
10091 }
10092
10093 let assigned_epoch = self.epoch.bump_assigned();
10094 let _epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
10095 if assigned_epoch != new_epoch {
10096 for (_, _, pending) in &prepared_external {
10097 let _ = std::fs::remove_file(pending);
10098 }
10099 return Err(MongrelError::Conflict(
10100 "commit epoch changed while sequencer lock was held".into(),
10101 ));
10102 }
10103
10104 prepared_run_links.disarm();
10108
10109 let append: Result<u64> = (|| {
10110 for (table_id, op) in wal_records {
10111 wal.append(txn_id, table_id, op)?;
10112 }
10113 wal.append_commit(txn_id, new_epoch, &added_runs)
10114 })();
10115 let commit_seq =
10116 append.map_err(|error| self.commit_outcome_unknown(new_epoch, error))?;
10117
10118 self.conflicts.record(&write_keys, new_epoch);
10123 (
10124 new_epoch,
10125 _epoch_guard,
10126 applies,
10127 committed_materialized_views,
10128 commit_seq,
10129 )
10130 };
10131 drop(commit_guard);
10132
10133 self.await_durable_commit(commit_seq, new_epoch)?;
10135 drop(security_guard);
10136
10137 let publish_result: Result<()> = {
10139 let mut first_error = None;
10140 let mut spilled_by_table: HashMap<u64, Vec<&SpilledRun>> = HashMap::new();
10141 for run in &spilled {
10142 spilled_by_table.entry(run.table_id).or_default().push(run);
10143 }
10144 let mut modified_tables = Vec::with_capacity(applies.len());
10145 for batch in applies {
10148 #[cfg(test)]
10149 PUBLISH_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
10150 let mut t = batch.handle.lock();
10151 for op in batch.ops {
10152 match op {
10153 StagedOp::Put(rows) => t.apply_put_rows_prepared(rows),
10154 StagedOp::Delete(row_ids) => {
10155 for row_id in row_ids {
10156 t.apply_delete(row_id, new_epoch);
10157 }
10158 }
10159 StagedOp::Truncate => t.apply_truncate(new_epoch),
10160 }
10161 }
10162 if let Some(runs) = spilled_by_table.remove(&batch.table_id) {
10163 for run in runs {
10164 t.link_run(crate::manifest::RunRef {
10165 run_id: run.run_id,
10166 level: 0,
10167 epoch_created: new_epoch.0,
10168 row_count: run.row_count,
10169 });
10170 t.apply_run_metadata_prepared(&run.rows)?;
10171 if truncated_tables.contains(&batch.table_id) {
10172 t.set_flushed_epoch(new_epoch);
10177 }
10178 }
10179 }
10180 t.invalidate_pending_cache();
10181 drop(t);
10182 modified_tables.push(batch.handle);
10183 }
10184
10185 for handle in modified_tables {
10189 #[cfg(test)]
10190 COMMIT_MANIFEST_WRITES.with(|count| count.set(count.get() + 1));
10191 if let Err(error) = handle.lock().persist_manifest(new_epoch) {
10192 first_error.get_or_insert(error);
10193 }
10194 }
10195 for (name, _, pending) in &prepared_external {
10196 if let Err(error) = publish_external_state_file(&self.root, name, pending) {
10197 first_error.get_or_insert(error);
10198 }
10199 }
10200 if !committed_materialized_views.is_empty() {
10201 let mut next_catalog = self.catalog.read().clone();
10202 for definition in committed_materialized_views {
10203 if let Some(existing) = next_catalog
10204 .materialized_views
10205 .iter_mut()
10206 .find(|existing| existing.name == definition.name)
10207 {
10208 *existing = definition;
10209 } else {
10210 next_catalog.materialized_views.push(definition);
10211 }
10212 }
10213 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
10214 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
10215 first_error.get_or_insert(error);
10216 }
10217 }
10218 match first_error {
10219 Some(error) => Err(error),
10220 None => Ok(()),
10221 }
10222 };
10223
10224 if has_changes {
10225 let _ = self.change_wake.send(());
10226 }
10227 self.finish_durable_publish(new_epoch, &mut _epoch_guard, publish_result)?;
10228 Ok((new_epoch, committed_row_ids))
10229 }
10230
10231 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
10234 let e = self.epoch.visible();
10235 let g = self.snapshots.register(e);
10236 (Snapshot::at(e), g)
10237 }
10238
10239 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
10242 let e = self.epoch.visible();
10243 let g = self.snapshots.register_owned(e);
10244 (Snapshot::at(e), g)
10245 }
10246
10247 pub fn set_history_retention_epochs(&self, epochs: u64) -> Result<()> {
10252 let _guard = self.ddl_lock.lock();
10253 let current = self.epoch.visible();
10254 let (old_epochs, old_start) = self.snapshots.history_config();
10255 let earliest_already_guaranteed = if old_epochs == 0 {
10256 current
10257 } else {
10258 Epoch(old_start.0.max(current.0.saturating_sub(old_epochs)))
10259 };
10260 let start = if epochs == 0 {
10261 current
10262 } else {
10263 earliest_already_guaranteed
10264 };
10265 let published = std::cell::Cell::new(false);
10266 let result = write_history_retention(&self.root, epochs, start, || {
10267 self.snapshots.configure_history(epochs, start);
10268 published.set(true);
10269 });
10270 match result {
10271 Err(error) if published.get() => Err(MongrelError::CommitOutcomeUnknown {
10272 epoch: current.0,
10273 message: format!("history-retention publication was not durable: {error}"),
10274 }),
10275 result => result,
10276 }
10277 }
10278
10279 pub fn history_retention_epochs(&self) -> u64 {
10280 self.snapshots.history_config().0
10281 }
10282
10283 pub fn earliest_retained_epoch(&self) -> Epoch {
10284 let current = self.epoch.visible();
10285 self.snapshots.history_floor(current).unwrap_or(current)
10286 }
10287
10288 pub fn snapshot_at_owned(&self, epoch: Epoch) -> Result<(Snapshot, OwnedSnapshotGuard)> {
10291 let current = self.epoch.visible();
10292 if epoch > current {
10293 return Err(MongrelError::InvalidArgument(format!(
10294 "epoch {} is in the future; current epoch is {}",
10295 epoch.0, current.0
10296 )));
10297 }
10298 let earliest = self.earliest_retained_epoch();
10299 if epoch < earliest {
10300 return Err(MongrelError::InvalidArgument(format!(
10301 "epoch {} is no longer retained; earliest available epoch is {}",
10302 epoch.0, earliest.0
10303 )));
10304 }
10305 let guard = self.snapshots.register_owned(epoch);
10306 Ok((Snapshot::at(epoch), guard))
10307 }
10308
10309 pub fn table_names(&self) -> Vec<String> {
10311 self.catalog
10312 .read()
10313 .tables
10314 .iter()
10315 .filter(|t| matches!(t.state, TableState::Live))
10316 .map(|t| t.name.clone())
10317 .collect()
10318 }
10319
10320 pub fn close(&self) -> Result<()> {
10326 for name in self.table_names() {
10327 if let Ok(handle) = self.table(&name) {
10328 if let Err(e) = handle.lock().close() {
10329 eprintln!("[close] flush failed for {name}: {e}");
10330 }
10331 }
10332 }
10333 Ok(())
10334 }
10335
10336 pub fn compact(&self) -> Result<(usize, usize)> {
10343 self.require(&crate::auth::Permission::Ddl)?;
10344 let mut compacted = 0;
10345 let mut skipped = 0;
10346 for name in self.table_names() {
10347 let Ok(handle) = self.table(&name) else {
10348 continue;
10349 };
10350 {
10351 let mut t = handle.lock();
10352 let before = t.run_count();
10353 if before < 2 && !t.should_compact() {
10354 skipped += 1;
10355 continue;
10356 }
10357 match t.compact() {
10358 Ok(()) => {
10359 let after = t.run_count();
10360 compacted += 1;
10361 eprintln!("[compact] {name}: {before} -> {after} runs");
10362 }
10363 Err(e) => {
10364 eprintln!("[compact] {name}: compaction failed: {e}");
10365 skipped += 1;
10366 }
10367 }
10368 }
10369 }
10370 Ok((compacted, skipped))
10371 }
10372
10373 pub fn compact_table(&self, name: &str) -> Result<bool> {
10376 self.require(&crate::auth::Permission::Ddl)?;
10377 let handle = self.table(name)?;
10378 let mut t = handle.lock();
10379 let before = t.run_count();
10380 if before < 2 {
10381 return Ok(false);
10382 }
10383 t.compact()?;
10384 Ok(t.run_count() < before)
10385 }
10386
10387 pub fn table(&self, name: &str) -> Result<TableHandle> {
10389 let cat = self.catalog.read();
10390 let entry = cat
10391 .live(name)
10392 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
10393 let id = entry.table_id;
10394 drop(cat);
10395 self.tables
10396 .read()
10397 .get(&id)
10398 .cloned()
10399 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
10400 }
10401
10402 pub fn has_ttl_tables(&self) -> bool {
10405 self.tables
10406 .read()
10407 .values()
10408 .any(|table| table.lock().ttl().is_some())
10409 }
10410
10411 pub(crate) fn table_by_id(&self, id: u64) -> Result<TableHandle> {
10414 self.tables
10415 .read()
10416 .get(&id)
10417 .cloned()
10418 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
10419 }
10420
10421 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
10427 if name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
10428 return Err(MongrelError::InvalidArgument(format!(
10429 "table names beginning with {CTAS_BUILD_TABLE_PREFIX:?} are reserved"
10430 )));
10431 }
10432 self.create_table_with_state(name, schema, TableState::Live)
10433 }
10434
10435 #[doc(hidden)]
10437 pub fn create_building_table(
10438 &self,
10439 build_name: &str,
10440 intended_name: &str,
10441 query_id: &str,
10442 schema: Schema,
10443 ) -> Result<u64> {
10444 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10445 || intended_name.is_empty()
10446 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10447 || query_id.is_empty()
10448 {
10449 return Err(MongrelError::InvalidArgument(
10450 "invalid CTAS building-table identity".into(),
10451 ));
10452 }
10453 self.create_table_with_state(
10454 build_name,
10455 schema,
10456 TableState::Building {
10457 intended_name: intended_name.to_string(),
10458 query_id: query_id.to_string(),
10459 created_at_unix_nanos: current_unix_nanos(),
10460 replaces_table_id: None,
10461 },
10462 )
10463 }
10464
10465 #[doc(hidden)]
10468 pub fn create_rebuilding_table(
10469 &self,
10470 build_name: &str,
10471 intended_name: &str,
10472 query_id: &str,
10473 schema: Schema,
10474 ) -> Result<u64> {
10475 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10476 || intended_name.is_empty()
10477 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10478 || query_id.is_empty()
10479 {
10480 return Err(MongrelError::InvalidArgument(
10481 "invalid rebuilding-table identity".into(),
10482 ));
10483 }
10484 let replaces_table_id = self
10485 .catalog
10486 .read()
10487 .live(intended_name)
10488 .ok_or_else(|| MongrelError::NotFound(format!("table {intended_name:?} not found")))?
10489 .table_id;
10490 self.create_table_with_state(
10491 build_name,
10492 schema,
10493 TableState::Building {
10494 intended_name: intended_name.to_string(),
10495 query_id: query_id.to_string(),
10496 created_at_unix_nanos: current_unix_nanos(),
10497 replaces_table_id: Some(replaces_table_id),
10498 },
10499 )
10500 }
10501
10502 fn create_table_with_state(
10503 &self,
10504 name: &str,
10505 schema: Schema,
10506 state: TableState,
10507 ) -> Result<u64> {
10508 use crate::wal::DdlOp;
10509 use std::sync::atomic::Ordering;
10510
10511 self.require(&crate::auth::Permission::Ddl)?;
10512 if self.poisoned.load(Ordering::Relaxed) {
10513 return Err(MongrelError::Other(
10514 "database poisoned by fsync error".into(),
10515 ));
10516 }
10517
10518 let _g = self.ddl_lock.lock();
10519 let _security_write = self.security_write()?;
10520 self.require(&crate::auth::Permission::Ddl)?;
10521 {
10522 let cat = self.catalog.read();
10523 match &state {
10524 TableState::Live => {
10525 if cat.live(name).is_some() || cat.building_for(name).is_some() {
10526 return Err(MongrelError::InvalidArgument(format!(
10527 "table {name:?} already exists or is being built"
10528 )));
10529 }
10530 }
10531 TableState::Building {
10532 intended_name,
10533 replaces_table_id,
10534 ..
10535 } => {
10536 let target_matches = match replaces_table_id {
10537 Some(table_id) => cat
10538 .live(intended_name)
10539 .is_some_and(|entry| entry.table_id == *table_id),
10540 None => cat.live(intended_name).is_none(),
10541 };
10542 if !target_matches || cat.building_for(intended_name).is_some() {
10543 return Err(MongrelError::InvalidArgument(format!(
10544 "table {intended_name:?} changed or is already being built"
10545 )));
10546 }
10547 if cat.building(name).is_some() {
10548 return Err(MongrelError::InvalidArgument(format!(
10549 "building table {name:?} already exists"
10550 )));
10551 }
10552 }
10553 TableState::Dropped { .. } => {
10554 return Err(MongrelError::InvalidArgument(
10555 "cannot create a dropped table".into(),
10556 ));
10557 }
10558 }
10559 }
10560
10561 let commit_lock = Arc::clone(&self.commit_lock);
10564 let _c = commit_lock.lock();
10565 let table_id = {
10566 let mut cat = self.catalog.write();
10567 let id = cat.next_table_id;
10568 cat.next_table_id = id
10569 .checked_add(1)
10570 .ok_or_else(|| MongrelError::InvalidArgument("table id space exhausted".into()))?;
10571 Result::<u64>::Ok(id)
10572 }?;
10573 let epoch = self.epoch.bump_assigned();
10574 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
10575 let txn_id = self.alloc_txn_id()?;
10576
10577 let mut schema = schema;
10581 schema.schema_id = table_id;
10582 schema.validate_auto_increment()?;
10589 schema.validate_defaults()?;
10590 schema.validate_ai()?;
10591 for index in &schema.indexes {
10592 index.validate_options()?;
10593 }
10594 for constraint in &schema.constraints.checks {
10595 constraint.expr.validate()?;
10596 }
10597
10598 let table_relative = Path::new(TABLES_DIR).join(table_id.to_string());
10601 let canonical_tdir = self.root.join(&table_relative);
10602 let table_root = Arc::new(
10603 self.durable_root
10604 .create_directory_all_pinned(&table_relative)?,
10605 );
10606 let tdir = table_root.io_path()?;
10607 let mut pending_table_dir = PendingTableDir::new(canonical_tdir);
10608 let ctx = SharedCtx {
10609 root_guard: Some(table_root),
10610 epoch: Arc::clone(&self.epoch),
10611 page_cache: Arc::clone(&self.page_cache),
10612 decoded_cache: Arc::clone(&self.decoded_cache),
10613 snapshots: Arc::clone(&self.snapshots),
10614 kek: self.kek.clone(),
10615 commit_lock: Arc::clone(&self.commit_lock),
10616 shared: Some(crate::engine::SharedWalCtx {
10617 wal: Arc::clone(&self.shared_wal),
10618 group: Arc::clone(&self.group),
10619 poisoned: Arc::clone(&self.poisoned),
10620 txn_ids: Arc::clone(&self.next_txn_id),
10621 change_wake: self.change_wake.clone(),
10622 }),
10623 table_name: Some(name.to_string()),
10624 auth: self.table_auth_checker(),
10625 read_only: self.read_only,
10626 };
10627 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
10628
10629 let schema_json = DdlOp::encode_schema(&schema)?;
10632 let ddl = match &state {
10633 TableState::Live => DdlOp::CreateTable {
10634 table_id,
10635 name: name.to_string(),
10636 schema_json,
10637 },
10638 TableState::Building {
10639 intended_name,
10640 query_id,
10641 created_at_unix_nanos,
10642 replaces_table_id,
10643 } => match replaces_table_id {
10644 Some(replaces_table_id) => DdlOp::CreateRebuildingTable {
10645 table_id,
10646 build_name: name.to_string(),
10647 intended_name: intended_name.clone(),
10648 query_id: query_id.clone(),
10649 created_at_unix_nanos: *created_at_unix_nanos,
10650 replaces_table_id: *replaces_table_id,
10651 schema_json,
10652 },
10653 None => DdlOp::CreateBuildingTable {
10654 table_id,
10655 build_name: name.to_string(),
10656 intended_name: intended_name.clone(),
10657 query_id: query_id.clone(),
10658 created_at_unix_nanos: *created_at_unix_nanos,
10659 schema_json,
10660 },
10661 },
10662 TableState::Dropped { .. } => {
10663 return Err(MongrelError::InvalidArgument(
10664 "cannot create a table in dropped state".into(),
10665 ));
10666 }
10667 };
10668 let mut next_catalog = self.catalog.read().clone();
10669 next_catalog.tables.push(CatalogEntry {
10670 table_id,
10671 name: name.to_string(),
10672 schema: schema.clone(),
10673 state: state.clone(),
10674 created_epoch: epoch.0,
10675 });
10676 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
10677 let commit_seq = {
10678 let mut wal = self.shared_wal.lock();
10679 let append: Result<u64> = (|| {
10680 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
10681 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
10682 wal.append_commit(txn_id, epoch, &[])
10683 })();
10684 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
10685 };
10686 self.await_durable_commit(commit_seq, epoch)?;
10687 pending_table_dir.disarm();
10688
10689 self.tables
10692 .write()
10693 .insert(table_id, TableHandle::new(table));
10694 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
10695 self.finish_durable_publish(epoch, &mut _epoch_guard, checkpoint)?;
10696 Ok(table_id)
10697 }
10698
10699 pub fn drop_table(&self, name: &str) -> Result<()> {
10701 self.drop_table_with_epoch(name).map(|_| ())
10702 }
10703
10704 pub fn drop_table_with_epoch(&self, name: &str) -> Result<Epoch> {
10706 self.drop_table_with_state(name, false, None)
10707 }
10708
10709 pub fn drop_table_with_epoch_controlled<F>(
10710 &self,
10711 name: &str,
10712 mut before_commit: F,
10713 ) -> Result<Epoch>
10714 where
10715 F: FnMut() -> Result<()>,
10716 {
10717 self.drop_table_with_state(name, false, Some(&mut before_commit))
10718 }
10719
10720 #[doc(hidden)]
10722 pub fn discard_building_table(&self, name: &str) -> Result<()> {
10723 if !name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
10724 return Err(MongrelError::InvalidArgument(
10725 "not a CTAS building table".into(),
10726 ));
10727 }
10728 self.drop_table_with_state(name, true, None).map(|_| ())
10729 }
10730
10731 fn drop_table_with_state(
10732 &self,
10733 name: &str,
10734 building: bool,
10735 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10736 ) -> Result<Epoch> {
10737 use crate::wal::DdlOp;
10738 use std::sync::atomic::Ordering;
10739
10740 self.require(&crate::auth::Permission::Ddl)?;
10741 if self.poisoned.load(Ordering::Relaxed) {
10742 return Err(MongrelError::Other(
10743 "database poisoned by fsync error".into(),
10744 ));
10745 }
10746
10747 let _g = self.ddl_lock.lock();
10748 let _security_write = self.security_write()?;
10749 self.require(&crate::auth::Permission::Ddl)?;
10750 let table_id = {
10751 let cat = self.catalog.read();
10752 if building {
10753 cat.building(name)
10754 } else {
10755 cat.live(name)
10756 }
10757 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
10758 .table_id
10759 };
10760
10761 let commit_lock = Arc::clone(&self.commit_lock);
10762 let _c = commit_lock.lock();
10763 let epoch = self.epoch.bump_assigned();
10764 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
10765 let txn_id = self.alloc_txn_id()?;
10766 let mut next_catalog = self.catalog.read().clone();
10767 let entry = next_catalog
10768 .tables
10769 .iter_mut()
10770 .find(|t| t.table_id == table_id)
10771 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
10772 entry.state = TableState::Dropped { at_epoch: epoch.0 };
10773 next_catalog.triggers.retain(|trigger| {
10774 !matches!(
10775 &trigger.trigger.target,
10776 TriggerTarget::Table(target) if target == name
10777 )
10778 });
10779 next_catalog
10780 .materialized_views
10781 .retain(|definition| definition.name != name);
10782 next_catalog
10783 .security
10784 .rls_tables
10785 .retain(|table| table != name);
10786 next_catalog
10787 .security
10788 .policies
10789 .retain(|policy| policy.table != name);
10790 next_catalog
10791 .security
10792 .masks
10793 .retain(|mask| mask.table != name);
10794 for role in &mut next_catalog.roles {
10795 role.permissions
10796 .retain(|permission| permission_table(permission) != Some(name));
10797 }
10798 advance_security_version(&mut next_catalog)?;
10799 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
10800 let commit_seq = {
10801 let mut wal = self.shared_wal.lock();
10802 if let Some(before_commit) = before_commit {
10803 before_commit()?;
10804 }
10805 let append: Result<u64> = (|| {
10806 wal.append(
10807 txn_id,
10808 table_id,
10809 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
10810 )?;
10811 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
10812 wal.append_commit(txn_id, epoch, &[])
10813 })();
10814 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
10815 };
10816 self.await_durable_commit(commit_seq, epoch)?;
10817
10818 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
10819 self.tables.write().remove(&table_id);
10820 self.finish_durable_publish(epoch, &mut _epoch_guard, checkpoint)?;
10821 Ok(epoch)
10822 }
10823
10824 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
10833 self.rename_table_with_epoch(name, new_name).map(|_| ())
10834 }
10835
10836 pub fn rename_table_with_epoch(&self, name: &str, new_name: &str) -> Result<Epoch> {
10838 self.rename_table_with_epoch_inner(name, new_name, None)
10839 }
10840
10841 pub fn rename_table_with_epoch_controlled<F>(
10842 &self,
10843 name: &str,
10844 new_name: &str,
10845 mut before_commit: F,
10846 ) -> Result<Epoch>
10847 where
10848 F: FnMut() -> Result<()>,
10849 {
10850 self.rename_table_with_epoch_inner(name, new_name, Some(&mut before_commit))
10851 }
10852
10853 fn rename_table_with_epoch_inner(
10854 &self,
10855 name: &str,
10856 new_name: &str,
10857 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10858 ) -> Result<Epoch> {
10859 if name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10860 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10861 {
10862 return Err(MongrelError::InvalidArgument(
10863 "the CTAS building-table namespace is reserved".into(),
10864 ));
10865 }
10866 self.rename_table_with_state(name, new_name, false, None, before_commit)
10867 }
10868
10869 #[doc(hidden)]
10871 pub fn publish_building_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
10872 self.publish_building_table_inner(build_name, new_name, None)
10873 }
10874
10875 #[doc(hidden)]
10876 pub fn publish_building_table_controlled<F>(
10877 &self,
10878 build_name: &str,
10879 new_name: &str,
10880 mut before_commit: F,
10881 ) -> Result<Epoch>
10882 where
10883 F: FnMut() -> Result<()>,
10884 {
10885 self.publish_building_table_inner(build_name, new_name, Some(&mut before_commit))
10886 }
10887
10888 fn publish_building_table_inner(
10889 &self,
10890 build_name: &str,
10891 new_name: &str,
10892 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10893 ) -> Result<Epoch> {
10894 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10895 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10896 {
10897 return Err(MongrelError::InvalidArgument(
10898 "invalid CTAS publish identity".into(),
10899 ));
10900 }
10901 self.rename_table_with_state(build_name, new_name, true, None, before_commit)
10902 }
10903
10904 #[doc(hidden)]
10906 pub fn publish_materialized_building_table(
10907 &self,
10908 build_name: &str,
10909 new_name: &str,
10910 definition: crate::catalog::MaterializedViewEntry,
10911 ) -> Result<Epoch> {
10912 self.publish_materialized_building_table_inner(build_name, new_name, definition, None)
10913 }
10914
10915 #[doc(hidden)]
10916 pub fn publish_materialized_building_table_controlled<F>(
10917 &self,
10918 build_name: &str,
10919 new_name: &str,
10920 definition: crate::catalog::MaterializedViewEntry,
10921 mut before_commit: F,
10922 ) -> Result<Epoch>
10923 where
10924 F: FnMut() -> Result<()>,
10925 {
10926 self.publish_materialized_building_table_inner(
10927 build_name,
10928 new_name,
10929 definition,
10930 Some(&mut before_commit),
10931 )
10932 }
10933
10934 fn publish_materialized_building_table_inner(
10935 &self,
10936 build_name: &str,
10937 new_name: &str,
10938 definition: crate::catalog::MaterializedViewEntry,
10939 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10940 ) -> Result<Epoch> {
10941 if definition.name != new_name || definition.query.trim().is_empty() {
10942 return Err(MongrelError::InvalidArgument(
10943 "invalid materialized-view publication".into(),
10944 ));
10945 }
10946 self.rename_table_with_state(build_name, new_name, true, Some(definition), before_commit)
10947 }
10948
10949 #[doc(hidden)]
10951 pub fn publish_rebuilding_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
10952 self.publish_rebuilding_table_inner(build_name, new_name, None, None)
10953 }
10954
10955 #[doc(hidden)]
10956 pub fn publish_rebuilding_table_controlled<F>(
10957 &self,
10958 build_name: &str,
10959 new_name: &str,
10960 mut before_commit: F,
10961 ) -> Result<Epoch>
10962 where
10963 F: FnMut() -> Result<()>,
10964 {
10965 self.publish_rebuilding_table_inner(build_name, new_name, None, Some(&mut before_commit))
10966 }
10967
10968 #[doc(hidden)]
10970 pub fn publish_materialized_rebuilding_table_controlled<F>(
10971 &self,
10972 build_name: &str,
10973 new_name: &str,
10974 definition: crate::catalog::MaterializedViewEntry,
10975 mut before_commit: F,
10976 ) -> Result<Epoch>
10977 where
10978 F: FnMut() -> Result<()>,
10979 {
10980 self.publish_rebuilding_table_inner(
10981 build_name,
10982 new_name,
10983 Some(definition),
10984 Some(&mut before_commit),
10985 )
10986 }
10987
10988 fn publish_rebuilding_table_inner(
10989 &self,
10990 build_name: &str,
10991 new_name: &str,
10992 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
10993 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10994 ) -> Result<Epoch> {
10995 use crate::wal::DdlOp;
10996
10997 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10998 || new_name.is_empty()
10999 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
11000 {
11001 return Err(MongrelError::InvalidArgument(
11002 "invalid rebuilding-table publish identity".into(),
11003 ));
11004 }
11005 if materialized_view.as_ref().is_some_and(|definition| {
11006 definition.name != new_name || definition.query.trim().is_empty()
11007 }) {
11008 return Err(MongrelError::InvalidArgument(
11009 "invalid materialized-view replacement".into(),
11010 ));
11011 }
11012 self.require(&crate::auth::Permission::Ddl)?;
11013 if self.poisoned.load(Ordering::Relaxed) {
11014 return Err(MongrelError::Other(
11015 "database poisoned by fsync error".into(),
11016 ));
11017 }
11018
11019 let _ddl = self.ddl_lock.lock();
11020 let _security_write = self.security_write()?;
11021 let (table_id, replaced_table_id) = {
11022 let catalog = self.catalog.read();
11023 let build = catalog.building(build_name).ok_or_else(|| {
11024 MongrelError::NotFound(format!("building table {build_name:?} not found"))
11025 })?;
11026 let replaced_table_id = match &build.state {
11027 TableState::Building {
11028 intended_name,
11029 replaces_table_id: Some(replaced_table_id),
11030 ..
11031 } if intended_name == new_name => *replaced_table_id,
11032 _ => {
11033 return Err(MongrelError::InvalidArgument(format!(
11034 "building table {build_name:?} is not a replacement for {new_name:?}"
11035 )))
11036 }
11037 };
11038 if catalog
11039 .live(new_name)
11040 .is_none_or(|entry| entry.table_id != replaced_table_id)
11041 {
11042 return Err(MongrelError::Conflict(format!(
11043 "table {new_name:?} changed while its replacement was built"
11044 )));
11045 }
11046 (build.table_id, replaced_table_id)
11047 };
11048
11049 let _commit = self.commit_lock.lock();
11050 let epoch = self.epoch.assigned().next();
11051 let txn_id = self.alloc_txn_id()?;
11052 let mut next_catalog = self.catalog.read().clone();
11053 apply_rebuilding_publish(
11054 &mut next_catalog,
11055 table_id,
11056 replaced_table_id,
11057 new_name,
11058 epoch.0,
11059 )?;
11060 if let Some(definition) = materialized_view.as_mut() {
11061 definition.last_refresh_epoch = epoch.0;
11062 }
11063 let materialized_view_json = materialized_view
11064 .as_ref()
11065 .map(DdlOp::encode_materialized_view)
11066 .transpose()?;
11067 if let Some(definition) = materialized_view {
11068 if let Some(existing) = next_catalog
11069 .materialized_views
11070 .iter_mut()
11071 .find(|existing| existing.name == definition.name)
11072 {
11073 *existing = definition;
11074 } else {
11075 next_catalog.materialized_views.push(definition);
11076 }
11077 }
11078 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11079 if let Some(before_commit) = before_commit {
11080 before_commit()?;
11081 }
11082 let assigned_epoch = self.epoch.bump_assigned();
11083 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
11084 if assigned_epoch != epoch {
11085 return Err(MongrelError::Conflict(
11086 "commit epoch changed while sequencer lock was held".into(),
11087 ));
11088 }
11089 let commit_seq = {
11090 let mut wal = self.shared_wal.lock();
11091 let append: Result<u64> = (|| {
11092 wal.append(
11093 txn_id,
11094 table_id,
11095 crate::wal::Op::Ddl(DdlOp::ReplaceBuildingTable {
11096 table_id,
11097 replaced_table_id,
11098 new_name: new_name.to_string(),
11099 }),
11100 )?;
11101 if let Some(definition_json) = materialized_view_json {
11102 wal.append(
11103 txn_id,
11104 table_id,
11105 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
11106 name: new_name.to_string(),
11107 definition_json,
11108 }),
11109 )?;
11110 }
11111 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11112 wal.append_commit(txn_id, epoch, &[])
11113 })();
11114 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11115 };
11116 self.await_durable_commit(commit_seq, epoch)?;
11117
11118 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
11119 self.tables.write().remove(&replaced_table_id);
11120 if let Some(table) = self.tables.read().get(&table_id) {
11121 table.lock().set_catalog_name(new_name.to_string());
11122 }
11123 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
11124 Ok(epoch)
11125 }
11126
11127 fn rename_table_with_state(
11128 &self,
11129 name: &str,
11130 new_name: &str,
11131 building: bool,
11132 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
11133 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11134 ) -> Result<Epoch> {
11135 use crate::wal::DdlOp;
11136 use std::sync::atomic::Ordering;
11137
11138 self.require(&crate::auth::Permission::Ddl)?;
11139 if self.poisoned.load(Ordering::Relaxed) {
11140 return Err(MongrelError::Other(
11141 "database poisoned by fsync error".into(),
11142 ));
11143 }
11144
11145 if name == new_name {
11148 return Ok(self.visible_epoch());
11149 }
11150 if new_name.is_empty() {
11151 return Err(MongrelError::InvalidArgument(
11152 "rename_table: new name must not be empty".into(),
11153 ));
11154 }
11155
11156 let _g = self.ddl_lock.lock();
11157 let _security_write = self.security_write()?;
11158 self.require(&crate::auth::Permission::Ddl)?;
11159 let table_id = {
11160 let cat = self.catalog.read();
11161 let src = if building {
11162 cat.building(name)
11163 } else {
11164 cat.live(name)
11165 }
11166 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
11167 if building
11168 && !matches!(
11169 &src.state,
11170 TableState::Building { intended_name, .. } if intended_name == new_name
11171 )
11172 {
11173 return Err(MongrelError::InvalidArgument(format!(
11174 "building table {name:?} is not reserved for {new_name:?}"
11175 )));
11176 }
11177 if cat.live(new_name).is_some() || (!building && cat.building_for(new_name).is_some()) {
11181 return Err(MongrelError::InvalidArgument(format!(
11182 "rename_table: a table named {new_name:?} already exists"
11183 )));
11184 }
11185 src.table_id
11186 };
11187
11188 let commit_lock = Arc::clone(&self.commit_lock);
11189 let _c = commit_lock.lock();
11190 let epoch = self.epoch.bump_assigned();
11191 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
11192 let txn_id = self.alloc_txn_id()?;
11193 if let Some(definition) = materialized_view.as_mut() {
11194 definition.last_refresh_epoch = epoch.0;
11195 }
11196 let materialized_view_json = materialized_view
11197 .as_ref()
11198 .map(DdlOp::encode_materialized_view)
11199 .transpose()?;
11200 let mut next_catalog = self.catalog.read().clone();
11201 let entry = next_catalog
11202 .tables
11203 .iter_mut()
11204 .find(|t| t.table_id == table_id)
11205 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
11206 entry.name = new_name.to_string();
11207 if building {
11208 entry.state = TableState::Live;
11209 }
11210 for trigger in &mut next_catalog.triggers {
11211 if matches!(
11212 &trigger.trigger.target,
11213 TriggerTarget::Table(target) if target == name
11214 ) {
11215 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
11216 }
11217 }
11218 if let Some(definition) = next_catalog
11219 .materialized_views
11220 .iter_mut()
11221 .find(|definition| definition.name == name)
11222 {
11223 definition.name = new_name.to_string();
11224 }
11225 if let Some(definition) = materialized_view.take() {
11226 next_catalog.materialized_views.push(definition);
11227 }
11228 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11229 for table in &mut next_catalog.security.rls_tables {
11230 if table == name {
11231 *table = new_name.to_string();
11232 }
11233 }
11234 for policy in &mut next_catalog.security.policies {
11235 if policy.table == name {
11236 policy.table = new_name.to_string();
11237 }
11238 }
11239 for mask in &mut next_catalog.security.masks {
11240 if mask.table == name {
11241 mask.table = new_name.to_string();
11242 }
11243 }
11244 for role in &mut next_catalog.roles {
11245 for permission in &mut role.permissions {
11246 rename_permission_table(permission, name, new_name);
11247 }
11248 }
11249 advance_security_version(&mut next_catalog)?;
11250 let ddl = if building {
11251 DdlOp::PublishBuildingTable {
11252 table_id,
11253 new_name: new_name.to_string(),
11254 }
11255 } else {
11256 DdlOp::RenameTable {
11257 table_id,
11258 new_name: new_name.to_string(),
11259 }
11260 };
11261 let commit_seq = {
11262 let mut wal = self.shared_wal.lock();
11263 if let Some(before_commit) = before_commit {
11264 before_commit()?;
11265 }
11266 let append: Result<u64> = (|| {
11267 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
11268 if let Some(definition_json) = materialized_view_json {
11269 wal.append(
11270 txn_id,
11271 table_id,
11272 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
11273 name: new_name.to_string(),
11274 definition_json,
11275 }),
11276 )?;
11277 }
11278 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11279 wal.append_commit(txn_id, epoch, &[])
11280 })();
11281 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11282 };
11283 self.await_durable_commit(commit_seq, epoch)?;
11284
11285 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
11286 if let Some(table) = self.tables.read().get(&table_id) {
11289 table.lock().set_catalog_name(new_name.to_string());
11290 }
11291 self.finish_durable_publish(epoch, &mut _epoch_guard, checkpoint)?;
11292 Ok(epoch)
11293 }
11294
11295 pub fn alter_column(
11296 &self,
11297 table_name: &str,
11298 column_name: &str,
11299 change: AlterColumn,
11300 ) -> Result<ColumnDef> {
11301 self.alter_column_with_epoch(table_name, column_name, change)
11302 .map(|(column, _)| column)
11303 }
11304
11305 pub fn alter_column_with_epoch(
11306 &self,
11307 table_name: &str,
11308 column_name: &str,
11309 change: AlterColumn,
11310 ) -> Result<(ColumnDef, Option<Epoch>)> {
11311 self.alter_column_with_epoch_inner(table_name, column_name, change, None, None, None)
11312 }
11313
11314 pub fn alter_column_with_epoch_controlled<B, A>(
11319 &self,
11320 table_name: &str,
11321 column_name: &str,
11322 change: AlterColumn,
11323 control: &crate::ExecutionControl,
11324 mut before_commit: B,
11325 mut after_commit: A,
11326 ) -> Result<(ColumnDef, Option<Epoch>)>
11327 where
11328 B: FnMut() -> Result<()>,
11329 A: FnMut(Option<Epoch>) -> Result<()>,
11330 {
11331 self.alter_column_with_epoch_inner(
11332 table_name,
11333 column_name,
11334 change,
11335 Some(control),
11336 Some(&mut before_commit),
11337 Some(&mut after_commit),
11338 )
11339 }
11340
11341 #[allow(clippy::too_many_arguments)]
11342 fn alter_column_with_epoch_inner(
11343 &self,
11344 table_name: &str,
11345 column_name: &str,
11346 change: AlterColumn,
11347 control: Option<&crate::ExecutionControl>,
11348 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11349 mut after_commit: Option<&mut dyn FnMut(Option<Epoch>) -> Result<()>>,
11350 ) -> Result<(ColumnDef, Option<Epoch>)> {
11351 use crate::wal::DdlOp;
11352 use std::sync::atomic::Ordering;
11353
11354 self.require(&crate::auth::Permission::Ddl)?;
11355 commit_prepare_checkpoint(control, 0)?;
11356 if self.poisoned.load(Ordering::Relaxed) {
11357 return Err(MongrelError::Other(
11358 "database poisoned by fsync error".into(),
11359 ));
11360 }
11361
11362 let _g = self.ddl_lock.lock();
11363 let table_id = {
11364 let cat = self.catalog.read();
11365 cat.live(table_name)
11366 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
11367 .table_id
11368 };
11369 let handle =
11370 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
11371 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
11372 })?;
11373
11374 let backfill = {
11380 let table = handle.lock();
11381 let old = table
11382 .schema()
11383 .column(column_name)
11384 .cloned()
11385 .ok_or_else(|| MongrelError::Schema(format!("unknown column {column_name}")))?;
11386 let next_flags = change.flags.unwrap_or(old.flags);
11387 if old.flags.contains(crate::schema::ColumnFlags::NULLABLE)
11388 && !next_flags.contains(crate::schema::ColumnFlags::NULLABLE)
11389 && old.default_value.is_some()
11390 {
11391 let snapshot = Snapshot::at(self.epoch.visible());
11392 let mut updates = Vec::new();
11393 let rows = match control {
11394 Some(control) => table.visible_rows_controlled(snapshot, control)?,
11395 None => table.visible_rows(snapshot)?,
11396 };
11397 for (row_index, row) in rows.into_iter().enumerate() {
11398 commit_prepare_checkpoint(control, row_index)?;
11399 if row
11400 .columns
11401 .get(&old.id)
11402 .is_some_and(|value| !matches!(value, Value::Null))
11403 {
11404 continue;
11405 }
11406 let mut cells: Vec<(u16, Value)> = row.columns.into_iter().collect();
11407 table.apply_defaults(&mut cells)?;
11408 updates.push((
11409 table_id,
11410 crate::txn::Staged::Update {
11411 row_id: row.row_id,
11412 new_row: cells,
11413 changed_columns: vec![old.id],
11414 },
11415 ));
11416 }
11417 updates
11418 } else {
11419 Vec::new()
11420 }
11421 };
11422 let durable_epoch = std::cell::Cell::new(None);
11423 let backfill_epoch = if backfill.is_empty() {
11424 None
11425 } else {
11426 let (principal, catalog_bound) = self.transaction_principal_snapshot();
11427 let txn_id = self.alloc_txn_id()?;
11428 let mut entered_fence = false;
11429 let commit_result = match (control, before_commit.as_deref_mut()) {
11430 (Some(control), Some(before_commit)) => self
11431 .commit_transaction_with_external_states_controlled(
11432 txn_id,
11433 self.epoch.visible(),
11434 backfill,
11435 Vec::new(),
11436 Vec::new(),
11437 principal.clone(),
11438 catalog_bound,
11439 None,
11440 control,
11441 &mut || {
11442 before_commit()?;
11443 entered_fence = true;
11444 Ok(())
11445 },
11446 )
11447 .map(|(epoch, _)| epoch),
11448 _ => self
11449 .commit_transaction_with_external_states(
11450 txn_id,
11451 self.epoch.visible(),
11452 backfill,
11453 Vec::new(),
11454 Vec::new(),
11455 principal,
11456 catalog_bound,
11457 None,
11458 )
11459 .map(|(epoch, _)| epoch),
11460 };
11461 let commit_result = if entered_fence {
11462 finish_controlled_commit_attempt(commit_result, &mut after_commit)
11463 } else {
11464 commit_result
11465 };
11466 match &commit_result {
11467 Ok(epoch) => durable_epoch.set(Some(*epoch)),
11468 Err(MongrelError::DurableCommit { epoch, .. }) => {
11469 durable_epoch.set(Some(Epoch(*epoch)));
11470 }
11471 Err(_) => {}
11472 }
11473 Some(commit_result?)
11474 };
11475 let result: Result<(ColumnDef, Option<Epoch>)> = (|| {
11476 let _security_write = self.security_write()?;
11477 self.require(&crate::auth::Permission::Ddl)?;
11478 if self
11479 .catalog
11480 .read()
11481 .live(table_name)
11482 .is_none_or(|entry| entry.table_id != table_id)
11483 {
11484 return Err(MongrelError::Conflict(format!(
11485 "table {table_name:?} changed during ALTER"
11486 )));
11487 }
11488 let mut table = handle.lock();
11489 let (column, prepared_schema) = table.prepare_alter_column(column_name, &change)?;
11490 let renamed_column = (column.name != column_name).then(|| column.name.clone());
11491 let Some(prepared_schema) = prepared_schema else {
11492 return Ok((column, backfill_epoch));
11493 };
11494
11495 let commit_lock = Arc::clone(&self.commit_lock);
11496 let _c = commit_lock.lock();
11497 let epoch = self.epoch.bump_assigned();
11498 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
11499 let txn_id = self.alloc_txn_id()?;
11500 let column_json = DdlOp::encode_column(&column)?;
11501 let mut next_catalog = self.catalog.read().clone();
11502 let catalog_entry_index = next_catalog
11503 .tables
11504 .iter()
11505 .position(|entry| entry.table_id == table_id)
11506 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
11507 if let Some(new_column_name) = &renamed_column {
11508 for (trigger_index, trigger) in next_catalog.triggers.iter_mut().enumerate() {
11509 commit_prepare_checkpoint(control, trigger_index)?;
11510 if matches!(
11511 &trigger.trigger.target,
11512 TriggerTarget::Table(target) if target == table_name
11513 ) {
11514 trigger.trigger = trigger.trigger.renamed_update_column(
11515 column_name,
11516 new_column_name.clone(),
11517 epoch.0,
11518 )?;
11519 }
11520 }
11521 for (role_index, role) in next_catalog.roles.iter_mut().enumerate() {
11522 commit_prepare_checkpoint(control, role_index)?;
11523 for (permission_index, permission) in role.permissions.iter_mut().enumerate() {
11524 commit_prepare_checkpoint(control, permission_index)?;
11525 rename_permission_column(
11526 permission,
11527 table_name,
11528 column_name,
11529 new_column_name,
11530 );
11531 }
11532 }
11533 advance_security_version(&mut next_catalog)?;
11534 }
11535 next_catalog.tables[catalog_entry_index].schema = prepared_schema.clone();
11536 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11537 commit_prepare_checkpoint(control, 0)?;
11538 let mut entered_fence = false;
11539 if let Some(before_commit) = before_commit.as_deref_mut() {
11540 before_commit()?;
11541 entered_fence = true;
11542 }
11543 let commit_result: Result<Epoch> = (|| {
11544 let commit_seq = {
11545 let mut wal = self.shared_wal.lock();
11546 let append: Result<u64> = (|| {
11547 wal.append(
11548 txn_id,
11549 table_id,
11550 crate::wal::Op::Ddl(DdlOp::AlterTable {
11551 table_id,
11552 column_json,
11553 }),
11554 )?;
11555 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11556 wal.append_commit(txn_id, epoch, &[])
11557 })();
11558 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11559 };
11560 self.await_durable_commit(commit_seq, epoch)?;
11561 durable_epoch.set(Some(epoch));
11562
11563 table.apply_altered_schema_prepared(prepared_schema);
11564 let schema = table.schema().clone();
11565 let table_checkpoint = table.checkpoint_altered_schema();
11566 drop(table);
11567 next_catalog.tables[catalog_entry_index].schema = schema;
11568 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11569 let catalog_result =
11570 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref());
11571 let security_version = next_catalog.security_version;
11572 *self.catalog.write() = next_catalog;
11573 if renamed_column.is_some() {
11574 self.security_coordinator
11575 .version
11576 .store(security_version, Ordering::Release);
11577 }
11578 self.epoch.publish_in_order(epoch);
11579 _epoch_guard.disarm();
11580 if let Err(error) = table_checkpoint.and(catalog_result) {
11581 self.poisoned.store(true, Ordering::Relaxed);
11582 return Err(MongrelError::DurableCommit {
11583 epoch: epoch.0,
11584 message: error.to_string(),
11585 });
11586 }
11587 Ok(epoch)
11588 })();
11589 let commit_result = if entered_fence {
11590 finish_controlled_commit_attempt(commit_result, &mut after_commit)
11591 } else {
11592 commit_result
11593 };
11594 let epoch = commit_result?;
11595 Ok((column, Some(epoch)))
11596 })();
11597 result.map_err(|error| match (durable_epoch.get(), error) {
11598 (_, error @ MongrelError::DurableCommit { .. }) => error,
11599 (Some(epoch), error) => MongrelError::DurableCommit {
11600 epoch: epoch.0,
11601 message: error.to_string(),
11602 },
11603 (None, error) => error,
11604 })
11605 }
11606
11607 pub fn set_table_ttl(
11610 &self,
11611 table_name: &str,
11612 column_name: &str,
11613 duration_nanos: u64,
11614 ) -> Result<crate::manifest::TtlPolicy> {
11615 let policy = self.replace_table_ttl(table_name, Some((column_name, duration_nanos)))?;
11616 policy.ok_or_else(|| MongrelError::Other("set TTL produced no policy".into()))
11617 }
11618
11619 #[doc(hidden)]
11621 pub fn set_building_table_ttl(
11622 &self,
11623 table_name: &str,
11624 column_name: &str,
11625 duration_nanos: u64,
11626 ) -> Result<crate::manifest::TtlPolicy> {
11627 let policy = self.replace_table_ttl_with_state(
11628 table_name,
11629 Some((column_name, duration_nanos)),
11630 true,
11631 )?;
11632 policy
11633 .ok_or_else(|| MongrelError::Other("set building-table TTL produced no policy".into()))
11634 }
11635
11636 pub fn clear_table_ttl(&self, table_name: &str) -> Result<()> {
11637 self.replace_table_ttl(table_name, None)?;
11638 Ok(())
11639 }
11640
11641 fn replace_table_ttl(
11642 &self,
11643 table_name: &str,
11644 requested: Option<(&str, u64)>,
11645 ) -> Result<Option<crate::manifest::TtlPolicy>> {
11646 self.replace_table_ttl_with_state(table_name, requested, false)
11647 }
11648
11649 fn replace_table_ttl_with_state(
11650 &self,
11651 table_name: &str,
11652 requested: Option<(&str, u64)>,
11653 building: bool,
11654 ) -> Result<Option<crate::manifest::TtlPolicy>> {
11655 use crate::wal::DdlOp;
11656 use std::sync::atomic::Ordering;
11657
11658 self.require(&crate::auth::Permission::Ddl)?;
11659 if self.poisoned.load(Ordering::Relaxed) {
11660 return Err(MongrelError::Other(
11661 "database poisoned by fsync error".into(),
11662 ));
11663 }
11664
11665 let _g = self.ddl_lock.lock();
11666 let _security_write = self.security_write()?;
11667 self.require(&crate::auth::Permission::Ddl)?;
11668 let table_id = {
11669 let cat = self.catalog.read();
11670 if building {
11671 cat.building(table_name)
11672 } else {
11673 cat.live(table_name)
11674 }
11675 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
11676 .table_id
11677 };
11678 let handle =
11679 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
11680 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
11681 })?;
11682 let mut table = handle.lock();
11683 let policy = match requested {
11684 Some((column, duration)) => Some(table.prepare_ttl_policy(column, duration)?),
11685 None => None,
11686 };
11687 if table.ttl() == policy {
11688 return Ok(policy);
11689 }
11690
11691 let commit_lock = Arc::clone(&self.commit_lock);
11692 let _c = commit_lock.lock();
11693 let epoch = self.epoch.bump_assigned();
11694 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
11695 let txn_id = self.alloc_txn_id()?;
11696 let policy_json = DdlOp::encode_ttl(policy)?;
11697 let mut next_catalog = self.catalog.read().clone();
11698 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11699 let commit_seq = {
11700 let mut wal = self.shared_wal.lock();
11701 let append: Result<u64> = (|| {
11702 wal.append(
11703 txn_id,
11704 table_id,
11705 crate::wal::Op::Ddl(DdlOp::SetTtl {
11706 table_id,
11707 policy_json,
11708 }),
11709 )?;
11710 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11711 wal.append_commit(txn_id, epoch, &[])
11712 })();
11713 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11714 };
11715 self.await_durable_commit(commit_seq, epoch)?;
11716
11717 let mut publish_error = table.apply_ttl_policy_at(policy, epoch).err();
11718 drop(table);
11719 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
11720 publish_error.get_or_insert(error);
11721 }
11722 self.finish_durable_publish(epoch, &mut _epoch_guard, publish_error.map_or(Ok(()), Err))?;
11723 Ok(policy)
11724 }
11725
11726 pub fn gc(&self) -> Result<usize> {
11732 let control = crate::ExecutionControl::new(None);
11733 self.gc_controlled(&control, || true)
11734 }
11735
11736 #[doc(hidden)]
11739 pub fn gc_controlled<F>(
11740 &self,
11741 control: &crate::ExecutionControl,
11742 before_publish: F,
11743 ) -> Result<usize>
11744 where
11745 F: FnOnce() -> bool,
11746 {
11747 self.gc_controlled_with_receipt(control, before_publish)
11748 .map(|(reclaimed, _)| reclaimed)
11749 }
11750
11751 #[doc(hidden)]
11754 pub fn gc_controlled_with_receipt<F>(
11755 &self,
11756 control: &crate::ExecutionControl,
11757 before_publish: F,
11758 ) -> Result<(usize, Option<MaintenanceReceipt>)>
11759 where
11760 F: FnOnce() -> bool,
11761 {
11762 enum Candidate {
11763 Directory(PathBuf),
11764 File(PathBuf),
11765 }
11766
11767 self.require(&crate::auth::Permission::Ddl)?;
11768 let _ddl = self.ddl_lock.lock();
11769 self.require(&crate::auth::Permission::Ddl)?;
11770 control.checkpoint()?;
11771 let maintenance_epoch = self.epoch.visible();
11772 let min_active = self.snapshots.min_active(maintenance_epoch).0;
11773 let mut candidates = Vec::new();
11774
11775 let cat = self.catalog.read();
11777 for (entry_index, entry) in cat.tables.iter().enumerate() {
11778 if entry_index % 256 == 0 {
11779 control.checkpoint()?;
11780 }
11781 if let TableState::Dropped { at_epoch } = entry.state {
11782 if at_epoch <= min_active {
11783 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
11784 if tdir.exists() {
11785 candidates.push(Candidate::Directory(tdir));
11786 }
11787 }
11788 }
11789 }
11790 drop(cat);
11791
11792 let cat = self.catalog.read();
11797 for (entry_index, entry) in cat.tables.iter().enumerate() {
11798 if entry_index % 256 == 0 {
11799 control.checkpoint()?;
11800 }
11801 if !matches!(entry.state, TableState::Live) {
11802 continue;
11803 }
11804 let txn_dir = self
11805 .root
11806 .join(TABLES_DIR)
11807 .join(entry.table_id.to_string())
11808 .join("_txn");
11809 if !txn_dir.exists() {
11810 continue;
11811 }
11812 for (sub_index, sub) in std::fs::read_dir(&txn_dir)?.enumerate() {
11813 if sub_index % 256 == 0 {
11814 control.checkpoint()?;
11815 }
11816 let sub = sub?;
11817 let name = sub.file_name();
11818 let Some(name) = name.to_str() else { continue };
11819 let is_active = name
11821 .parse::<u64>()
11822 .map(|id| self.active_spills.is_active(id))
11823 .unwrap_or(false);
11824 if is_active {
11825 continue;
11826 }
11827 candidates.push(Candidate::Directory(sub.path()));
11828 }
11829 }
11830 drop(cat);
11831
11832 let external_names = {
11833 let cat = self.catalog.read();
11834 cat.external_tables
11835 .iter()
11836 .map(|entry| entry.name.clone())
11837 .collect::<std::collections::HashSet<_>>()
11838 };
11839 let vtab_dir = self.root.join(VTAB_DIR);
11840 if vtab_dir.exists() {
11841 for (entry_index, entry) in std::fs::read_dir(&vtab_dir)?.enumerate() {
11842 if entry_index % 256 == 0 {
11843 control.checkpoint()?;
11844 }
11845 let entry = entry?;
11846 let name = entry.file_name();
11847 let Some(name) = name.to_str() else { continue };
11848 if external_names.contains(name) {
11849 continue;
11850 }
11851 let path = entry.path();
11852 if path.is_dir() {
11853 candidates.push(Candidate::Directory(path));
11854 } else {
11855 candidates.push(Candidate::File(path));
11856 }
11857 }
11858 }
11859
11860 let tables = self
11864 .tables
11865 .read()
11866 .iter()
11867 .map(|(table_id, handle)| (*table_id, handle.clone()))
11868 .collect::<Vec<_>>();
11869 let mut retiring = Vec::new();
11870 for (table_index, (table_id, handle)) in tables.iter().enumerate() {
11871 if table_index % 256 == 0 {
11872 control.checkpoint()?;
11873 }
11874 let backup_pinned: HashSet<u128> = self
11875 .backup_pins
11876 .lock()
11877 .keys()
11878 .filter_map(|(pinned_table, run_id)| {
11879 (*pinned_table == *table_id).then_some(*run_id)
11880 })
11881 .collect();
11882 if handle
11883 .lock()
11884 .has_reapable_retiring(Epoch(min_active), &backup_pinned)
11885 {
11886 retiring.push((handle.clone(), backup_pinned));
11887 }
11888 }
11889
11890 let all_durable = self.active_spills.is_idle()
11898 && tables.iter().all(|(_, handle)| {
11899 let g = handle.lock();
11900 g.memtable_len() == 0 && g.mutable_run_len() == 0
11901 });
11902 let retain = self
11903 .replication_wal_retention_segments
11904 .load(std::sync::atomic::Ordering::Relaxed);
11905 let reap_wal = all_durable
11906 && self
11907 .shared_wal
11908 .lock()
11909 .has_gc_segments_retain_recent(retain)?;
11910
11911 if candidates.is_empty() && retiring.is_empty() && !reap_wal {
11912 return Ok((0, None));
11913 }
11914 control.checkpoint()?;
11915 if !before_publish() {
11916 return Err(MongrelError::Cancelled);
11917 }
11918
11919 let mut reclaimed = 0;
11920 for candidate in candidates {
11921 match candidate {
11922 Candidate::Directory(path) => std::fs::remove_dir_all(path)?,
11923 Candidate::File(path) => std::fs::remove_file(path)?,
11924 }
11925 reclaimed += 1;
11926 }
11927 for (handle, backup_pinned) in retiring {
11928 reclaimed += handle
11929 .lock()
11930 .reap_retiring(Epoch(min_active), &backup_pinned)?;
11931 }
11932 if reap_wal {
11933 reclaimed += self
11934 .shared_wal
11935 .lock()
11936 .gc_segments_retain_recent(u64::MAX, retain)?;
11937 }
11938
11939 Ok((
11940 reclaimed,
11941 Some(MaintenanceReceipt {
11942 epoch: maintenance_epoch,
11943 }),
11944 ))
11945 }
11946
11947 pub fn checkpoint(&self) -> Result<()> {
11965 self.checkpoint_controlled(|| Ok(()))
11966 }
11967
11968 #[doc(hidden)]
11971 pub fn checkpoint_controlled<F>(&self, before_wal_reset: F) -> Result<()>
11972 where
11973 F: FnOnce() -> Result<()>,
11974 {
11975 self.require(&crate::auth::Permission::Ddl)?;
11976 let _replication = self.replication_barrier.write();
11980 let _ddl = self.ddl_lock.lock();
11981 let _security = self.security_coordinator.gate.read();
11982 self.require(&crate::auth::Permission::Ddl)?;
11983
11984 let mut handles = self
11985 .tables
11986 .read()
11987 .iter()
11988 .map(|(table_id, handle)| (*table_id, handle.clone()))
11989 .collect::<Vec<_>>();
11990 handles.sort_by_key(|(table_id, _)| *table_id);
11991 let mut tables = handles
11992 .iter()
11993 .map(|(table_id, handle)| (*table_id, handle.lock()))
11994 .collect::<Vec<_>>();
11995
11996 for (_, table) in &mut tables {
11998 if table.has_pending_writes() || table.memtable_len() > 0 || table.mutable_run_len() > 0
11999 {
12000 table.force_flush()?;
12001 }
12002 }
12003
12004 for (_, table) in &mut tables {
12007 if table.run_count() >= 2 || table.should_compact() {
12008 table.compact()?;
12009 }
12010 }
12011
12012 before_wal_reset()?;
12013
12014 let maintenance_epoch = self.epoch.visible();
12016 let min_active = self.snapshots.min_active(maintenance_epoch);
12017 for (table_id, table) in &mut tables {
12018 let backup_pinned: HashSet<u128> = self
12019 .backup_pins
12020 .lock()
12021 .keys()
12022 .filter_map(|(pinned_table, run_id)| {
12023 (*pinned_table == *table_id).then_some(*run_id)
12024 })
12025 .collect();
12026 table.reap_retiring(min_active, &backup_pinned)?;
12027 }
12028
12029 self.shared_wal.lock().reset_after_checkpoint()?;
12032
12033 let catalog_snapshot = self.catalog.read().clone();
12035 for entry in &catalog_snapshot.tables {
12036 if matches!(entry.state, TableState::Dropped { at_epoch } if at_epoch <= min_active.0) {
12037 crate::durable_file::remove_directory_all(
12038 &self.root.join(TABLES_DIR).join(entry.table_id.to_string()),
12039 )?;
12040 }
12041 if !matches!(entry.state, TableState::Live) {
12042 continue;
12043 }
12044 let transaction_dir = self
12045 .root
12046 .join(TABLES_DIR)
12047 .join(entry.table_id.to_string())
12048 .join("_txn");
12049 if transaction_dir.is_dir() {
12050 for child in std::fs::read_dir(&transaction_dir)? {
12051 let child = child?;
12052 let active = child
12053 .file_name()
12054 .to_str()
12055 .and_then(|name| name.parse::<u64>().ok())
12056 .is_some_and(|txn_id| self.active_spills.is_active(txn_id));
12057 if !active {
12058 crate::durable_file::remove_directory_all(&child.path())?;
12059 }
12060 }
12061 }
12062 }
12063 let external_names = catalog_snapshot
12064 .external_tables
12065 .iter()
12066 .map(|entry| entry.name.as_str())
12067 .collect::<HashSet<_>>();
12068 let external_root = self.root.join(VTAB_DIR);
12069 if external_root.is_dir() {
12070 for entry in std::fs::read_dir(&external_root)? {
12071 let entry = entry?;
12072 let name = entry.file_name();
12073 if name
12074 .to_str()
12075 .is_some_and(|name| external_names.contains(name))
12076 {
12077 continue;
12078 }
12079 if entry.file_type()?.is_dir() {
12080 crate::durable_file::remove_directory_all(&entry.path())?;
12081 } else {
12082 std::fs::remove_file(entry.path())?;
12083 crate::durable_file::sync_directory(&external_root)?;
12084 }
12085 }
12086 }
12087
12088 catalog::write_atomic(&self.root, &catalog_snapshot, self.meta_dek.as_ref())?;
12091 let visible = self.epoch.visible();
12092 for (_, table) in &tables {
12093 table.persist_manifest(visible)?;
12094 }
12095
12096 Ok(())
12097 }
12098 fn alloc_txn_id(&self) -> Result<u64> {
12099 crate::txn::allocate_txn_id(&self.next_txn_id)
12100 }
12101
12102 pub fn set_spill_threshold(&self, bytes: u64) {
12106 self.spill_threshold
12107 .store(bytes, std::sync::atomic::Ordering::Relaxed);
12108 }
12109
12110 #[doc(hidden)]
12114 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12115 *self.spill_hook.lock() = Some(Box::new(f));
12116 }
12117
12118 #[doc(hidden)]
12121 pub fn __set_security_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12122 *self.security_commit_hook.lock() = Some(Box::new(f));
12123 }
12124
12125 #[doc(hidden)]
12128 pub fn __set_catalog_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12129 *self.catalog_commit_hook.lock() = Some(Box::new(f));
12130 }
12131
12132 #[doc(hidden)]
12135 pub fn __set_backup_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12136 *self.backup_hook.lock() = Some(Box::new(f));
12137 }
12138
12139 #[doc(hidden)]
12141 pub fn __set_replication_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12142 *self.replication_hook.lock() = Some(Box::new(f));
12143 }
12144
12145 #[doc(hidden)]
12149 pub fn __wal_group_sync_count(&self) -> u64 {
12150 self.shared_wal.lock().group_sync_count()
12151 }
12152
12153 #[doc(hidden)]
12156 pub fn __poison(&self) {
12157 self.poisoned
12158 .store(true, std::sync::atomic::Ordering::Relaxed);
12159 }
12160
12161 pub fn check(&self) -> Vec<CheckIssue> {
12174 match self.check_inner(None) {
12175 Ok(issues) => issues,
12176 Err(error) => vec![CheckIssue {
12177 table_id: WAL_TABLE_ID,
12178 table_name: "shared WAL".into(),
12179 severity: "error".into(),
12180 description: error.to_string(),
12181 }],
12182 }
12183 }
12184
12185 #[doc(hidden)]
12187 pub fn check_controlled(&self, control: &crate::ExecutionControl) -> Result<Vec<CheckIssue>> {
12188 self.check_inner(Some(control))
12189 }
12190
12191 fn check_inner(&self, control: Option<&crate::ExecutionControl>) -> Result<Vec<CheckIssue>> {
12192 let mut issues = Vec::new();
12193 let cat = self.catalog.read();
12194 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
12195 for (table_index, entry) in cat.tables.iter().enumerate() {
12196 if table_index % 256 == 0 {
12197 if let Some(control) = control {
12198 control.checkpoint()?;
12199 }
12200 }
12201 if !matches!(entry.state, TableState::Live) {
12202 continue;
12203 }
12204 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
12205 let mut err = |sev: &str, desc: String| {
12206 issues.push(CheckIssue {
12207 table_id: entry.table_id,
12208 table_name: entry.name.clone(),
12209 severity: sev.into(),
12210 description: desc,
12211 });
12212 };
12213 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
12214 Ok(m) => m,
12215 Err(e) => {
12216 err("error", format!("manifest read failed: {e}"));
12217 continue;
12218 }
12219 };
12220 if m.flushed_epoch > m.current_epoch {
12221 err(
12222 "error",
12223 format!(
12224 "flushed_epoch {} exceeds current_epoch {} (impossible)",
12225 m.flushed_epoch, m.current_epoch
12226 ),
12227 );
12228 }
12229
12230 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
12231 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
12232 for (run_index, rr) in m.runs.iter().enumerate() {
12233 if run_index % 256 == 0 {
12234 if let Some(control) = control {
12235 control.checkpoint()?;
12236 }
12237 }
12238 referenced.insert(rr.run_id);
12239 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
12240 if !run_path.exists() {
12241 err("error", format!("missing run file: r-{}.sr", rr.run_id));
12242 continue;
12243 }
12244 match crate::sorted_run::RunReader::open(
12245 &run_path,
12246 entry.schema.clone(),
12247 self.kek.clone(),
12248 ) {
12249 Ok(reader) => {
12250 if reader.row_count() as u64 != rr.row_count {
12251 err(
12252 "error",
12253 format!(
12254 "run r-{} row count mismatch: manifest {} vs run {}",
12255 rr.run_id,
12256 rr.row_count,
12257 reader.row_count()
12258 ),
12259 );
12260 }
12261 }
12262 Err(e) => {
12263 err(
12264 "error",
12265 format!("run r-{} integrity check failed: {e}", rr.run_id),
12266 );
12267 }
12268 }
12269 }
12270
12271 for r in &m.retiring {
12275 referenced.insert(r.run_id);
12276 }
12277
12278 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
12280 for (entry_index, ent) in rd.flatten().enumerate() {
12281 if entry_index % 256 == 0 {
12282 if let Some(control) = control {
12283 control.checkpoint()?;
12284 }
12285 }
12286 let p = ent.path();
12287 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
12288 continue;
12289 }
12290 let run_id = p
12291 .file_stem()
12292 .and_then(|s| s.to_str())
12293 .and_then(|s| s.strip_prefix("r-"))
12294 .and_then(|s| s.parse::<u128>().ok());
12295 if let Some(id) = run_id {
12296 if !referenced.contains(&id) {
12297 err(
12298 "warning",
12299 format!("orphan run file r-{id}.sr not referenced by the manifest"),
12300 );
12301 }
12302 }
12303 }
12304 }
12305 }
12306
12307 let external_names = cat
12308 .external_tables
12309 .iter()
12310 .map(|entry| entry.name.clone())
12311 .collect::<std::collections::HashSet<_>>();
12312 let vtab_dir = self.root.join(VTAB_DIR);
12313 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
12314 for (entry_index, entry) in entries.flatten().enumerate() {
12315 if entry_index % 256 == 0 {
12316 if let Some(control) = control {
12317 control.checkpoint()?;
12318 }
12319 }
12320 let name = entry.file_name();
12321 let Some(name) = name.to_str() else { continue };
12322 if !external_names.contains(name) {
12323 issues.push(CheckIssue {
12324 table_id: EXTERNAL_TABLE_ID,
12325 table_name: name.to_string(),
12326 severity: "warning".into(),
12327 description: format!(
12328 "orphan external table state entry {:?} not referenced by the catalog",
12329 entry.path()
12330 ),
12331 });
12332 }
12333 }
12334 }
12335
12336 if let Some(control) = control {
12343 control.checkpoint()?;
12344 }
12345 for (seg, msg) in self.shared_wal.lock().verify_segments() {
12346 issues.push(CheckIssue {
12347 table_id: WAL_TABLE_ID,
12348 table_name: "<wal>".into(),
12349 severity: "error".into(),
12350 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
12351 });
12352 }
12353 Ok(issues)
12354 }
12355
12356 pub fn doctor(&self) -> Result<Vec<u64>> {
12360 let control = crate::ExecutionControl::new(None);
12361 self.doctor_controlled(&control, || true)
12362 }
12363
12364 #[doc(hidden)]
12368 pub fn doctor_controlled<F>(
12369 &self,
12370 control: &crate::ExecutionControl,
12371 before_publish: F,
12372 ) -> Result<Vec<u64>>
12373 where
12374 F: FnOnce() -> bool,
12375 {
12376 self.doctor_controlled_with_receipt(control, before_publish)
12377 .map(|(quarantined, _)| quarantined)
12378 }
12379
12380 #[doc(hidden)]
12383 pub fn doctor_controlled_with_receipt<F>(
12384 &self,
12385 control: &crate::ExecutionControl,
12386 before_publish: F,
12387 ) -> Result<(Vec<u64>, Option<MaintenanceReceipt>)>
12388 where
12389 F: FnOnce() -> bool,
12390 {
12391 let _ddl = self.ddl_lock.lock();
12394 let _security_write = self.security_write()?;
12395 let issues = self.check_inner(Some(control))?;
12396 let bad_tables: std::collections::HashSet<u64> = issues
12401 .iter()
12402 .filter(|i| {
12403 i.severity == "error"
12404 && i.table_id != WAL_TABLE_ID
12405 && i.table_id != EXTERNAL_TABLE_ID
12406 })
12407 .map(|i| i.table_id)
12408 .collect();
12409 if bad_tables.is_empty() {
12410 return Ok((Vec::new(), None));
12411 }
12412 let _commit = self.commit_lock.lock();
12413 control.checkpoint()?;
12414 if !before_publish() {
12415 return Err(MongrelError::Cancelled);
12416 }
12417 let maintenance_epoch = self.epoch.bump_assigned();
12418 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), maintenance_epoch);
12419
12420 let qdir = self.root.join("_quarantine");
12421 crate::durable_file::create_directory(&qdir)?;
12422 let mut bad_tables = bad_tables.into_iter().collect::<Vec<_>>();
12423 bad_tables.sort_unstable();
12424
12425 let mut handles = self
12429 .tables
12430 .read()
12431 .iter()
12432 .filter(|(table_id, _)| bad_tables.binary_search(table_id).is_ok())
12433 .map(|(table_id, handle)| (*table_id, handle.clone()))
12434 .collect::<Vec<_>>();
12435 handles.sort_by_key(|(table_id, _)| *table_id);
12436 let mut table_guards = handles
12437 .iter()
12438 .map(|(table_id, handle)| (*table_id, handle.lock()))
12439 .collect::<Vec<_>>();
12440
12441 let mut next_catalog = self.catalog.read().clone();
12442 for table_id in &bad_tables {
12443 if let Some(entry) = next_catalog
12444 .tables
12445 .iter_mut()
12446 .find(|entry| entry.table_id == *table_id)
12447 {
12448 entry.state = TableState::Dropped {
12449 at_epoch: maintenance_epoch.0,
12450 };
12451 }
12452 }
12453 next_catalog.db_epoch = next_catalog.db_epoch.max(maintenance_epoch.0);
12454
12455 let txn_id = self.alloc_txn_id()?;
12456 let commit_seq = {
12457 let mut wal = self.shared_wal.lock();
12458 let append: Result<u64> = (|| {
12459 for table_id in &bad_tables {
12460 wal.append(
12461 txn_id,
12462 *table_id,
12463 crate::wal::Op::Ddl(crate::wal::DdlOp::DropTable {
12464 table_id: *table_id,
12465 }),
12466 )?;
12467 }
12468 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
12469 wal.append_commit(txn_id, maintenance_epoch, &[])
12470 })();
12471 append.map_err(|error| self.commit_outcome_unknown(maintenance_epoch, error))?
12472 };
12473 self.await_durable_commit(commit_seq, maintenance_epoch)?;
12474 for (_, table) in &mut table_guards {
12475 table.mark_unavailable_after_quarantine();
12476 }
12477 {
12478 let mut live_tables = self.tables.write();
12479 for table_id in &bad_tables {
12480 live_tables.remove(table_id);
12481 }
12482 }
12483 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
12484 self.finish_durable_publish(maintenance_epoch, &mut epoch_guard, checkpoint)?;
12485
12486 for table_id in &bad_tables {
12490 let source = self.root.join(TABLES_DIR).join(table_id.to_string());
12491 if source.exists() {
12492 let destination = qdir.join(table_id.to_string());
12493 if let Err(error) = crate::durable_file::rename(&source, &destination) {
12494 return Err(MongrelError::DurableCommit {
12495 epoch: maintenance_epoch.0,
12496 message: format!(
12497 "DOCTOR dropped table {table_id} but quarantine move failed: {error}"
12498 ),
12499 });
12500 }
12501 }
12502 }
12503 Ok((
12504 bad_tables,
12505 Some(MaintenanceReceipt {
12506 epoch: maintenance_epoch,
12507 }),
12508 ))
12509 }
12510
12511 #[allow(dead_code)]
12513 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
12514 self.kek.as_ref()
12515 }
12516
12517 #[allow(dead_code)]
12519 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
12520 &self.epoch
12521 }
12522
12523 #[allow(dead_code)]
12525 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
12526 &self.snapshots
12527 }
12528}
12529
12530fn external_state_dir(root: &Path, name: &str) -> PathBuf {
12531 root.join(VTAB_DIR).join(name)
12532}
12533
12534fn append_catalog_snapshot(
12535 wal: &mut crate::wal::SharedWal,
12536 txn_id: u64,
12537 catalog: &Catalog,
12538) -> Result<()> {
12539 let catalog_json = crate::wal::DdlOp::encode_catalog(catalog)?;
12540 wal.append(
12541 txn_id,
12542 WAL_TABLE_ID,
12543 crate::wal::Op::Ddl(crate::wal::DdlOp::CatalogSnapshot { catalog_json }),
12544 )?;
12545 Ok(())
12546}
12547
12548fn filter_ignored_staging(
12549 staging: Vec<(u64, crate::txn::Staged)>,
12550 ignored_indices: &std::collections::BTreeSet<usize>,
12551) -> Vec<(u64, crate::txn::Staged)> {
12552 if ignored_indices.is_empty() {
12553 return staging;
12554 }
12555 staging
12556 .into_iter()
12557 .enumerate()
12558 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
12559 .collect()
12560}
12561
12562fn external_state_file(root: &Path, name: &str) -> PathBuf {
12563 external_state_dir(root, name).join("state.json")
12564}
12565
12566fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
12567 let path = external_state_file(root, name);
12568 match std::fs::read(path) {
12569 Ok(bytes) => Ok(bytes),
12570 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
12571 Err(e) => Err(e.into()),
12572 }
12573}
12574
12575fn current_external_state_bytes(
12576 root: &Path,
12577 external_states: &[(String, Vec<u8>)],
12578 name: &str,
12579) -> Result<Vec<u8>> {
12580 for (table, state) in external_states.iter().rev() {
12581 if table == name {
12582 return Ok(state.clone());
12583 }
12584 }
12585 read_external_state_file(root, name)
12586}
12587
12588fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
12589 let mut out = external_states;
12590 dedup_external_states_in_place(&mut out);
12591 out
12592}
12593
12594fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
12595 let mut seen = std::collections::HashSet::new();
12596 let mut out = Vec::with_capacity(external_states.len());
12597 for (name, state) in std::mem::take(external_states).into_iter().rev() {
12598 if seen.insert(name.clone()) {
12599 out.push((name, state));
12600 }
12601 }
12602 out.reverse();
12603 *external_states = out;
12604}
12605
12606fn prepare_external_state_file(
12607 root: &Path,
12608 name: &str,
12609 state: &[u8],
12610 txn_id: u64,
12611) -> Result<PathBuf> {
12612 crate::durable_file::create_directory(&root.join(VTAB_DIR))?;
12613 let dir = external_state_dir(root, name);
12614 crate::durable_file::create_directory(&dir)?;
12615 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
12616 {
12617 let mut file = std::fs::OpenOptions::new()
12618 .create_new(true)
12619 .write(true)
12620 .open(&pending)?;
12621 file.write_all(state)?;
12622 file.sync_all()?;
12623 }
12624 Ok(pending)
12625}
12626
12627fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
12628 let path = external_state_file(root, name);
12629 crate::durable_file::replace(pending, &path)?;
12630 Ok(())
12631}
12632
12633fn write_external_state_file(
12634 durable: &crate::durable_file::DurableRoot,
12635 name: &str,
12636 state: &[u8],
12637) -> Result<()> {
12638 let directory = Path::new(VTAB_DIR).join(name);
12639 durable.create_directory_all(&directory)?;
12640 durable.write_atomic(directory.join("state.json"), state)?;
12641 Ok(())
12642}
12643
12644fn validate_recovered_data_table(
12645 catalog: &Catalog,
12646 tables: &HashMap<u64, TableHandle>,
12647 table_id: u64,
12648 commit_epoch: u64,
12649 offset: u64,
12650) -> Result<bool> {
12651 let entry = catalog
12652 .tables
12653 .iter()
12654 .find(|entry| entry.table_id == table_id)
12655 .ok_or_else(|| MongrelError::CorruptWal {
12656 offset,
12657 reason: format!("committed record references unknown table {table_id}"),
12658 })?;
12659 if commit_epoch < entry.created_epoch {
12660 return Err(MongrelError::CorruptWal {
12661 offset,
12662 reason: format!(
12663 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
12664 entry.created_epoch
12665 ),
12666 });
12667 }
12668 match entry.state {
12669 TableState::Dropped { at_epoch } => {
12670 let abandoned_build_boundary =
12675 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
12676 if commit_epoch >= at_epoch && !abandoned_build_boundary {
12677 Err(MongrelError::CorruptWal {
12678 offset,
12679 reason: format!(
12680 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
12681 ),
12682 })
12683 } else {
12684 Ok(false)
12685 }
12686 }
12687 TableState::Live | TableState::Building { .. } => {
12688 if tables.contains_key(&table_id) {
12689 Ok(true)
12690 } else {
12691 Err(MongrelError::CorruptWal {
12692 offset,
12693 reason: format!("live table {table_id} has no mounted recovery handle"),
12694 })
12695 }
12696 }
12697 }
12698}
12699
12700type RecoveryTableStage = (
12701 Vec<crate::memtable::Row>,
12702 Vec<(crate::rowid::RowId, Epoch)>,
12703 Option<Epoch>,
12704 Epoch,
12705);
12706
12707#[derive(Clone)]
12708struct RecoveryValidationTable {
12709 schema: Schema,
12710 flushed_epoch: u64,
12711}
12712
12713fn validate_shared_wal_recovery_plan(
12714 durable_root: &crate::durable_file::DurableRoot,
12715 catalog: &Catalog,
12716 recovered_table_ids: &HashSet<u64>,
12717 reconciled_table_ids: &HashSet<u64>,
12718 meta_dek: Option<&[u8; META_DEK_LEN]>,
12719 kek: Option<Arc<crate::encryption::Kek>>,
12720 records: &[crate::wal::Record],
12721) -> Result<()> {
12722 use crate::wal::{DdlOp, Op};
12723
12724 let mut tables = HashMap::<u64, RecoveryValidationTable>::new();
12725 for entry in &catalog.tables {
12726 if !matches!(entry.state, TableState::Live) {
12727 continue;
12728 }
12729 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
12730 let manifest = match crate::manifest::read_durable(durable_root, &relative_dir, meta_dek) {
12731 Ok(manifest) => Some(manifest),
12732 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
12733 Err(error) => return Err(error),
12734 };
12735 let flushed_epoch = if let Some(manifest) = manifest {
12736 if manifest.table_id != entry.table_id {
12737 return Err(MongrelError::Conflict(format!(
12738 "catalog table {} storage identity mismatch",
12739 entry.table_id
12740 )));
12741 }
12742 if (manifest.schema_id != entry.schema.schema_id
12743 && !reconciled_table_ids.contains(&entry.table_id))
12744 || manifest.flushed_epoch > manifest.current_epoch
12745 || manifest.global_idx_epoch > manifest.current_epoch
12746 || manifest.next_row_id == u64::MAX
12747 || manifest.auto_inc_next < 0
12748 || manifest.auto_inc_next == i64::MAX
12749 || (entry.schema.auto_increment_column().is_none() && manifest.auto_inc_next != 0)
12750 {
12751 return Err(MongrelError::InvalidArgument(format!(
12752 "table {} manifest counters or schema identity are invalid",
12753 entry.table_id
12754 )));
12755 }
12756 #[cfg(feature = "encryption")]
12757 let idx_dek = kek.as_ref().map(|key| key.derive_idx_key());
12758 #[cfg(not(feature = "encryption"))]
12759 let idx_dek: Option<zeroize::Zeroizing<[u8; 32]>> = None;
12760 crate::global_idx::read_durable_for(
12761 durable_root,
12762 &relative_dir,
12763 entry.table_id,
12764 &entry.schema,
12765 idx_dek.as_deref(),
12766 )?;
12767 let mut run_ids = HashSet::new();
12768 let mut maximum_row_id = None::<u64>;
12769 for run in &manifest.runs {
12770 if run.run_id >= u64::MAX as u128
12771 || run.epoch_created > manifest.current_epoch
12772 || !run_ids.insert(run.run_id)
12773 {
12774 return Err(MongrelError::InvalidArgument(format!(
12775 "table {} manifest contains an invalid or duplicate run id",
12776 entry.table_id
12777 )));
12778 }
12779 let relative = relative_dir
12780 .join(crate::engine::RUNS_DIR)
12781 .join(format!("r-{}.sr", run.run_id as u64));
12782 let file = durable_root.open_regular(&relative)?;
12783 let mut reader = crate::sorted_run::RunReader::open_file(
12784 file,
12785 entry.schema.clone(),
12786 kek.clone(),
12787 )?;
12788 let header = reader.header();
12789 if header.run_id != run.run_id
12790 || header.level != run.level
12791 || header.row_count != run.row_count
12792 || !header.is_uniform_epoch() && header.epoch_created != run.epoch_created
12793 || header.is_uniform_epoch() && header.epoch_created != 0
12794 || header.schema_id > entry.schema.schema_id
12795 {
12796 return Err(MongrelError::InvalidArgument(format!(
12797 "table {} run {} differs from its manifest: header=(id {}, level {}, rows {}, epoch {}, schema {}), manifest=(id {}, level {}, rows {}, epoch {}, schema <= {})",
12798 entry.table_id,
12799 run.run_id,
12800 header.run_id,
12801 header.level,
12802 header.row_count,
12803 header.epoch_created,
12804 header.schema_id,
12805 run.run_id,
12806 run.level,
12807 run.row_count,
12808 run.epoch_created,
12809 entry.schema.schema_id,
12810 )));
12811 }
12812 if header.row_count != 0 {
12813 maximum_row_id = Some(
12814 maximum_row_id
12815 .map_or(header.max_row_id, |value| value.max(header.max_row_id)),
12816 );
12817 }
12818 reader.validate_all_pages()?;
12819 }
12820 if maximum_row_id.is_some_and(|maximum| manifest.next_row_id <= maximum) {
12821 return Err(MongrelError::InvalidArgument(format!(
12822 "table {} next_row_id does not advance beyond persisted rows",
12823 entry.table_id
12824 )));
12825 }
12826 for run in &manifest.retiring {
12827 if run.run_id >= u64::MAX as u128
12828 || run.retire_epoch > manifest.current_epoch
12829 || !run_ids.insert(run.run_id)
12830 {
12831 return Err(MongrelError::InvalidArgument(format!(
12832 "table {} manifest contains an invalid or aliased retired run",
12833 entry.table_id
12834 )));
12835 }
12836 }
12837 manifest.flushed_epoch
12838 } else {
12839 if !recovered_table_ids.contains(&entry.table_id) {
12840 return Err(MongrelError::NotFound(format!(
12841 "live table {} manifest is missing",
12842 entry.table_id
12843 )));
12844 }
12845 0
12846 };
12847 tables.insert(
12848 entry.table_id,
12849 RecoveryValidationTable {
12850 schema: entry.schema.clone(),
12851 flushed_epoch,
12852 },
12853 );
12854 }
12855
12856 let committed = records
12857 .iter()
12858 .filter_map(|record| match record.op {
12859 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
12860 _ => None,
12861 })
12862 .collect::<HashMap<_, _>>();
12863 let mut run_ids = HashSet::new();
12864 let mut recovered_row_ids = HashMap::<u64, HashSet<u64>>::new();
12865 for record in records {
12866 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
12867 continue;
12868 };
12869 match &record.op {
12870 Op::Put { table_id, rows } => {
12871 let table = validate_recovery_data_table_plan(
12872 catalog,
12873 &tables,
12874 *table_id,
12875 commit_epoch,
12876 record.seq.0,
12877 )?;
12878 let decoded: Vec<crate::memtable::Row> =
12879 bincode::deserialize(rows).map_err(|error| MongrelError::CorruptWal {
12880 offset: record.seq.0,
12881 reason: format!(
12882 "committed Put payload for transaction {} could not be decoded: {error}",
12883 record.txn_id
12884 ),
12885 })?;
12886 if let Some(table) = table {
12887 for row in &decoded {
12888 if !recovered_row_ids
12889 .entry(*table_id)
12890 .or_default()
12891 .insert(row.row_id.0)
12892 {
12893 return Err(MongrelError::CorruptWal {
12894 offset: record.seq.0,
12895 reason: format!(
12896 "committed WAL repeats recovered row id {} for table {table_id}",
12897 row.row_id.0
12898 ),
12899 });
12900 }
12901 validate_recovered_row(&table.schema, row)?;
12902 }
12903 }
12904 }
12905 Op::Delete { table_id, .. } | Op::TruncateTable { table_id } => {
12906 validate_recovery_data_table_plan(
12907 catalog,
12908 &tables,
12909 *table_id,
12910 commit_epoch,
12911 record.seq.0,
12912 )?;
12913 }
12914 Op::ExternalTableState { name, .. } => validate_recovered_external_name(name)?,
12915 Op::Ddl(DdlOp::ResetExternalTableState {
12916 name,
12917 generation_epoch,
12918 }) => {
12919 if *generation_epoch != commit_epoch {
12920 return Err(MongrelError::CorruptWal {
12921 offset: record.seq.0,
12922 reason: format!(
12923 "external state reset epoch {generation_epoch} does not match WAL commit epoch {commit_epoch}"
12924 ),
12925 });
12926 }
12927 validate_recovered_external_name(name)?;
12928 }
12929 Op::TxnCommit { added_runs, .. } => {
12930 for added in added_runs {
12931 let Some(table) = validate_recovery_data_table_plan(
12932 catalog,
12933 &tables,
12934 added.table_id,
12935 commit_epoch,
12936 record.seq.0,
12937 )?
12938 else {
12939 continue;
12940 };
12941 if added.run_id >= u64::MAX as u128
12942 || !run_ids.insert((added.table_id, added.run_id))
12943 {
12944 return Err(MongrelError::CorruptWal {
12945 offset: record.seq.0,
12946 reason: format!(
12947 "duplicate or invalid recovered run {} for table {}",
12948 added.run_id, added.table_id
12949 ),
12950 });
12951 }
12952 if commit_epoch <= table.flushed_epoch {
12953 continue;
12954 }
12955 validate_planned_spilled_run(
12956 durable_root,
12957 record.txn_id,
12958 commit_epoch,
12959 added,
12960 &table.schema,
12961 kek.clone(),
12962 )?;
12963 }
12964 }
12965 _ => {}
12966 }
12967 }
12968 Ok(())
12969}
12970
12971fn validate_recovery_data_table_plan<'a>(
12972 catalog: &Catalog,
12973 tables: &'a HashMap<u64, RecoveryValidationTable>,
12974 table_id: u64,
12975 commit_epoch: u64,
12976 offset: u64,
12977) -> Result<Option<&'a RecoveryValidationTable>> {
12978 let entry = catalog
12979 .tables
12980 .iter()
12981 .find(|entry| entry.table_id == table_id)
12982 .ok_or_else(|| MongrelError::CorruptWal {
12983 offset,
12984 reason: format!("committed record references unknown table {table_id}"),
12985 })?;
12986 if commit_epoch < entry.created_epoch {
12987 return Err(MongrelError::CorruptWal {
12988 offset,
12989 reason: format!(
12990 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
12991 entry.created_epoch
12992 ),
12993 });
12994 }
12995 match entry.state {
12996 TableState::Dropped { at_epoch } => {
12997 let abandoned =
12998 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
12999 if commit_epoch >= at_epoch && !abandoned {
13000 return Err(MongrelError::CorruptWal {
13001 offset,
13002 reason: format!(
13003 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
13004 ),
13005 });
13006 }
13007 Ok(None)
13008 }
13009 TableState::Live => {
13010 tables
13011 .get(&table_id)
13012 .map(Some)
13013 .ok_or_else(|| MongrelError::CorruptWal {
13014 offset,
13015 reason: format!("live table {table_id} has no recovery plan"),
13016 })
13017 }
13018 TableState::Building { .. } => Err(MongrelError::CorruptWal {
13019 offset,
13020 reason: format!("building table {table_id} was not normalized before recovery"),
13021 }),
13022 }
13023}
13024
13025fn validate_planned_spilled_run(
13026 root: &crate::durable_file::DurableRoot,
13027 txn_id: u64,
13028 commit_epoch: u64,
13029 added: &crate::wal::AddedRun,
13030 schema: &Schema,
13031 kek: Option<Arc<crate::encryption::Kek>>,
13032) -> Result<()> {
13033 let table = Path::new(TABLES_DIR).join(added.table_id.to_string());
13034 let destination = table
13035 .join(crate::engine::RUNS_DIR)
13036 .join(format!("r-{}.sr", added.run_id as u64));
13037 let pending = table
13038 .join("_txn")
13039 .join(txn_id.to_string())
13040 .join(format!("r-{}.sr", added.run_id as u64));
13041 let file = match root.open_regular(&destination) {
13042 Ok(file) => file,
13043 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
13044 root.open_regular(&pending).map_err(|pending_error| {
13045 if pending_error.kind() == std::io::ErrorKind::NotFound {
13046 MongrelError::CorruptWal {
13047 offset: commit_epoch,
13048 reason: format!(
13049 "committed spilled run {} for transaction {txn_id} is missing",
13050 added.run_id
13051 ),
13052 }
13053 } else {
13054 pending_error.into()
13055 }
13056 })?
13057 }
13058 Err(error) => return Err(error.into()),
13059 };
13060 let mut reader = crate::sorted_run::RunReader::open_file(file, schema.clone(), kek)?;
13061 let header = reader.header();
13062 if header.run_id != added.run_id
13063 || header.content_hash != added.content_hash
13064 || header.row_count != added.row_count
13065 || header.level != added.level
13066 || header.min_row_id != added.min_row_id
13067 || header.max_row_id != added.max_row_id
13068 || header.schema_id != schema.schema_id
13069 || !header.is_uniform_epoch()
13070 || header.epoch_created != 0
13071 {
13072 return Err(MongrelError::CorruptWal {
13073 offset: commit_epoch,
13074 reason: format!(
13075 "committed spilled run {} metadata differs from WAL",
13076 added.run_id
13077 ),
13078 });
13079 }
13080 reader.validate_all_pages()?;
13081 Ok(())
13082}
13083
13084fn recover_shared_wal(
13093 durable_root: &crate::durable_file::DurableRoot,
13094 tables: &HashMap<u64, TableHandle>,
13095 catalog: &Catalog,
13096 epoch: &EpochAuthority,
13097 records: &[crate::wal::Record],
13098) -> Result<()> {
13099 use crate::memtable::Row;
13100 use crate::wal::{DdlOp, Op};
13101
13102 let mut committed: HashMap<u64, u64> = HashMap::new();
13104 let mut spilled_to_link: Vec<(
13105 u64, u64, Vec<crate::wal::AddedRun>,
13108 )> = Vec::new();
13109 for r in records {
13110 if let Op::TxnCommit {
13111 epoch: ce,
13112 ref added_runs,
13113 } = r.op
13114 {
13115 committed.insert(r.txn_id, ce);
13116 if !added_runs.is_empty() {
13117 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
13118 }
13119 }
13120 }
13121 for record in records {
13122 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
13123 continue;
13124 };
13125 match &record.op {
13126 Op::Put { table_id, .. }
13127 | Op::Delete { table_id, .. }
13128 | Op::TruncateTable { table_id } => {
13129 validate_recovered_data_table(
13130 catalog,
13131 tables,
13132 *table_id,
13133 commit_epoch,
13134 record.seq.0,
13135 )?;
13136 }
13137 Op::TxnCommit { added_runs, .. } => {
13138 for run in added_runs {
13139 validate_recovered_data_table(
13140 catalog,
13141 tables,
13142 run.table_id,
13143 commit_epoch,
13144 record.seq.0,
13145 )?;
13146 }
13147 }
13148 _ => {}
13149 }
13150 }
13151 let truncated_transactions: HashSet<(u64, u64)> = records
13152 .iter()
13153 .filter_map(|record| {
13154 committed.get(&record.txn_id)?;
13155 match record.op {
13156 Op::TruncateTable { table_id } => Some((record.txn_id, table_id)),
13157 _ => None,
13158 }
13159 })
13160 .collect();
13161
13162 enum ExternalRecoveryAction {
13164 Write { name: String, state: Vec<u8> },
13165 Reset { name: String },
13166 }
13167 let mut stage: HashMap<u64, RecoveryTableStage> = HashMap::new();
13168 let mut external_actions = Vec::new();
13169 let mut max_epoch = epoch.visible().0;
13170 for r in records.iter().cloned() {
13171 let Some(&ce) = committed.get(&r.txn_id) else {
13172 continue; };
13174 let commit_epoch = Epoch(ce);
13175 max_epoch = max_epoch.max(ce);
13176 match r.op {
13177 Op::Put { table_id, rows } => {
13178 let skip = tables
13180 .get(&table_id)
13181 .map(|h| h.lock().flushed_epoch() >= ce)
13182 .unwrap_or(true);
13183 if skip {
13184 continue;
13185 }
13186 let rows: Vec<Row> = bincode::deserialize(&rows).map_err(|error| {
13187 MongrelError::CorruptWal {
13188 offset: r.seq.0,
13189 reason: format!(
13190 "committed Put payload for transaction {} could not be decoded: {error}",
13191 r.txn_id
13192 ),
13193 }
13194 })?;
13195 let rows: Vec<Row> = rows
13198 .into_iter()
13199 .map(|mut row| {
13200 row.committed_epoch = commit_epoch;
13201 row
13202 })
13203 .collect();
13204 let entry = stage
13205 .entry(table_id)
13206 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
13207 entry.0.extend(rows);
13208 entry.3 = commit_epoch;
13209 }
13210 Op::Delete { table_id, row_ids } => {
13211 let skip = tables
13212 .get(&table_id)
13213 .map(|h| h.lock().flushed_epoch() >= ce)
13214 .unwrap_or(true);
13215 if skip {
13216 continue;
13217 }
13218 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
13219 let entry = stage
13220 .entry(table_id)
13221 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
13222 entry.1.extend(dels);
13223 entry.3 = commit_epoch;
13224 }
13225 Op::TruncateTable { table_id } => {
13226 let skip = tables
13227 .get(&table_id)
13228 .map(|h| h.lock().flushed_epoch() >= ce)
13229 .unwrap_or(true);
13230 if skip {
13231 continue;
13232 }
13233 stage.insert(
13234 table_id,
13235 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
13236 );
13237 }
13238 Op::ExternalTableState { name, state } => {
13239 let current_generation = catalog
13240 .external_tables
13241 .iter()
13242 .find(|entry| entry.name == name)
13243 .map(|entry| entry.created_epoch);
13244 if current_generation.is_some_and(|created_epoch| ce >= created_epoch) {
13245 validate_recovered_external_name(&name)?;
13246 external_actions.push(ExternalRecoveryAction::Write { name, state });
13247 }
13248 }
13249 Op::Ddl(DdlOp::ResetExternalTableState {
13250 name,
13251 generation_epoch,
13252 }) => {
13253 if generation_epoch != ce {
13254 return Err(MongrelError::CorruptWal {
13255 offset: r.seq.0,
13256 reason: format!(
13257 "external state reset epoch {generation_epoch} does not match WAL commit epoch {ce}"
13258 ),
13259 });
13260 }
13261 validate_recovered_external_name(&name)?;
13262 external_actions.push(ExternalRecoveryAction::Reset { name });
13263 }
13264 Op::Flush { .. }
13265 | Op::TxnCommit { .. }
13266 | Op::TxnAbort
13267 | Op::Ddl(_)
13268 | Op::BeforeImage { .. }
13269 | Op::CommitTimestamp { .. }
13270 | Op::SpilledRows { .. } => {}
13271 }
13272 }
13273 for (_, commit_epoch, added_runs) in &mut spilled_to_link {
13274 added_runs.retain(|added| {
13275 tables
13276 .get(&added.table_id)
13277 .is_some_and(|table| table.lock().flushed_epoch() < *commit_epoch)
13278 });
13279 }
13280 spilled_to_link.retain(|(_, _, added_runs)| !added_runs.is_empty());
13281 validate_recovery_table_stages(tables, &stage)?;
13282 validate_recovery_spilled_runs(durable_root, tables, &spilled_to_link)?;
13283
13284 for action in external_actions {
13288 match action {
13289 ExternalRecoveryAction::Write { name, state } => {
13290 write_external_state_file(durable_root, &name, &state)?;
13291 }
13292 ExternalRecoveryAction::Reset { name } => {
13293 durable_root.create_directory_all(VTAB_DIR)?;
13294 durable_root.remove_directory_all(Path::new(VTAB_DIR).join(name))?;
13295 }
13296 }
13297 }
13298 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
13299 let Some(handle) = tables.get(&table_id) else {
13300 continue;
13301 };
13302 let mut t = handle.lock();
13303 if let Some(epoch) = truncate_epoch {
13304 t.apply_truncate(epoch);
13305 }
13306 t.recover_apply(rows, deletes)?;
13307 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
13311 t.live_count = rows.len() as u64;
13312 t.persist_manifest(table_epoch.max(epoch.visible()))?;
13316 }
13317
13318 for (txn_id, ce, added_runs) in &spilled_to_link {
13322 for ar in added_runs {
13323 let Some(handle) = tables.get(&ar.table_id) else {
13324 continue;
13325 };
13326 let mut t = handle.lock();
13327 let table_dir = Path::new(TABLES_DIR).join(ar.table_id.to_string());
13328 let destination = table_dir
13329 .join(crate::engine::RUNS_DIR)
13330 .join(format!("r-{}.sr", ar.run_id));
13331 match durable_root.open_regular(&destination) {
13332 Ok(_) => {}
13333 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
13334 let pending = table_dir
13335 .join("_txn")
13336 .join(txn_id.to_string())
13337 .join(format!("r-{}.sr", ar.run_id));
13338 durable_root.rename_file_new(&pending, &destination)?;
13339 }
13340 Err(error) => return Err(error.into()),
13341 }
13342 let linked = t.recover_spilled_run(crate::manifest::RunRef {
13348 run_id: ar.run_id,
13349 level: ar.level,
13350 epoch_created: *ce,
13351 row_count: ar.row_count,
13352 });
13353 let replaced = truncated_transactions.contains(&(*txn_id, ar.table_id));
13354 if replaced {
13355 t.set_flushed_epoch(Epoch(*ce));
13356 }
13357 if linked || replaced {
13358 t.persist_manifest(Epoch(*ce).max(epoch.visible()))?;
13359 }
13360 }
13361 }
13362
13363 epoch.advance_recovered(Epoch(max_epoch));
13364 Ok(())
13365}
13366
13367fn reconcile_recovered_table_metadata(
13368 tables: &HashMap<u64, TableHandle>,
13369 epoch: Epoch,
13370) -> Result<()> {
13371 let mut table_ids = tables.keys().copied().collect::<Vec<_>>();
13372 table_ids.sort_unstable();
13373 let mut plans = Vec::with_capacity(table_ids.len());
13374 for table_id in &table_ids {
13375 let handle = tables.get(table_id).ok_or_else(|| {
13376 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
13377 })?;
13378 plans.push((*table_id, handle.lock().plan_recovered_metadata()?));
13379 }
13380 for (table_id, plan) in plans {
13383 let handle = tables.get(&table_id).ok_or_else(|| {
13384 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
13385 })?;
13386 handle.lock().apply_recovered_metadata(plan, epoch)?;
13387 }
13388 Ok(())
13389}
13390
13391fn validate_recovered_external_name(name: &str) -> Result<()> {
13392 if name.is_empty()
13393 || !name.chars().all(|character| {
13394 character.is_ascii_alphanumeric() || character == '_' || character == '-'
13395 })
13396 {
13397 return Err(MongrelError::CorruptWal {
13398 offset: 0,
13399 reason: format!("unsafe recovered external-table name {name:?}"),
13400 });
13401 }
13402 Ok(())
13403}
13404
13405fn validate_recovery_table_stages(
13406 tables: &HashMap<u64, TableHandle>,
13407 stages: &HashMap<u64, RecoveryTableStage>,
13408) -> Result<()> {
13409 for (table_id, (rows, _, _, _)) in stages {
13410 let handle = tables
13411 .get(table_id)
13412 .ok_or_else(|| MongrelError::CorruptWal {
13413 offset: *table_id,
13414 reason: format!("recovery stage references unmounted table {table_id}"),
13415 })?;
13416 let table = handle.lock();
13417 table.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
13420 for row in rows {
13421 validate_recovered_row(table.schema(), row)?;
13422 }
13423 }
13424 Ok(())
13425}
13426
13427fn validate_recovered_row(schema: &Schema, row: &crate::memtable::Row) -> Result<()> {
13428 if row.deleted || row.row_id.0 == u64::MAX {
13429 return Err(MongrelError::CorruptWal {
13430 offset: row.row_id.0,
13431 reason: "committed Put payload contains a tombstone or exhausted row id".into(),
13432 });
13433 }
13434 let cells = row
13435 .columns
13436 .iter()
13437 .map(|(column, value)| (*column, value.clone()))
13438 .collect::<Vec<_>>();
13439 schema
13440 .validate_persisted_values(&cells)
13441 .map_err(|error| MongrelError::CorruptWal {
13442 offset: row.row_id.0,
13443 reason: format!("recovered row violates table schema: {error}"),
13444 })?;
13445 if schema.auto_increment_column().is_some_and(|column| {
13446 matches!(row.columns.get(&column.id), Some(Value::Int64(value)) if *value == i64::MAX)
13447 }) {
13448 return Err(MongrelError::CorruptWal {
13449 offset: row.row_id.0,
13450 reason: "recovered AUTO_INCREMENT value exhausts i64".into(),
13451 });
13452 }
13453 Ok(())
13454}
13455
13456fn validate_recovery_spilled_runs(
13457 root: &crate::durable_file::DurableRoot,
13458 tables: &HashMap<u64, TableHandle>,
13459 spilled: &[(u64, u64, Vec<crate::wal::AddedRun>)],
13460) -> Result<()> {
13461 let mut identities = HashSet::new();
13462 for (txn_id, commit_epoch, added_runs) in spilled {
13463 for added in added_runs {
13464 if added.run_id >= u64::MAX as u128 {
13465 return Err(MongrelError::CorruptWal {
13466 offset: *commit_epoch,
13467 reason: format!(
13468 "recovered run id {} exceeds the on-disk namespace",
13469 added.run_id
13470 ),
13471 });
13472 }
13473 let Some(handle) = tables.get(&added.table_id) else {
13474 continue;
13475 };
13476 if !identities.insert((added.table_id, added.run_id)) {
13477 return Err(MongrelError::CorruptWal {
13478 offset: *commit_epoch,
13479 reason: format!(
13480 "duplicate recovered run {} for table {}",
13481 added.run_id, added.table_id
13482 ),
13483 });
13484 }
13485 let table = handle.lock();
13486 validate_planned_spilled_run(
13487 root,
13488 *txn_id,
13489 *commit_epoch,
13490 added,
13491 table.schema(),
13492 table.kek(),
13493 )?;
13494 }
13495 }
13496 Ok(())
13497}
13498
13499fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
13500 match condition {
13501 ProcedureCondition::Pk { .. } => {
13502 if schema.primary_key().is_none() {
13503 return Err(MongrelError::InvalidArgument(
13504 "procedure condition Pk references a table without a primary key".into(),
13505 ));
13506 }
13507 }
13508 ProcedureCondition::BitmapEq { column_id, .. }
13509 | ProcedureCondition::BitmapIn { column_id, .. }
13510 | ProcedureCondition::Range { column_id, .. }
13511 | ProcedureCondition::RangeF64 { column_id, .. }
13512 | ProcedureCondition::IsNull { column_id }
13513 | ProcedureCondition::IsNotNull { column_id }
13514 | ProcedureCondition::FmContains { column_id, .. } => {
13515 validate_column_id(*column_id, schema)?;
13516 }
13517 }
13518 Ok(())
13519}
13520
13521fn bind_procedure_args(
13522 procedure: &StoredProcedure,
13523 mut args: HashMap<String, crate::Value>,
13524) -> Result<HashMap<String, crate::Value>> {
13525 let mut out = HashMap::new();
13526 for param in &procedure.params {
13527 let value = match args.remove(¶m.name) {
13528 Some(value) => value,
13529 None => param.default.clone().ok_or_else(|| {
13530 MongrelError::InvalidArgument(format!(
13531 "missing required procedure parameter {:?}",
13532 param.name
13533 ))
13534 })?,
13535 };
13536 if !param.nullable && matches!(value, crate::Value::Null) {
13537 return Err(MongrelError::InvalidArgument(format!(
13538 "procedure parameter {:?} must not be NULL",
13539 param.name
13540 )));
13541 }
13542 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty.clone()) {
13543 return Err(MongrelError::InvalidArgument(format!(
13544 "procedure parameter {:?} has wrong type",
13545 param.name
13546 )));
13547 }
13548 out.insert(param.name.clone(), value);
13549 }
13550 if let Some(extra) = args.keys().next() {
13551 return Err(MongrelError::InvalidArgument(format!(
13552 "unknown procedure parameter {extra:?}"
13553 )));
13554 }
13555 Ok(out)
13556}
13557
13558fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
13559 matches!(
13560 (value, ty),
13561 (crate::Value::Bool(_), crate::TypeId::Bool)
13562 | (crate::Value::Int64(_), crate::TypeId::Int8)
13563 | (crate::Value::Int64(_), crate::TypeId::Int16)
13564 | (crate::Value::Int64(_), crate::TypeId::Int32)
13565 | (crate::Value::Int64(_), crate::TypeId::Int64)
13566 | (crate::Value::Int64(_), crate::TypeId::UInt8)
13567 | (crate::Value::Int64(_), crate::TypeId::UInt16)
13568 | (crate::Value::Int64(_), crate::TypeId::UInt32)
13569 | (crate::Value::Int64(_), crate::TypeId::UInt64)
13570 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
13571 | (crate::Value::Int64(_), crate::TypeId::Date32)
13572 | (crate::Value::Float64(_), crate::TypeId::Float32)
13573 | (crate::Value::Float64(_), crate::TypeId::Float64)
13574 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
13575 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
13576 )
13577}
13578
13579fn eval_cells(
13580 cells: &[crate::procedure::ProcedureCell],
13581 args: &HashMap<String, crate::Value>,
13582 outputs: &HashMap<String, ProcedureCallOutput>,
13583) -> Result<Vec<(u16, crate::Value)>> {
13584 cells
13585 .iter()
13586 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
13587 .collect()
13588}
13589
13590fn eval_condition(
13591 condition: &ProcedureCondition,
13592 args: &HashMap<String, crate::Value>,
13593 outputs: &HashMap<String, ProcedureCallOutput>,
13594) -> Result<crate::Condition> {
13595 Ok(match condition {
13596 ProcedureCondition::Pk { value } => {
13597 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
13598 }
13599 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
13600 column_id: *column_id,
13601 value: eval_value(value, args, outputs)?.encode_key(),
13602 },
13603 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
13604 column_id: *column_id,
13605 values: values
13606 .iter()
13607 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
13608 .collect::<Result<Vec<_>>>()?,
13609 },
13610 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
13611 column_id: *column_id,
13612 lo: expect_i64(eval_value(lo, args, outputs)?)?,
13613 hi: expect_i64(eval_value(hi, args, outputs)?)?,
13614 },
13615 ProcedureCondition::RangeF64 {
13616 column_id,
13617 lo,
13618 lo_inclusive,
13619 hi,
13620 hi_inclusive,
13621 } => crate::Condition::RangeF64 {
13622 column_id: *column_id,
13623 lo: expect_f64(eval_value(lo, args, outputs)?)?,
13624 lo_inclusive: *lo_inclusive,
13625 hi: expect_f64(eval_value(hi, args, outputs)?)?,
13626 hi_inclusive: *hi_inclusive,
13627 },
13628 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
13629 column_id: *column_id,
13630 },
13631 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
13632 column_id: *column_id,
13633 },
13634 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
13635 column_id: *column_id,
13636 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
13637 },
13638 })
13639}
13640
13641fn eval_value(
13642 value: &ProcedureValue,
13643 args: &HashMap<String, crate::Value>,
13644 outputs: &HashMap<String, ProcedureCallOutput>,
13645) -> Result<crate::Value> {
13646 match value {
13647 ProcedureValue::Literal(value) => Ok(value.clone()),
13648 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
13649 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
13650 }),
13651 ProcedureValue::StepScalar(id) => match outputs.get(id) {
13652 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
13653 _ => Err(MongrelError::InvalidArgument(format!(
13654 "procedure step {id:?} did not return a scalar"
13655 ))),
13656 },
13657 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
13658 Err(MongrelError::InvalidArgument(
13659 "row-valued procedure reference cannot be used as a scalar".into(),
13660 ))
13661 }
13662 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
13663 "structured procedure value cannot be used as a scalar cell".into(),
13664 )),
13665 }
13666}
13667
13668fn eval_return_output(
13669 value: &ProcedureValue,
13670 args: &HashMap<String, crate::Value>,
13671 outputs: &HashMap<String, ProcedureCallOutput>,
13672) -> Result<ProcedureCallOutput> {
13673 match value {
13674 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
13675 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
13676 args.get(name).cloned().ok_or_else(|| {
13677 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
13678 })?,
13679 )),
13680 ProcedureValue::StepRows(id)
13681 | ProcedureValue::StepRow(id)
13682 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
13683 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
13684 }),
13685 ProcedureValue::Object(fields) => {
13686 let mut out = Vec::with_capacity(fields.len());
13687 for (name, value) in fields {
13688 out.push((name.clone(), eval_return_output(value, args, outputs)?));
13689 }
13690 Ok(ProcedureCallOutput::Object(out))
13691 }
13692 ProcedureValue::Array(values) => {
13693 let mut out = Vec::with_capacity(values.len());
13694 for value in values {
13695 out.push(eval_return_output(value, args, outputs)?);
13696 }
13697 Ok(ProcedureCallOutput::Array(out))
13698 }
13699 }
13700}
13701
13702fn expect_i64(value: crate::Value) -> Result<i64> {
13703 match value {
13704 crate::Value::Int64(value) => Ok(value),
13705 _ => Err(MongrelError::InvalidArgument(
13706 "procedure value must be Int64".into(),
13707 )),
13708 }
13709}
13710
13711fn expect_f64(value: crate::Value) -> Result<f64> {
13712 match value {
13713 crate::Value::Float64(value) => Ok(value),
13714 _ => Err(MongrelError::InvalidArgument(
13715 "procedure value must be Float64".into(),
13716 )),
13717 }
13718}
13719
13720fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
13721 match value {
13722 crate::Value::Bytes(value) => Ok(value),
13723 _ => Err(MongrelError::InvalidArgument(
13724 "procedure value must be Bytes".into(),
13725 )),
13726 }
13727}
13728
13729fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
13730 if schema.columns.iter().any(|c| c.id == column_id) {
13731 Ok(())
13732 } else {
13733 Err(MongrelError::InvalidArgument(format!(
13734 "unknown column id {column_id}"
13735 )))
13736 }
13737}
13738
13739fn trigger_matches_event(
13740 trigger: &StoredTrigger,
13741 event: &WriteEvent,
13742 cat: &Catalog,
13743) -> Result<bool> {
13744 if trigger.event != event.kind {
13745 return Ok(false);
13746 }
13747 let TriggerTarget::Table(target) = &trigger.target else {
13748 return Ok(false);
13749 };
13750 if target != &event.table {
13751 return Ok(false);
13752 }
13753 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
13754 let schema = &cat
13755 .live(target)
13756 .ok_or_else(|| {
13757 MongrelError::InvalidArgument(format!(
13758 "trigger {:?} references unknown table {target:?}",
13759 trigger.name
13760 ))
13761 })?
13762 .schema;
13763 let mut watched = Vec::with_capacity(trigger.update_of.len());
13764 for name in &trigger.update_of {
13765 let col = schema.column(name).ok_or_else(|| {
13766 MongrelError::InvalidArgument(format!(
13767 "trigger {:?} references unknown UPDATE OF column {name:?}",
13768 trigger.name
13769 ))
13770 })?;
13771 watched.push(col.id);
13772 }
13773 if !event
13774 .changed_columns
13775 .iter()
13776 .any(|column_id| watched.contains(column_id))
13777 {
13778 return Ok(false);
13779 }
13780 }
13781 Ok(true)
13782}
13783
13784fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
13785 let mut ids = std::collections::BTreeSet::new();
13786 if let Some(old) = old {
13787 ids.extend(old.columns.keys().copied());
13788 }
13789 if let Some(new) = new {
13790 ids.extend(new.columns.keys().copied());
13791 }
13792 ids.into_iter()
13793 .filter(|id| {
13794 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
13795 })
13796 .collect()
13797}
13798
13799fn eval_trigger_cells(
13800 cells: &[crate::trigger::TriggerCell],
13801 event: &WriteEvent,
13802 selected: Option<&TriggerRowImage>,
13803) -> Result<Vec<(u16, Value)>> {
13804 cells
13805 .iter()
13806 .map(|cell| {
13807 Ok((
13808 cell.column_id,
13809 eval_trigger_value(&cell.value, event, selected)?,
13810 ))
13811 })
13812 .collect()
13813}
13814
13815fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
13816 match expr {
13817 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
13818 Value::Bool(value) => Ok(value),
13819 Value::Null => Ok(false),
13820 other => Err(MongrelError::InvalidArgument(format!(
13821 "trigger WHEN value must be boolean, got {other:?}"
13822 ))),
13823 },
13824 TriggerExpr::Eq { left, right } => Ok(values_equal(
13825 &eval_trigger_value(left, event, None)?,
13826 &eval_trigger_value(right, event, None)?,
13827 )),
13828 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
13829 &eval_trigger_value(left, event, None)?,
13830 &eval_trigger_value(right, event, None)?,
13831 )),
13832 TriggerExpr::Lt { left, right } => match value_order(
13833 &eval_trigger_value(left, event, None)?,
13834 &eval_trigger_value(right, event, None)?,
13835 ) {
13836 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
13837 None => Ok(false),
13838 },
13839 TriggerExpr::Lte { left, right } => match value_order(
13840 &eval_trigger_value(left, event, None)?,
13841 &eval_trigger_value(right, event, None)?,
13842 ) {
13843 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
13844 None => Ok(false),
13845 },
13846 TriggerExpr::Gt { left, right } => match value_order(
13847 &eval_trigger_value(left, event, None)?,
13848 &eval_trigger_value(right, event, None)?,
13849 ) {
13850 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
13851 None => Ok(false),
13852 },
13853 TriggerExpr::Gte { left, right } => match value_order(
13854 &eval_trigger_value(left, event, None)?,
13855 &eval_trigger_value(right, event, None)?,
13856 ) {
13857 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
13858 None => Ok(false),
13859 },
13860 TriggerExpr::IsNull(value) => Ok(matches!(
13861 eval_trigger_value(value, event, None)?,
13862 Value::Null
13863 )),
13864 TriggerExpr::IsNotNull(value) => Ok(!matches!(
13865 eval_trigger_value(value, event, None)?,
13866 Value::Null
13867 )),
13868 TriggerExpr::And { left, right } => {
13869 if !eval_trigger_expr(left, event)? {
13870 Ok(false)
13871 } else {
13872 Ok(eval_trigger_expr(right, event)?)
13873 }
13874 }
13875 TriggerExpr::Or { left, right } => {
13876 if eval_trigger_expr(left, event)? {
13877 Ok(true)
13878 } else {
13879 Ok(eval_trigger_expr(right, event)?)
13880 }
13881 }
13882 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
13883 }
13884}
13885
13886fn eval_trigger_condition(
13887 condition: &TriggerCondition,
13888 event: &WriteEvent,
13889 selected: &TriggerRowImage,
13890 schema: &Schema,
13891) -> Result<bool> {
13892 match condition {
13893 TriggerCondition::Pk { value } => {
13894 let pk = schema.primary_key().ok_or_else(|| {
13895 MongrelError::InvalidArgument(
13896 "trigger condition Pk references a table without a primary key".into(),
13897 )
13898 })?;
13899 let lhs = eval_trigger_value(value, event, Some(selected))?;
13900 Ok(values_equal(
13901 &lhs,
13902 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
13903 ))
13904 }
13905 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
13906 selected.columns.get(column_id).unwrap_or(&Value::Null),
13907 &eval_trigger_value(value, event, Some(selected))?,
13908 )),
13909 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
13910 selected.columns.get(column_id).unwrap_or(&Value::Null),
13911 &eval_trigger_value(value, event, Some(selected))?,
13912 )),
13913 TriggerCondition::Lt { column_id, value } => match value_order(
13914 selected.columns.get(column_id).unwrap_or(&Value::Null),
13915 &eval_trigger_value(value, event, Some(selected))?,
13916 ) {
13917 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
13918 None => Ok(false),
13919 },
13920 TriggerCondition::Lte { column_id, value } => match value_order(
13921 selected.columns.get(column_id).unwrap_or(&Value::Null),
13922 &eval_trigger_value(value, event, Some(selected))?,
13923 ) {
13924 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
13925 None => Ok(false),
13926 },
13927 TriggerCondition::Gt { column_id, value } => match value_order(
13928 selected.columns.get(column_id).unwrap_or(&Value::Null),
13929 &eval_trigger_value(value, event, Some(selected))?,
13930 ) {
13931 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
13932 None => Ok(false),
13933 },
13934 TriggerCondition::Gte { column_id, value } => match value_order(
13935 selected.columns.get(column_id).unwrap_or(&Value::Null),
13936 &eval_trigger_value(value, event, Some(selected))?,
13937 ) {
13938 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
13939 None => Ok(false),
13940 },
13941 TriggerCondition::IsNull { column_id } => Ok(matches!(
13942 selected.columns.get(column_id),
13943 None | Some(Value::Null)
13944 )),
13945 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
13946 selected.columns.get(column_id),
13947 None | Some(Value::Null)
13948 )),
13949 TriggerCondition::And { left, right } => {
13950 if !eval_trigger_condition(left, event, selected, schema)? {
13951 Ok(false)
13952 } else {
13953 Ok(eval_trigger_condition(right, event, selected, schema)?)
13954 }
13955 }
13956 TriggerCondition::Or { left, right } => {
13957 if eval_trigger_condition(left, event, selected, schema)? {
13958 Ok(true)
13959 } else {
13960 Ok(eval_trigger_condition(right, event, selected, schema)?)
13961 }
13962 }
13963 TriggerCondition::Not(condition) => {
13964 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
13965 }
13966 }
13967}
13968
13969fn eval_trigger_value(
13970 value: &TriggerValue,
13971 event: &WriteEvent,
13972 selected: Option<&TriggerRowImage>,
13973) -> Result<Value> {
13974 match value {
13975 TriggerValue::Literal(value) => Ok(value.clone()),
13976 TriggerValue::NewColumn(column_id) => event
13977 .new
13978 .as_ref()
13979 .and_then(|row| row.columns.get(column_id))
13980 .cloned()
13981 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
13982 TriggerValue::OldColumn(column_id) => event
13983 .old
13984 .as_ref()
13985 .and_then(|row| row.columns.get(column_id))
13986 .cloned()
13987 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
13988 TriggerValue::SelectedColumn(column_id) => selected
13989 .and_then(|row| row.columns.get(column_id))
13990 .cloned()
13991 .ok_or_else(|| {
13992 MongrelError::InvalidArgument("SELECTED column is not available".into())
13993 }),
13994 }
13995}
13996
13997fn values_equal(left: &Value, right: &Value) -> bool {
13998 match (left, right) {
13999 (Value::Null, Value::Null) => true,
14000 (Value::Bool(a), Value::Bool(b)) => a == b,
14001 (Value::Int64(a), Value::Int64(b)) => a == b,
14002 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
14003 (Value::Bytes(a), Value::Bytes(b)) => a == b,
14004 (Value::Embedding(a), Value::Embedding(b)) => {
14005 a.len() == b.len()
14006 && a.iter()
14007 .zip(b.iter())
14008 .all(|(a, b)| a.to_bits() == b.to_bits())
14009 }
14010 _ => false,
14011 }
14012}
14013
14014fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
14015 match (left, right) {
14016 (Value::Null, _) | (_, Value::Null) => None,
14017 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
14018 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
14019 (Value::Int64(a), Value::Float64(b)) => {
14022 let af = *a as f64;
14023 Some(af.total_cmp(b))
14024 }
14025 (Value::Float64(a), Value::Int64(b)) => {
14028 let bf = *b as f64;
14029 Some(a.total_cmp(&bf))
14030 }
14031 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
14032 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
14033 (Value::Embedding(_), Value::Embedding(_)) => None,
14034 _ => None,
14035 }
14036}
14037
14038fn trigger_message(value: Value) -> String {
14039 match value {
14040 Value::Null => "NULL".into(),
14041 Value::Bool(value) => value.to_string(),
14042 Value::Int64(value) => value.to_string(),
14043 Value::Float64(value) => value.to_string(),
14044 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
14045 Value::Embedding(value) => format!("{value:?}"),
14046 Value::Decimal(value) => value.to_string(),
14047 Value::Interval {
14048 months,
14049 days,
14050 nanos,
14051 } => format!("{months}m {days}d {nanos}ns"),
14052 Value::Uuid(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
14053 Value::Json(b) => String::from_utf8_lossy(&b).into_owned(),
14054 }
14055}
14056
14057fn validate_trigger_step<'a>(
14058 step: &TriggerStep,
14059 cat: &'a Catalog,
14060 target_schema: &Schema,
14061 event: TriggerEvent,
14062 select_schemas: &mut HashMap<String, &'a Schema>,
14063) -> Result<()> {
14064 match step {
14065 TriggerStep::SetNew { cells } => {
14066 if event == TriggerEvent::Delete {
14067 return Err(MongrelError::InvalidArgument(
14068 "SetNew trigger step is not valid for DELETE triggers".into(),
14069 ));
14070 }
14071 for cell in cells {
14072 validate_column_id(cell.column_id, target_schema)?;
14073 validate_trigger_value(&cell.value, target_schema, event)?;
14074 }
14075 }
14076 TriggerStep::Insert { table, cells } => {
14077 let schema = trigger_write_schema(cat, table, "insert")?;
14078 for cell in cells {
14079 validate_column_id(cell.column_id, schema)?;
14080 validate_trigger_value(&cell.value, target_schema, event)?;
14081 }
14082 }
14083 TriggerStep::UpdateByPk { table, pk, cells } => {
14084 let schema = trigger_write_schema(cat, table, "update")?;
14085 if schema.primary_key().is_none() {
14086 return Err(MongrelError::InvalidArgument(format!(
14087 "trigger update_by_pk references table {table:?} without a primary key"
14088 )));
14089 }
14090 validate_trigger_value(pk, target_schema, event)?;
14091 for cell in cells {
14092 validate_column_id(cell.column_id, schema)?;
14093 validate_trigger_value(&cell.value, target_schema, event)?;
14094 }
14095 }
14096 TriggerStep::DeleteByPk { table, pk } => {
14097 let schema = trigger_write_schema(cat, table, "delete")?;
14098 if schema.primary_key().is_none() {
14099 return Err(MongrelError::InvalidArgument(format!(
14100 "trigger delete_by_pk references table {table:?} without a primary key"
14101 )));
14102 }
14103 validate_trigger_value(pk, target_schema, event)?;
14104 }
14105 TriggerStep::Select {
14106 id,
14107 table,
14108 conditions,
14109 } => {
14110 let schema = trigger_read_schema(cat, table)?;
14111 for condition in conditions {
14112 validate_trigger_condition(condition, schema, target_schema, event)?;
14113 }
14114 if select_schemas.contains_key(id) {
14115 return Err(MongrelError::InvalidArgument(format!(
14116 "duplicate select id {id:?} in trigger program"
14117 )));
14118 }
14119 select_schemas.insert(id.clone(), schema);
14120 }
14121 TriggerStep::Foreach { id, steps } => {
14122 if !select_schemas.contains_key(id) {
14123 return Err(MongrelError::InvalidArgument(format!(
14124 "foreach references unknown select id {id:?}"
14125 )));
14126 }
14127 let mut inner_select_schemas = select_schemas.clone();
14128 for step in steps {
14129 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
14130 }
14131 }
14132 TriggerStep::DeleteWhere { table, conditions } => {
14133 let schema = trigger_write_schema(cat, table, "delete")?;
14134 for condition in conditions {
14135 validate_trigger_condition(condition, schema, target_schema, event)?;
14136 }
14137 }
14138 TriggerStep::UpdateWhere {
14139 table,
14140 conditions,
14141 cells,
14142 } => {
14143 let schema = trigger_write_schema(cat, table, "update")?;
14144 for condition in conditions {
14145 validate_trigger_condition(condition, schema, target_schema, event)?;
14146 }
14147 for cell in cells {
14148 validate_column_id(cell.column_id, schema)?;
14149 validate_trigger_value(&cell.value, target_schema, event)?;
14150 }
14151 }
14152 TriggerStep::Raise { message, .. } => {
14153 validate_trigger_value(message, target_schema, event)?
14154 }
14155 }
14156 Ok(())
14157}
14158
14159fn trigger_validation_error(error: MongrelError) -> MongrelError {
14160 match error {
14161 MongrelError::TriggerValidation(_) => error,
14162 MongrelError::InvalidArgument(message)
14163 | MongrelError::Conflict(message)
14164 | MongrelError::NotFound(message) => MongrelError::TriggerValidation(message),
14165 error => error,
14166 }
14167}
14168
14169fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
14170 if let Some(entry) = cat.live(table) {
14171 return Ok(&entry.schema);
14172 }
14173 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
14174 let allowed = match op {
14175 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
14176 "update" | "delete" => entry.capabilities.writable,
14177 _ => false,
14178 };
14179 if !allowed {
14180 return Err(MongrelError::InvalidArgument(format!(
14181 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
14182 entry.module
14183 )));
14184 }
14185 if !entry.capabilities.transaction_safe {
14186 return Err(MongrelError::InvalidArgument(format!(
14187 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
14188 entry.module
14189 )));
14190 }
14191 return Ok(&entry.declared_schema);
14192 }
14193 Err(MongrelError::InvalidArgument(format!(
14194 "trigger references unknown table {table:?}"
14195 )))
14196}
14197
14198fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
14199 if let Some(entry) = cat.live(table) {
14200 return Ok(&entry.schema);
14201 }
14202 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
14203 if entry.capabilities.trigger_safe {
14204 return Ok(&entry.declared_schema);
14205 }
14206 return Err(MongrelError::InvalidArgument(format!(
14207 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
14208 entry.module
14209 )));
14210 }
14211 Err(MongrelError::InvalidArgument(format!(
14212 "trigger references unknown table {table:?}"
14213 )))
14214}
14215
14216fn validate_trigger_condition(
14217 condition: &TriggerCondition,
14218 schema: &Schema,
14219 target_schema: &Schema,
14220 event: TriggerEvent,
14221) -> Result<()> {
14222 match condition {
14223 TriggerCondition::Pk { value } => {
14224 if schema.primary_key().is_none() {
14225 return Err(MongrelError::InvalidArgument(
14226 "trigger condition Pk references a table without a primary key".into(),
14227 ));
14228 }
14229 validate_trigger_value(value, target_schema, event)
14230 }
14231 TriggerCondition::Eq { column_id, value }
14232 | TriggerCondition::NotEq { column_id, value }
14233 | TriggerCondition::Lt { column_id, value }
14234 | TriggerCondition::Lte { column_id, value }
14235 | TriggerCondition::Gt { column_id, value }
14236 | TriggerCondition::Gte { column_id, value } => {
14237 validate_column_id(*column_id, schema)?;
14238 validate_trigger_value(value, target_schema, event)
14239 }
14240 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
14241 validate_column_id(*column_id, schema)
14242 }
14243 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
14244 validate_trigger_condition(left, schema, target_schema, event)?;
14245 validate_trigger_condition(right, schema, target_schema, event)
14246 }
14247 TriggerCondition::Not(condition) => {
14248 validate_trigger_condition(condition, schema, target_schema, event)
14249 }
14250 }
14251}
14252
14253fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
14254 match expr {
14255 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
14256 validate_trigger_value(value, schema, event)
14257 }
14258 TriggerExpr::Eq { left, right }
14259 | TriggerExpr::NotEq { left, right }
14260 | TriggerExpr::Lt { left, right }
14261 | TriggerExpr::Lte { left, right }
14262 | TriggerExpr::Gt { left, right }
14263 | TriggerExpr::Gte { left, right } => {
14264 validate_trigger_value(left, schema, event)?;
14265 validate_trigger_value(right, schema, event)
14266 }
14267 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
14268 validate_trigger_expr(left, schema, event)?;
14269 validate_trigger_expr(right, schema, event)
14270 }
14271 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
14272 }
14273}
14274
14275fn validate_trigger_value(
14276 value: &TriggerValue,
14277 schema: &Schema,
14278 event: TriggerEvent,
14279) -> Result<()> {
14280 match value {
14281 TriggerValue::Literal(_) => Ok(()),
14282 TriggerValue::NewColumn(id) => {
14283 if event == TriggerEvent::Delete {
14284 return Err(MongrelError::InvalidArgument(
14285 "DELETE triggers cannot reference NEW".into(),
14286 ));
14287 }
14288 validate_column_id(*id, schema)
14289 }
14290 TriggerValue::OldColumn(id) => {
14291 if event == TriggerEvent::Insert {
14292 return Err(MongrelError::InvalidArgument(
14293 "INSERT triggers cannot reference OLD".into(),
14294 ));
14295 }
14296 validate_column_id(*id, schema)
14297 }
14298 TriggerValue::SelectedColumn(_) => Ok(()),
14302 }
14303}
14304
14305fn recover_ddl_from_wal(
14311 root: &Path,
14312 durable_root: Option<&crate::durable_file::DurableRoot>,
14313 target_catalog: &mut Catalog,
14314 meta_dek: Option<&[u8; META_DEK_LEN]>,
14315 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
14316 apply: bool,
14317 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
14318) -> Result<()> {
14319 use crate::wal::SharedWal;
14320 let records = match durable_root {
14321 Some(root) => SharedWal::replay_durable_with_dek(root, wal_dek)?,
14322 None => SharedWal::replay_with_dek(root, wal_dek)?,
14323 };
14324 recover_ddl_from_records(
14325 root,
14326 durable_root,
14327 target_catalog,
14328 meta_dek,
14329 apply,
14330 table_roots,
14331 &records,
14332 )
14333}
14334
14335fn recover_ddl_from_records(
14336 root: &Path,
14337 durable_root: Option<&crate::durable_file::DurableRoot>,
14338 target_catalog: &mut Catalog,
14339 meta_dek: Option<&[u8; META_DEK_LEN]>,
14340 apply: bool,
14341 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
14342 records: &[crate::wal::Record],
14343) -> Result<()> {
14344 use crate::wal::{DdlOp, Op};
14345
14346 let original_catalog = target_catalog.clone();
14347 let mut recovered_catalog = original_catalog.clone();
14348 let cat = &mut recovered_catalog;
14349 let mut created_table_ids = HashSet::<u64>::new();
14350 let mut ttl_updates = HashMap::<u64, (Option<crate::manifest::TtlPolicy>, u64)>::new();
14351
14352 let mut committed: HashMap<u64, u64> = HashMap::new();
14353 for r in records {
14354 if let Op::TxnCommit { epoch: ce, .. } = r.op {
14355 committed.insert(r.txn_id, ce);
14356 }
14357 }
14358 let catalog_snapshot_txns = records
14359 .iter()
14360 .filter_map(|record| {
14361 (committed.contains_key(&record.txn_id)
14362 && matches!(&record.op, Op::Ddl(DdlOp::CatalogSnapshot { .. })))
14363 .then_some(record.txn_id)
14364 })
14365 .collect::<HashSet<_>>();
14366
14367 let mut changed = false;
14368 let mut applied_catalog_epoch = cat.db_epoch;
14369 let max_committed_epoch = committed.values().copied().max().unwrap_or(cat.db_epoch);
14370 for r in records.iter().cloned() {
14371 let Some(&ce) = committed.get(&r.txn_id) else {
14372 continue;
14373 };
14374 let txn_id = r.txn_id;
14375 match r.op {
14376 Op::Ddl(DdlOp::CreateTable {
14377 table_id,
14378 ref name,
14379 ref schema_json,
14380 }) => {
14381 if cat.tables.iter().any(|t| t.table_id == table_id) {
14382 continue;
14383 }
14384 let schema = DdlOp::decode_schema(schema_json)?;
14385 validate_recovered_schema(&schema)?;
14386 created_table_ids.insert(table_id);
14387 cat.tables.push(CatalogEntry {
14388 table_id,
14389 name: name.clone(),
14390 schema,
14391 state: TableState::Live,
14392 created_epoch: ce,
14393 });
14394 cat.next_table_id =
14395 cat.next_table_id
14396 .max(table_id.checked_add(1).ok_or_else(|| {
14397 MongrelError::Full("table id namespace exhausted".into())
14398 })?);
14399 changed = true;
14400 }
14401 Op::Ddl(DdlOp::CreateBuildingTable {
14402 table_id,
14403 ref build_name,
14404 ref intended_name,
14405 ref query_id,
14406 created_at_unix_nanos,
14407 ref schema_json,
14408 }) => {
14409 if cat.tables.iter().any(|table| table.table_id == table_id) {
14410 continue;
14411 }
14412 let schema = DdlOp::decode_schema(schema_json)?;
14413 validate_recovered_schema(&schema)?;
14414 created_table_ids.insert(table_id);
14415 cat.tables.push(CatalogEntry {
14416 table_id,
14417 name: build_name.clone(),
14418 schema,
14419 state: TableState::Building {
14420 intended_name: intended_name.clone(),
14421 query_id: query_id.clone(),
14422 created_at_unix_nanos,
14423 replaces_table_id: None,
14424 },
14425 created_epoch: ce,
14426 });
14427 cat.next_table_id =
14428 cat.next_table_id
14429 .max(table_id.checked_add(1).ok_or_else(|| {
14430 MongrelError::Full("table id namespace exhausted".into())
14431 })?);
14432 changed = true;
14433 }
14434 Op::Ddl(DdlOp::CreateRebuildingTable {
14435 table_id,
14436 ref build_name,
14437 ref intended_name,
14438 ref query_id,
14439 created_at_unix_nanos,
14440 replaces_table_id,
14441 ref schema_json,
14442 }) => {
14443 if cat.tables.iter().any(|table| table.table_id == table_id) {
14444 continue;
14445 }
14446 let schema = DdlOp::decode_schema(schema_json)?;
14447 validate_recovered_schema(&schema)?;
14448 created_table_ids.insert(table_id);
14449 cat.tables.push(CatalogEntry {
14450 table_id,
14451 name: build_name.clone(),
14452 schema,
14453 state: TableState::Building {
14454 intended_name: intended_name.clone(),
14455 query_id: query_id.clone(),
14456 created_at_unix_nanos,
14457 replaces_table_id: Some(replaces_table_id),
14458 },
14459 created_epoch: ce,
14460 });
14461 cat.next_table_id =
14462 cat.next_table_id
14463 .max(table_id.checked_add(1).ok_or_else(|| {
14464 MongrelError::Full("table id namespace exhausted".into())
14465 })?);
14466 changed = true;
14467 }
14468 Op::Ddl(DdlOp::DropTable { table_id }) => {
14469 let mut dropped_name = None;
14470 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
14471 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
14472 dropped_name = Some(entry.name.clone());
14473 entry.state = TableState::Dropped { at_epoch: ce };
14474 changed = true;
14475 }
14476 }
14477 if let Some(name) = dropped_name {
14478 let before = cat.materialized_views.len();
14479 cat.materialized_views
14480 .retain(|definition| definition.name != name);
14481 changed |= before != cat.materialized_views.len();
14482 cat.security.rls_tables.retain(|table| table != &name);
14483 cat.security.policies.retain(|policy| policy.table != name);
14484 cat.security.masks.retain(|mask| mask.table != name);
14485 for role in &mut cat.roles {
14486 role.permissions
14487 .retain(|permission| permission_table(permission) != Some(&name));
14488 }
14489 if !catalog_snapshot_txns.contains(&txn_id) {
14490 advance_security_version(cat)?;
14491 }
14492 }
14493 }
14494 Op::Ddl(DdlOp::PublishBuildingTable {
14495 table_id,
14496 ref new_name,
14497 }) => {
14498 if let Some(entry) = cat
14499 .tables
14500 .iter_mut()
14501 .find(|table| table.table_id == table_id)
14502 {
14503 if entry.name != *new_name || !matches!(entry.state, TableState::Live) {
14504 entry.name = new_name.clone();
14505 entry.state = TableState::Live;
14506 changed = true;
14507 }
14508 }
14509 }
14510 Op::Ddl(DdlOp::ReplaceBuildingTable {
14511 table_id,
14512 replaced_table_id,
14513 ref new_name,
14514 }) => {
14515 changed |=
14516 apply_rebuilding_publish(cat, table_id, replaced_table_id, new_name, ce)?;
14517 }
14518 Op::Ddl(DdlOp::RenameTable {
14519 table_id,
14520 ref new_name,
14521 }) => {
14522 let mut old_name = None;
14523 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
14524 if entry.name != *new_name {
14525 old_name = Some(entry.name.clone());
14526 entry.name = new_name.clone();
14527 changed = true;
14528 }
14529 }
14530 if let Some(old_name) = old_name {
14531 if let Some(definition) = cat
14532 .materialized_views
14533 .iter_mut()
14534 .find(|definition| definition.name == old_name)
14535 {
14536 definition.name = new_name.clone();
14537 }
14538 for table in &mut cat.security.rls_tables {
14539 if *table == old_name {
14540 *table = new_name.clone();
14541 }
14542 }
14543 for policy in &mut cat.security.policies {
14544 if policy.table == old_name {
14545 policy.table = new_name.clone();
14546 }
14547 }
14548 for mask in &mut cat.security.masks {
14549 if mask.table == old_name {
14550 mask.table = new_name.clone();
14551 }
14552 }
14553 for role in &mut cat.roles {
14554 for permission in &mut role.permissions {
14555 rename_permission_table(permission, &old_name, new_name);
14556 }
14557 }
14558 if !catalog_snapshot_txns.contains(&txn_id) {
14559 advance_security_version(cat)?;
14560 }
14561 }
14562 }
14566 Op::Ddl(DdlOp::AlterTable {
14567 table_id,
14568 ref column_json,
14569 }) => {
14570 let column = DdlOp::decode_column(column_json)?;
14571 let mut renamed = None;
14572 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
14573 renamed = entry
14574 .schema
14575 .columns
14576 .iter()
14577 .find(|existing| existing.id == column.id && existing.name != column.name)
14578 .map(|existing| {
14579 (
14580 entry.name.clone(),
14581 existing.name.clone(),
14582 column.name.clone(),
14583 )
14584 });
14585 if apply_recovered_column_def(&mut entry.schema, column)? {
14586 validate_recovered_schema(&entry.schema)?;
14587 changed = true;
14588 }
14589 }
14590 if let Some((table, old_name, new_name)) = renamed {
14591 for role in &mut cat.roles {
14592 for permission in &mut role.permissions {
14593 rename_permission_column(permission, &table, &old_name, &new_name);
14594 }
14595 }
14596 if !catalog_snapshot_txns.contains(&txn_id) {
14597 advance_security_version(cat)?;
14598 }
14599 }
14600 }
14601 Op::Ddl(DdlOp::SetTtl {
14602 table_id,
14603 ref policy_json,
14604 }) => {
14605 let policy = DdlOp::decode_ttl(policy_json)?;
14606 let entry = cat
14607 .tables
14608 .iter()
14609 .find(|entry| entry.table_id == table_id)
14610 .ok_or_else(|| {
14611 MongrelError::Schema(format!(
14612 "recovered TTL references unknown table id {table_id}"
14613 ))
14614 })?;
14615 if let Some(policy) = policy {
14616 let valid = entry
14617 .schema
14618 .columns
14619 .iter()
14620 .find(|column| column.id == policy.column_id)
14621 .is_some_and(|column| {
14622 column.ty == TypeId::TimestampNanos
14623 && policy.duration_nanos > 0
14624 && policy.duration_nanos <= i64::MAX as u64
14625 });
14626 if !valid {
14627 return Err(MongrelError::Schema(format!(
14628 "invalid recovered TTL policy for table id {table_id}"
14629 )));
14630 }
14631 }
14632 ttl_updates.insert(table_id, (policy, ce));
14633 }
14634 Op::Ddl(DdlOp::SetMaterializedView {
14635 ref name,
14636 ref definition_json,
14637 }) => {
14638 let definition = DdlOp::decode_materialized_view(definition_json)?;
14639 if definition.name != *name {
14640 return Err(MongrelError::Schema(format!(
14641 "materialized view WAL name mismatch: {name:?}"
14642 )));
14643 }
14644 if cat.live(name).is_some() {
14645 if let Some(existing) = cat
14646 .materialized_views
14647 .iter_mut()
14648 .find(|existing| existing.name == *name)
14649 {
14650 if *existing != definition {
14651 *existing = definition;
14652 changed = true;
14653 }
14654 } else {
14655 cat.materialized_views.push(definition);
14656 changed = true;
14657 }
14658 }
14659 }
14660 Op::Ddl(DdlOp::SetSecurityCatalog { ref security_json }) => {
14661 let security = DdlOp::decode_security(security_json)?;
14662 validate_security_catalog(cat, &security)?;
14663 if cat.security != security {
14664 cat.security = security;
14665 if !catalog_snapshot_txns.contains(&txn_id) {
14666 advance_security_version(cat)?;
14667 }
14668 changed = true;
14669 }
14670 }
14671 Op::Ddl(DdlOp::SetSqlPragma { ref key, value }) => {
14672 let target = match key.as_str() {
14673 "user_version" => &mut cat.user_version,
14674 "application_id" => &mut cat.application_id,
14675 _ => {
14676 return Err(MongrelError::InvalidArgument(format!(
14677 "unsupported recovered SQL pragma {key:?}"
14678 )))
14679 }
14680 };
14681 if *target != Some(value) {
14682 *target = Some(value);
14683 cat.db_epoch = cat.db_epoch.max(ce);
14684 changed = true;
14685 }
14686 }
14687 Op::Ddl(DdlOp::CatalogSnapshot { ref catalog_json }) => {
14688 if ce <= applied_catalog_epoch {
14689 continue;
14690 }
14691 let snapshot = DdlOp::decode_catalog(catalog_json)?;
14692 if snapshot.db_epoch != ce {
14693 return Err(MongrelError::Schema(format!(
14694 "catalog snapshot epoch {} does not match WAL commit epoch {ce}",
14695 snapshot.db_epoch
14696 )));
14697 }
14698 validate_recovered_catalog(&snapshot)?;
14699 validate_catalog_transition(cat, &snapshot)?;
14700 *cat = snapshot;
14701 applied_catalog_epoch = ce;
14702 changed = true;
14703 }
14704 _ => {}
14705 }
14706 }
14707
14708 if cat.db_epoch < max_committed_epoch {
14709 cat.db_epoch = max_committed_epoch;
14710 changed = true;
14711 }
14712 changed |= repair_catalog_allocator_counters(cat)?;
14713
14714 validate_recovered_catalog(cat)?;
14715 let storage_reconciliation = validate_recovered_storage_plan(
14716 root,
14717 durable_root,
14718 cat,
14719 &created_table_ids,
14720 &ttl_updates,
14721 meta_dek,
14722 )?;
14723
14724 let needs_storage_apply = !storage_reconciliation.is_empty() || !ttl_updates.is_empty();
14725 if apply && (changed || needs_storage_apply) {
14726 for table_id in storage_reconciliation {
14727 let entry = cat
14728 .tables
14729 .iter()
14730 .find(|entry| entry.table_id == table_id)
14731 .ok_or_else(|| MongrelError::CorruptWal {
14732 offset: table_id,
14733 reason: "recovery storage plan lost its catalog table".into(),
14734 })?;
14735 ensure_recovered_table_storage(
14736 table_roots
14737 .and_then(|roots| roots.get(&table_id))
14738 .map(Arc::as_ref),
14739 durable_root,
14740 &root.join(TABLES_DIR).join(table_id.to_string()),
14741 table_id,
14742 &entry.schema,
14743 meta_dek,
14744 )?;
14745 }
14746 for (table_id, (policy, ttl_epoch)) in ttl_updates {
14747 let Some(entry) = cat.tables.iter().find(|entry| {
14748 entry.table_id == table_id
14749 && matches!(entry.state, TableState::Live | TableState::Building { .. })
14750 }) else {
14751 continue;
14752 };
14753 let table_root = if let Some(root) = table_roots.and_then(|roots| roots.get(&table_id))
14754 {
14755 root.try_clone()?
14756 } else if let Some(root) = durable_root {
14757 root.open_directory(Path::new(TABLES_DIR).join(table_id.to_string()))?
14758 } else {
14759 crate::durable_file::DurableRoot::open(
14760 root.join(TABLES_DIR).join(table_id.to_string()),
14761 )?
14762 };
14763 let table_dir = table_root.io_path()?;
14764 let mut manifest = crate::manifest::read_durable(&table_root, "", meta_dek)?;
14765 if manifest.ttl != policy || manifest.current_epoch < ttl_epoch {
14766 manifest.ttl = policy;
14767 manifest.current_epoch = manifest.current_epoch.max(ttl_epoch);
14768 manifest.schema_id = entry.schema.schema_id;
14769 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
14770 }
14771 }
14772 if changed {
14773 match durable_root {
14774 Some(root) => catalog::write_durable(root, cat, meta_dek)?,
14775 None => catalog::write_atomic(root, cat, meta_dek)?,
14776 }
14777 }
14778 }
14779 *target_catalog = recovered_catalog;
14780 Ok(())
14781}
14782
14783fn ensure_recovered_table_storage(
14784 pinned_table: Option<&crate::durable_file::DurableRoot>,
14785 durable_root: Option<&crate::durable_file::DurableRoot>,
14786 fallback_table_dir: &Path,
14787 table_id: u64,
14788 schema: &Schema,
14789 meta_dek: Option<&[u8; META_DEK_LEN]>,
14790) -> Result<()> {
14791 let table_root = if let Some(root) = pinned_table {
14792 root.try_clone()?
14793 } else if let Some(root) = durable_root {
14794 let relative = Path::new(TABLES_DIR).join(table_id.to_string());
14795 match root.open_directory(&relative) {
14796 Ok(table) => table,
14797 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
14798 root.create_directory_all_pinned(relative)?
14799 }
14800 Err(error) => return Err(error.into()),
14801 }
14802 } else {
14803 crate::durable_file::create_directory_all(fallback_table_dir)?;
14804 crate::durable_file::DurableRoot::open(fallback_table_dir)?
14805 };
14806 let table_dir = table_root.io_path()?;
14807 let mut existing_manifest = match crate::manifest::read_durable(&table_root, "", meta_dek) {
14808 Ok(manifest) => {
14809 if manifest.table_id != table_id {
14810 return Err(MongrelError::Conflict(format!(
14811 "recovered table directory id mismatch: expected {table_id}, found {}",
14812 manifest.table_id
14813 )));
14814 }
14815 Some(manifest)
14816 }
14817 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
14818 Err(error) => return Err(error),
14819 };
14820
14821 table_root.create_directory_all(crate::engine::WAL_DIR)?;
14822 table_root.create_directory_all(crate::engine::RUNS_DIR)?;
14823 crate::engine::write_schema(&table_dir, schema)?;
14824
14825 if let Some(mut manifest) = existing_manifest.take() {
14826 if manifest.schema_id != schema.schema_id {
14827 manifest.schema_id = schema.schema_id;
14828 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
14829 }
14830 } else {
14831 let mut manifest = crate::manifest::Manifest::new(table_id, schema.schema_id);
14833 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
14834 }
14835 Ok(())
14836}
14837
14838fn validate_recovered_schema(schema: &Schema) -> Result<()> {
14839 schema.validate_auto_increment()?;
14840 schema.validate_defaults()?;
14841 schema.validate_ai()?;
14842 let mut column_ids = HashSet::new();
14843 let mut column_names = HashSet::new();
14844 for column in &schema.columns {
14845 if !column_ids.insert(column.id) || !column_names.insert(column.name.as_str()) {
14846 return Err(MongrelError::Schema(
14847 "recovered schema contains duplicate columns".into(),
14848 ));
14849 }
14850 match &column.ty {
14851 TypeId::Decimal128 { precision, scale }
14852 if *precision == 0 || *precision > 38 || scale.unsigned_abs() > *precision =>
14853 {
14854 return Err(MongrelError::Schema(format!(
14855 "column {:?} has invalid decimal precision or scale",
14856 column.name
14857 )));
14858 }
14859 TypeId::Enum { variants }
14860 if variants.is_empty()
14861 || variants.iter().any(String::is_empty)
14862 || variants.iter().collect::<HashSet<_>>().len() != variants.len() =>
14863 {
14864 return Err(MongrelError::Schema(format!(
14865 "column {:?} has invalid enum variants",
14866 column.name
14867 )));
14868 }
14869 _ => {}
14870 }
14871 }
14872 let mut index_names = HashSet::new();
14873 for index in &schema.indexes {
14874 index.validate_options()?;
14875 if index.name.is_empty()
14876 || !index_names.insert(index.name.as_str())
14877 || schema
14878 .columns
14879 .iter()
14880 .all(|column| column.id != index.column_id)
14881 {
14882 return Err(MongrelError::Schema(format!(
14883 "recovered index {:?} references missing column {}",
14884 index.name, index.column_id
14885 )));
14886 }
14887 }
14888 let mut colocated = HashSet::new();
14889 for group in &schema.colocation {
14890 if group.is_empty()
14891 || group.iter().any(|id| !column_ids.contains(id))
14892 || group.iter().any(|id| !colocated.insert(*id))
14893 {
14894 return Err(MongrelError::Schema(
14895 "recovered schema contains invalid column co-location groups".into(),
14896 ));
14897 }
14898 }
14899
14900 let mut constraint_ids = HashSet::new();
14901 let mut constraint_names = HashSet::<String>::new();
14902 let mut validate_constraint_identity = |id: u16, name: &str| -> Result<()> {
14903 if name.is_empty()
14904 || !constraint_ids.insert(id)
14905 || !constraint_names.insert(name.to_owned())
14906 {
14907 return Err(MongrelError::Schema(
14908 "recovered schema contains duplicate or empty constraint identities".into(),
14909 ));
14910 }
14911 Ok(())
14912 };
14913 for unique in &schema.constraints.uniques {
14914 validate_constraint_identity(unique.id, &unique.name)?;
14915 if unique.columns.is_empty()
14916 || unique.columns.iter().any(|id| !column_ids.contains(id))
14917 || unique.columns.iter().collect::<HashSet<_>>().len() != unique.columns.len()
14918 {
14919 return Err(MongrelError::Schema(format!(
14920 "unique constraint {:?} has invalid columns",
14921 unique.name
14922 )));
14923 }
14924 }
14925 for foreign_key in &schema.constraints.foreign_keys {
14926 validate_constraint_identity(foreign_key.id, &foreign_key.name)?;
14927 if foreign_key.ref_table.is_empty()
14928 || foreign_key.columns.is_empty()
14929 || foreign_key.columns.len() != foreign_key.ref_columns.len()
14930 || foreign_key
14931 .columns
14932 .iter()
14933 .any(|id| !column_ids.contains(id))
14934 || foreign_key.columns.iter().collect::<HashSet<_>>().len() != foreign_key.columns.len()
14935 || foreign_key.ref_columns.iter().collect::<HashSet<_>>().len()
14936 != foreign_key.ref_columns.len()
14937 {
14938 return Err(MongrelError::Schema(format!(
14939 "foreign key {:?} has invalid columns",
14940 foreign_key.name
14941 )));
14942 }
14943 if (matches!(foreign_key.on_delete, crate::constraint::FkAction::SetNull)
14944 || matches!(foreign_key.on_update, crate::constraint::FkAction::SetNull))
14945 && foreign_key.columns.iter().any(|id| {
14946 schema
14947 .columns
14948 .iter()
14949 .find(|column| column.id == *id)
14950 .is_none_or(|column| {
14951 !column.flags.contains(crate::schema::ColumnFlags::NULLABLE)
14952 })
14953 })
14954 {
14955 return Err(MongrelError::Schema(format!(
14956 "foreign key {:?} uses SET NULL on a non-nullable column",
14957 foreign_key.name
14958 )));
14959 }
14960 }
14961 for check in &schema.constraints.checks {
14962 validate_constraint_identity(check.id, &check.name)?;
14963 check.expr.validate()?;
14964 validate_check_columns(&check.expr, &column_ids)?;
14965 }
14966 Ok(())
14967}
14968
14969fn validate_check_columns(
14970 expression: &crate::constraint::CheckExpr,
14971 column_ids: &HashSet<u16>,
14972) -> Result<()> {
14973 use crate::constraint::CheckExpr;
14974 match expression {
14975 CheckExpr::Col(id) | CheckExpr::IsNull(id) | CheckExpr::IsNotNull(id) => {
14976 if column_ids.contains(id) {
14977 Ok(())
14978 } else {
14979 Err(MongrelError::Schema(format!(
14980 "check constraint references unknown column {id}"
14981 )))
14982 }
14983 }
14984 CheckExpr::Regex { col, .. } => {
14985 if column_ids.contains(col) {
14986 Ok(())
14987 } else {
14988 Err(MongrelError::Schema(format!(
14989 "check constraint references unknown column {col}"
14990 )))
14991 }
14992 }
14993 CheckExpr::Add(left, right)
14994 | CheckExpr::Sub(left, right)
14995 | CheckExpr::Mul(left, right)
14996 | CheckExpr::Div(left, right)
14997 | CheckExpr::Mod(left, right)
14998 | CheckExpr::Eq(left, right)
14999 | CheckExpr::Ne(left, right)
15000 | CheckExpr::Lt(left, right)
15001 | CheckExpr::Le(left, right)
15002 | CheckExpr::Gt(left, right)
15003 | CheckExpr::Ge(left, right)
15004 | CheckExpr::And(left, right)
15005 | CheckExpr::Or(left, right) => {
15006 validate_check_columns(left, column_ids)?;
15007 validate_check_columns(right, column_ids)
15008 }
15009 CheckExpr::Not(inner) => validate_check_columns(inner, column_ids),
15010 CheckExpr::True | CheckExpr::Lit(_) => Ok(()),
15011 }
15012}
15013
15014fn validate_catalog_transition(current: &Catalog, next: &Catalog) -> Result<()> {
15015 for (name, prior, candidate) in [
15016 ("db_epoch", current.db_epoch, next.db_epoch),
15017 ("next_table_id", current.next_table_id, next.next_table_id),
15018 (
15019 "next_segment_no",
15020 current.next_segment_no,
15021 next.next_segment_no,
15022 ),
15023 ("next_user_id", current.next_user_id, next.next_user_id),
15024 (
15025 "security_version",
15026 current.security_version,
15027 next.security_version,
15028 ),
15029 ] {
15030 if candidate < prior {
15031 return Err(MongrelError::Schema(format!(
15032 "catalog snapshot rolls back {name} from {prior} to {candidate}"
15033 )));
15034 }
15035 }
15036 for prior in ¤t.tables {
15037 let Some(candidate) = next
15038 .tables
15039 .iter()
15040 .find(|entry| entry.table_id == prior.table_id)
15041 else {
15042 return Err(MongrelError::Schema(format!(
15043 "catalog snapshot removes table identity {}",
15044 prior.table_id
15045 )));
15046 };
15047 if candidate.created_epoch != prior.created_epoch
15048 || candidate.schema.schema_id < prior.schema.schema_id
15049 || matches!(prior.state, TableState::Dropped { .. })
15050 && !matches!(candidate.state, TableState::Dropped { .. })
15051 {
15052 return Err(MongrelError::Schema(format!(
15053 "catalog snapshot rolls back table identity {}",
15054 prior.table_id
15055 )));
15056 }
15057 }
15058 for prior in ¤t.users {
15059 if let Some(candidate) = next.users.iter().find(|user| user.id == prior.id) {
15060 if candidate.username != prior.username
15061 || candidate.created_epoch != prior.created_epoch
15062 {
15063 return Err(MongrelError::Schema(format!(
15064 "catalog snapshot reuses user identity {}",
15065 prior.id
15066 )));
15067 }
15068 }
15069 }
15070 Ok(())
15071}
15072
15073fn validate_recovered_catalog(catalog: &Catalog) -> Result<()> {
15074 let mut table_ids = HashSet::new();
15075 let mut active_names = HashSet::new();
15076 let mut max_table_id = None::<u64>;
15077 for entry in &catalog.tables {
15078 if !table_ids.insert(entry.table_id) {
15079 return Err(MongrelError::Schema(format!(
15080 "catalog contains duplicate table id {}",
15081 entry.table_id
15082 )));
15083 }
15084 max_table_id = Some(max_table_id.map_or(entry.table_id, |value| value.max(entry.table_id)));
15085 if entry.name.is_empty() || entry.created_epoch > catalog.db_epoch {
15086 return Err(MongrelError::Schema(format!(
15087 "catalog table {} has invalid name or creation epoch",
15088 entry.table_id
15089 )));
15090 }
15091 validate_recovered_schema(&entry.schema)?;
15092 match &entry.state {
15093 TableState::Live => {
15094 if !active_names.insert(entry.name.as_str()) {
15095 return Err(MongrelError::Schema(format!(
15096 "catalog contains duplicate active table name {:?}",
15097 entry.name
15098 )));
15099 }
15100 }
15101 TableState::Dropped { at_epoch } => {
15102 if *at_epoch < entry.created_epoch || *at_epoch > catalog.db_epoch {
15103 return Err(MongrelError::Schema(format!(
15104 "catalog table {} has invalid drop epoch {at_epoch}",
15105 entry.table_id
15106 )));
15107 }
15108 }
15109 TableState::Building {
15110 intended_name,
15111 query_id,
15112 replaces_table_id,
15113 ..
15114 } => {
15115 if intended_name.is_empty() || query_id.is_empty() {
15116 return Err(MongrelError::Schema(format!(
15117 "building table {} has empty identity fields",
15118 entry.table_id
15119 )));
15120 }
15121 if !active_names.insert(entry.name.as_str()) {
15122 return Err(MongrelError::Schema(format!(
15123 "catalog contains duplicate active/building table name {:?}",
15124 entry.name
15125 )));
15126 }
15127 if replaces_table_id.is_some_and(|id| id == entry.table_id) {
15128 return Err(MongrelError::Schema(
15129 "building table cannot replace itself".into(),
15130 ));
15131 }
15132 }
15133 }
15134 }
15135 if let Some(maximum) = max_table_id {
15136 let required = maximum
15137 .checked_add(1)
15138 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
15139 if catalog.next_table_id < required {
15140 return Err(MongrelError::Schema(format!(
15141 "catalog next_table_id {} precedes required {required}",
15142 catalog.next_table_id
15143 )));
15144 }
15145 }
15146 for entry in &catalog.tables {
15147 if let TableState::Building {
15148 replaces_table_id: Some(replaced),
15149 ..
15150 } = entry.state
15151 {
15152 if !table_ids.contains(&replaced) {
15153 return Err(MongrelError::Schema(format!(
15154 "building table {} replaces unknown table {replaced}",
15155 entry.table_id
15156 )));
15157 }
15158 }
15159 }
15160 for entry in &catalog.tables {
15161 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
15162 validate_foreign_key_targets(catalog, &entry.schema)?;
15163 }
15164 }
15165
15166 let mut external_names = HashSet::new();
15167 for entry in &catalog.external_tables {
15168 entry.validate()?;
15169 validate_recovered_schema(&entry.declared_schema)?;
15170 if !entry.declared_schema.constraints.is_empty() {
15171 return Err(MongrelError::Schema(format!(
15172 "external table {:?} cannot carry engine-enforced constraints",
15173 entry.name
15174 )));
15175 }
15176 if entry.created_epoch > catalog.db_epoch
15177 || !external_names.insert(entry.name.as_str())
15178 || active_names.contains(entry.name.as_str())
15179 {
15180 return Err(MongrelError::Schema(format!(
15181 "invalid or duplicate external table {:?}",
15182 entry.name
15183 )));
15184 }
15185 }
15186
15187 let mut procedure_names = HashSet::new();
15188 for entry in &catalog.procedures {
15189 entry.procedure.validate()?;
15190 if entry.procedure.created_epoch > entry.procedure.updated_epoch
15191 || entry.procedure.updated_epoch > catalog.db_epoch
15192 || !procedure_names.insert(entry.procedure.name.as_str())
15193 {
15194 return Err(MongrelError::Schema(format!(
15195 "invalid or duplicate procedure {:?}",
15196 entry.procedure.name
15197 )));
15198 }
15199 validate_recovered_procedure_references(catalog, &entry.procedure)?;
15200 }
15201
15202 let mut trigger_names = HashSet::new();
15203 for entry in &catalog.triggers {
15204 entry.trigger.validate()?;
15205 if entry.trigger.created_epoch > entry.trigger.updated_epoch
15206 || entry.trigger.updated_epoch > catalog.db_epoch
15207 || !trigger_names.insert(entry.trigger.name.as_str())
15208 {
15209 return Err(MongrelError::Schema(format!(
15210 "invalid or duplicate trigger {:?}",
15211 entry.trigger.name
15212 )));
15213 }
15214 validate_recovered_trigger_references(catalog, &entry.trigger)?;
15215 }
15216
15217 let mut views = HashSet::new();
15218 for view in &catalog.materialized_views {
15219 let target = catalog.live(&view.name).ok_or_else(|| {
15220 MongrelError::Schema(format!(
15221 "materialized view {:?} has no live table",
15222 view.name
15223 ))
15224 })?;
15225 if view.name.is_empty()
15226 || view.query.trim().is_empty()
15227 || view.last_refresh_epoch > catalog.db_epoch
15228 || !views.insert(view.name.as_str())
15229 {
15230 return Err(MongrelError::Schema(format!(
15231 "materialized view {:?} has no unique live table",
15232 view.name
15233 )));
15234 }
15235 if let Some(incremental) = &view.incremental {
15236 let source = catalog.live(&incremental.source_table).ok_or_else(|| {
15237 MongrelError::Schema(format!(
15238 "materialized view {:?} references missing source {:?}",
15239 view.name, incremental.source_table
15240 ))
15241 })?;
15242 if source.table_id != incremental.source_table_id
15243 || source
15244 .schema
15245 .columns
15246 .iter()
15247 .all(|column| column.id != incremental.group_column)
15248 {
15249 return Err(MongrelError::Schema(format!(
15250 "materialized view {:?} has invalid incremental source",
15251 view.name
15252 )));
15253 }
15254 let target_ids = target
15255 .schema
15256 .columns
15257 .iter()
15258 .map(|column| column.id)
15259 .collect::<HashSet<_>>();
15260 let mut output_ids = HashSet::new();
15261 let count_outputs = incremental
15262 .outputs
15263 .iter()
15264 .filter(|output| {
15265 matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
15266 })
15267 .count();
15268 if incremental.checkpoint_event_id.is_empty()
15269 || !target_ids.contains(&incremental.group_output_column)
15270 || !target_ids.contains(&incremental.count_output_column)
15271 || incremental.outputs.is_empty()
15272 || count_outputs != 1
15273 || incremental.outputs.iter().any(|output| {
15274 !target_ids.contains(&output.output_column)
15275 || output.output_column == incremental.group_output_column
15276 || !output_ids.insert(output.output_column)
15277 || matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
15278 && output.output_column != incremental.count_output_column
15279 || match output.kind {
15280 crate::catalog::IncrementalAggregateKind::Sum { source_column } => {
15281 source
15282 .schema
15283 .columns
15284 .iter()
15285 .all(|column| column.id != source_column)
15286 }
15287 crate::catalog::IncrementalAggregateKind::Count => false,
15288 }
15289 })
15290 {
15291 return Err(MongrelError::Schema(format!(
15292 "materialized view {:?} has invalid incremental outputs",
15293 view.name
15294 )));
15295 }
15296 }
15297 }
15298
15299 validate_security_catalog(catalog, &catalog.security)?;
15300 validate_recovered_auth_catalog(catalog)?;
15301 Ok(())
15302}
15303
15304fn repair_catalog_allocator_counters(catalog: &mut Catalog) -> Result<bool> {
15305 let mut changed = false;
15306 if let Some(maximum) = catalog.tables.iter().map(|entry| entry.table_id).max() {
15307 let required = maximum
15308 .checked_add(1)
15309 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
15310 if catalog.next_table_id < required {
15311 catalog.next_table_id = required;
15312 changed = true;
15313 }
15314 }
15315 if let Some(maximum) = catalog.users.iter().map(|user| user.id).max() {
15316 let required = maximum
15317 .checked_add(1)
15318 .ok_or_else(|| MongrelError::Full("user id namespace exhausted".into()))?;
15319 if catalog.next_user_id < required {
15320 catalog.next_user_id = required;
15321 changed = true;
15322 }
15323 }
15324 Ok(changed)
15325}
15326
15327fn validate_foreign_key_targets(catalog: &Catalog, schema: &Schema) -> Result<()> {
15328 for foreign_key in &schema.constraints.foreign_keys {
15329 let parent = catalog.live(&foreign_key.ref_table).ok_or_else(|| {
15330 MongrelError::Schema(format!(
15331 "foreign key {:?} references unknown live table {:?}",
15332 foreign_key.name, foreign_key.ref_table
15333 ))
15334 })?;
15335 let referenced_unique = parent
15336 .schema
15337 .constraints
15338 .uniques
15339 .iter()
15340 .any(|unique| unique.columns == foreign_key.ref_columns)
15341 || foreign_key.ref_columns.len() == 1
15342 && parent
15343 .schema
15344 .primary_key()
15345 .is_some_and(|column| column.id == foreign_key.ref_columns[0]);
15346 if !referenced_unique {
15347 return Err(MongrelError::Schema(format!(
15348 "foreign key {:?} does not reference a unique key",
15349 foreign_key.name
15350 )));
15351 }
15352 for (local_id, parent_id) in foreign_key.columns.iter().zip(&foreign_key.ref_columns) {
15353 let local = schema.columns.iter().find(|column| column.id == *local_id);
15354 let referenced = parent
15355 .schema
15356 .columns
15357 .iter()
15358 .find(|column| column.id == *parent_id);
15359 if local
15360 .zip(referenced)
15361 .is_none_or(|(local, referenced)| local.ty != referenced.ty)
15362 {
15363 return Err(MongrelError::Schema(format!(
15364 "foreign key {:?} has missing or incompatible columns",
15365 foreign_key.name
15366 )));
15367 }
15368 }
15369 }
15370 Ok(())
15371}
15372
15373fn validate_recovered_procedure_references(
15374 catalog: &Catalog,
15375 procedure: &StoredProcedure,
15376) -> Result<()> {
15377 for step in &procedure.body.steps {
15378 let Some(table_name) = step.table() else {
15379 continue;
15380 };
15381 let schema = &catalog
15382 .live(table_name)
15383 .ok_or_else(|| {
15384 MongrelError::Schema(format!(
15385 "procedure {:?} references unknown table {table_name:?}",
15386 procedure.name
15387 ))
15388 })?
15389 .schema;
15390 match step {
15391 ProcedureStep::NativeQuery {
15392 conditions,
15393 projection,
15394 ..
15395 } => {
15396 for condition in conditions {
15397 validate_condition_columns(condition, schema)?;
15398 }
15399 for id in projection.iter().flatten() {
15400 validate_column_id(*id, schema)?;
15401 }
15402 }
15403 ProcedureStep::Put { cells, .. } => {
15404 for cell in cells {
15405 validate_column_id(cell.column_id, schema)?;
15406 }
15407 }
15408 ProcedureStep::Upsert {
15409 cells,
15410 update_cells,
15411 ..
15412 } => {
15413 for cell in cells.iter().chain(update_cells.iter().flatten()) {
15414 validate_column_id(cell.column_id, schema)?;
15415 }
15416 }
15417 ProcedureStep::DeleteByPk { .. } if schema.primary_key().is_none() => {
15418 return Err(MongrelError::Schema(format!(
15419 "procedure {:?} deletes by primary key on table without one",
15420 procedure.name
15421 )));
15422 }
15423 ProcedureStep::DeleteByPk { .. }
15424 | ProcedureStep::DeleteRows { .. }
15425 | ProcedureStep::SqlQuery { .. } => {}
15426 }
15427 }
15428 Ok(())
15429}
15430
15431fn validate_recovered_trigger_references(catalog: &Catalog, trigger: &StoredTrigger) -> Result<()> {
15432 let target_schema = match &trigger.target {
15433 TriggerTarget::Table(name) => catalog
15434 .live(name)
15435 .ok_or_else(|| {
15436 MongrelError::Schema(format!(
15437 "trigger {:?} references unknown table {name:?}",
15438 trigger.name
15439 ))
15440 })?
15441 .schema
15442 .clone(),
15443 TriggerTarget::View(_) => Schema {
15444 columns: trigger.target_columns.clone(),
15445 ..Schema::default()
15446 },
15447 };
15448 for column in &trigger.update_of {
15449 if target_schema.column(column).is_none() {
15450 return Err(MongrelError::Schema(format!(
15451 "trigger {:?} references unknown UPDATE OF column {column:?}",
15452 trigger.name
15453 )));
15454 }
15455 }
15456 if let Some(expr) = &trigger.when {
15457 validate_trigger_expr(expr, &target_schema, trigger.event)?;
15458 }
15459 let mut selects = HashMap::new();
15460 for step in &trigger.program.steps {
15461 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before {
15462 return Err(MongrelError::Schema(
15463 "SetNew is only valid in BEFORE triggers".into(),
15464 ));
15465 }
15466 validate_trigger_step(step, catalog, &target_schema, trigger.event, &mut selects)?;
15467 }
15468 Ok(())
15469}
15470
15471fn validate_recovered_auth_catalog(catalog: &Catalog) -> Result<()> {
15472 let mut role_names = HashSet::new();
15473 for role in &catalog.roles {
15474 if role.name.is_empty()
15475 || role.created_epoch > catalog.db_epoch
15476 || !role_names.insert(role.name.as_str())
15477 {
15478 return Err(MongrelError::Schema(format!(
15479 "invalid or duplicate role {:?}",
15480 role.name
15481 )));
15482 }
15483 for permission in &role.permissions {
15484 if let Some(table) = permission_table(permission) {
15485 let schema = catalog
15486 .live(table)
15487 .map(|entry| &entry.schema)
15488 .or_else(|| {
15489 catalog
15490 .external_tables
15491 .iter()
15492 .find(|entry| entry.name == table)
15493 .map(|entry| &entry.declared_schema)
15494 })
15495 .ok_or_else(|| {
15496 MongrelError::Schema(format!(
15497 "role {:?} references unknown table {table:?}",
15498 role.name
15499 ))
15500 })?;
15501 let columns = match permission {
15502 crate::auth::Permission::SelectColumns { columns, .. }
15503 | crate::auth::Permission::InsertColumns { columns, .. }
15504 | crate::auth::Permission::UpdateColumns { columns, .. } => Some(columns),
15505 _ => None,
15506 };
15507 if columns.is_some_and(|columns| {
15508 columns.is_empty()
15509 || columns.iter().any(|column| schema.column(column).is_none())
15510 }) {
15511 return Err(MongrelError::Schema(format!(
15512 "role {:?} contains invalid column permissions",
15513 role.name
15514 )));
15515 }
15516 }
15517 }
15518 }
15519 let mut user_ids = HashSet::new();
15520 let mut usernames = HashSet::new();
15521 let mut maximum_user_id = 0;
15522 for user in &catalog.users {
15523 maximum_user_id = maximum_user_id.max(user.id);
15524 if user.id == 0
15525 || user.username.is_empty()
15526 || user.password_hash.is_empty()
15527 || user.created_epoch > catalog.db_epoch
15528 || !user_ids.insert(user.id)
15529 || !usernames.insert(user.username.as_str())
15530 || user
15531 .roles
15532 .iter()
15533 .any(|role| !role_names.contains(role.as_str()))
15534 {
15535 return Err(MongrelError::Schema(format!(
15536 "invalid or duplicate user {:?}",
15537 user.username
15538 )));
15539 }
15540 }
15541 if !catalog.users.is_empty() && catalog.next_user_id <= maximum_user_id {
15542 return Err(MongrelError::Schema(
15543 "catalog next_user_id does not advance beyond existing user ids".into(),
15544 ));
15545 }
15546 if catalog.require_auth && !catalog.users.iter().any(|user| user.is_admin) {
15547 return Err(MongrelError::Schema(
15548 "authenticated catalog has no administrator".into(),
15549 ));
15550 }
15551 Ok(())
15552}
15553
15554fn validate_recovered_storage_plan(
15555 root: &Path,
15556 durable_root: Option<&crate::durable_file::DurableRoot>,
15557 catalog: &Catalog,
15558 created_table_ids: &HashSet<u64>,
15559 ttl_updates: &HashMap<u64, (Option<crate::manifest::TtlPolicy>, u64)>,
15560 meta_dek: Option<&[u8; META_DEK_LEN]>,
15561) -> Result<Vec<u64>> {
15562 const MAX_SCHEMA_BYTES: u64 = 16 * 1024 * 1024;
15563 let mut reconcile = Vec::new();
15564 for entry in &catalog.tables {
15565 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
15566 continue;
15567 }
15568 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
15569 let table_dir = root.join(TABLES_DIR).join(entry.table_id.to_string());
15570 let table_exists = match durable_root {
15571 Some(root) => match root.open_directory(&relative_dir) {
15572 Ok(_) => true,
15573 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
15574 Err(error) => return Err(error.into()),
15575 },
15576 None => table_dir.is_dir(),
15577 };
15578 if !table_exists {
15579 if created_table_ids.contains(&entry.table_id) {
15580 reconcile.push(entry.table_id);
15581 continue;
15582 }
15583 return Err(MongrelError::NotFound(format!(
15584 "catalog table {} storage is missing",
15585 entry.table_id
15586 )));
15587 }
15588 let manifest_result = match durable_root {
15589 Some(root) => crate::manifest::read_durable(root, &relative_dir, meta_dek),
15590 None => crate::manifest::read(&table_dir, meta_dek),
15591 };
15592 let manifest = match manifest_result {
15593 Ok(manifest) => manifest,
15594 Err(MongrelError::Io(error))
15595 if created_table_ids.contains(&entry.table_id)
15596 && error.kind() == std::io::ErrorKind::NotFound =>
15597 {
15598 reconcile.push(entry.table_id);
15599 continue;
15600 }
15601 Err(error) => return Err(error),
15602 };
15603 if manifest.table_id != entry.table_id {
15604 return Err(MongrelError::Conflict(format!(
15605 "catalog table {} storage identity mismatch",
15606 entry.table_id
15607 )));
15608 }
15609 let schema_result = match durable_root {
15610 Some(root) => root
15611 .open_regular(relative_dir.join(crate::engine::SCHEMA_FILENAME))
15612 .map_err(MongrelError::from),
15613 None => crate::durable_file::open_regular_nofollow(
15614 &table_dir.join(crate::engine::SCHEMA_FILENAME),
15615 ),
15616 };
15617 let file = match schema_result {
15618 Ok(file) => file,
15619 Err(MongrelError::Io(error))
15620 if created_table_ids.contains(&entry.table_id)
15621 && error.kind() == std::io::ErrorKind::NotFound =>
15622 {
15623 reconcile.push(entry.table_id);
15624 continue;
15625 }
15626 Err(error) => return Err(error),
15627 };
15628 let length = file.metadata()?.len();
15629 if length > MAX_SCHEMA_BYTES {
15630 return Err(MongrelError::ResourceLimitExceeded {
15631 resource: "recovered schema bytes",
15632 requested: usize::try_from(length).unwrap_or(usize::MAX),
15633 limit: MAX_SCHEMA_BYTES as usize,
15634 });
15635 }
15636 let disk_schema: Schema = serde_json::from_reader(file.take(MAX_SCHEMA_BYTES + 1))
15637 .map_err(|error| MongrelError::Schema(format!("decode recovered schema: {error}")))?;
15638 if manifest.schema_id != entry.schema.schema_id
15639 || crate::wal::DdlOp::encode_schema(&disk_schema)?
15640 != crate::wal::DdlOp::encode_schema(&entry.schema)?
15641 {
15642 reconcile.push(entry.table_id);
15643 }
15644 }
15645 for table_id in ttl_updates.keys() {
15646 if !catalog.tables.iter().any(|entry| {
15647 entry.table_id == *table_id
15648 && matches!(entry.state, TableState::Live | TableState::Building { .. })
15649 }) {
15650 continue;
15651 }
15652 let relative_dir = Path::new(TABLES_DIR).join(table_id.to_string());
15653 let table_exists = match durable_root {
15654 Some(root) => match root.open_directory(&relative_dir) {
15655 Ok(_) => true,
15656 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
15657 Err(error) => return Err(error.into()),
15658 },
15659 None => root.join(&relative_dir).is_dir(),
15660 };
15661 if !table_exists && !created_table_ids.contains(table_id) {
15662 return Err(MongrelError::NotFound(format!(
15663 "TTL recovery table {table_id} storage is missing"
15664 )));
15665 }
15666 }
15667 reconcile.sort_unstable();
15668 reconcile.dedup();
15669 Ok(reconcile)
15670}
15671
15672fn validate_catalog_table_storage(
15673 root: &crate::durable_file::DurableRoot,
15674 catalog: &Catalog,
15675 meta_dek: Option<&[u8; META_DEK_LEN]>,
15676) -> Result<()> {
15677 for entry in &catalog.tables {
15678 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
15679 continue;
15680 }
15681 let table_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
15682 let manifest = crate::manifest::read_durable(root, &table_dir, meta_dek)?;
15683 if manifest.table_id != entry.table_id || manifest.schema_id != entry.schema.schema_id {
15684 return Err(MongrelError::Conflict(format!(
15685 "catalog table {} storage identity mismatch",
15686 entry.table_id
15687 )));
15688 }
15689 root.open_regular(table_dir.join(crate::engine::SCHEMA_FILENAME))?;
15690 }
15691 Ok(())
15692}
15693
15694fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> Result<bool> {
15695 match schema.columns.iter_mut().find(|c| c.id == column.id) {
15696 Some(existing) if *existing == column => Ok(false),
15697 Some(existing) => {
15698 *existing = column;
15699 schema.schema_id = schema
15700 .schema_id
15701 .checked_add(1)
15702 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
15703 Ok(true)
15704 }
15705 None => {
15706 schema.columns.push(column);
15707 schema.schema_id = schema
15708 .schema_id
15709 .checked_add(1)
15710 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
15711 Ok(true)
15712 }
15713 }
15714}
15715
15716fn permission_table(permission: &crate::auth::Permission) -> Option<&str> {
15717 use crate::auth::Permission;
15718 match permission {
15719 Permission::Select { table }
15720 | Permission::Insert { table }
15721 | Permission::Update { table }
15722 | Permission::Delete { table }
15723 | Permission::SelectColumns { table, .. }
15724 | Permission::InsertColumns { table, .. }
15725 | Permission::UpdateColumns { table, .. } => Some(table),
15726 Permission::All | Permission::Ddl | Permission::Admin => None,
15727 }
15728}
15729
15730fn apply_rebuilding_publish(
15731 catalog: &mut Catalog,
15732 table_id: u64,
15733 replaced_table_id: u64,
15734 new_name: &str,
15735 epoch: u64,
15736) -> Result<bool> {
15737 let already_published = catalog.tables.iter().any(|entry| {
15738 entry.table_id == table_id
15739 && entry.name == new_name
15740 && matches!(entry.state, TableState::Live)
15741 }) && catalog.tables.iter().any(|entry| {
15742 entry.table_id == replaced_table_id && matches!(entry.state, TableState::Dropped { .. })
15743 });
15744 if already_published {
15745 return Ok(false);
15746 }
15747 let schema = catalog
15748 .tables
15749 .iter()
15750 .find(|entry| entry.table_id == table_id)
15751 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?
15752 .schema
15753 .clone();
15754 let replaced = catalog
15755 .tables
15756 .iter_mut()
15757 .find(|entry| entry.table_id == replaced_table_id)
15758 .ok_or_else(|| MongrelError::NotFound(format!("table id {replaced_table_id} not found")))?;
15759 replaced.state = TableState::Dropped { at_epoch: epoch };
15760 let replacement = catalog
15761 .tables
15762 .iter_mut()
15763 .find(|entry| entry.table_id == table_id)
15764 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?;
15765 replacement.name = new_name.to_string();
15766 replacement.state = TableState::Live;
15767
15768 for role in &mut catalog.roles {
15769 role.permissions.retain_mut(|permission| {
15770 retain_rebuilt_permission_columns(permission, new_name, &schema)
15771 });
15772 }
15773 for definition in &mut catalog.materialized_views {
15774 if let Some(incremental) = definition.incremental.as_mut() {
15775 if incremental.source_table == new_name
15776 && incremental.source_table_id == replaced_table_id
15777 {
15778 incremental.source_table_id = table_id;
15779 }
15780 }
15781 }
15782 advance_security_version(catalog)?;
15783 Ok(true)
15784}
15785
15786fn retain_rebuilt_permission_columns(
15787 permission: &mut crate::auth::Permission,
15788 target_table: &str,
15789 schema: &Schema,
15790) -> bool {
15791 use crate::auth::Permission;
15792 let columns = match permission {
15793 Permission::SelectColumns { table, columns }
15794 | Permission::InsertColumns { table, columns }
15795 | Permission::UpdateColumns { table, columns }
15796 if table == target_table =>
15797 {
15798 Some(columns)
15799 }
15800 _ => None,
15801 };
15802 if let Some(columns) = columns {
15803 columns.retain(|column| schema.column(column).is_some());
15804 !columns.is_empty()
15805 } else {
15806 true
15807 }
15808}
15809
15810fn rename_permission_table(permission: &mut crate::auth::Permission, old: &str, new: &str) {
15811 use crate::auth::Permission;
15812 let table = match permission {
15813 Permission::Select { table }
15814 | Permission::Insert { table }
15815 | Permission::Update { table }
15816 | Permission::Delete { table }
15817 | Permission::SelectColumns { table, .. }
15818 | Permission::InsertColumns { table, .. }
15819 | Permission::UpdateColumns { table, .. } => Some(table),
15820 Permission::All | Permission::Ddl | Permission::Admin => None,
15821 };
15822 if let Some(table) = table.filter(|table| table.as_str() == old) {
15823 *table = new.to_string();
15824 }
15825}
15826
15827fn rename_permission_column(
15828 permission: &mut crate::auth::Permission,
15829 target_table: &str,
15830 old: &str,
15831 new: &str,
15832) {
15833 use crate::auth::Permission;
15834 let columns = match permission {
15835 Permission::SelectColumns { table, columns }
15836 | Permission::InsertColumns { table, columns }
15837 | Permission::UpdateColumns { table, columns }
15838 if table == target_table =>
15839 {
15840 Some(columns)
15841 }
15842 _ => None,
15843 };
15844 if let Some(column) = columns
15845 .into_iter()
15846 .flatten()
15847 .find(|column| column.as_str() == old)
15848 {
15849 *column = new.to_string();
15850 }
15851}
15852
15853fn merge_permission(
15854 permissions: &mut Vec<crate::auth::Permission>,
15855 permission: crate::auth::Permission,
15856) {
15857 use crate::auth::Permission;
15858 let (kind, table, mut columns) = match permission {
15859 Permission::SelectColumns { table, columns } => (0, table, columns),
15860 Permission::InsertColumns { table, columns } => (1, table, columns),
15861 Permission::UpdateColumns { table, columns } => (2, table, columns),
15862 permission if !permissions.contains(&permission) => {
15863 permissions.push(permission);
15864 return;
15865 }
15866 _ => return,
15867 };
15868 for permission in permissions.iter_mut() {
15869 let existing = match permission {
15870 Permission::SelectColumns {
15871 table: existing_table,
15872 columns,
15873 } if kind == 0 && existing_table == &table => Some(columns),
15874 Permission::InsertColumns {
15875 table: existing_table,
15876 columns,
15877 } if kind == 1 && existing_table == &table => Some(columns),
15878 Permission::UpdateColumns {
15879 table: existing_table,
15880 columns,
15881 } if kind == 2 && existing_table == &table => Some(columns),
15882 _ => None,
15883 };
15884 if let Some(existing) = existing {
15885 existing.append(&mut columns);
15886 existing.sort();
15887 existing.dedup();
15888 return;
15889 }
15890 }
15891 columns.sort();
15892 columns.dedup();
15893 let permission = if kind == 0 {
15894 Permission::SelectColumns { table, columns }
15895 } else if kind == 1 {
15896 Permission::InsertColumns { table, columns }
15897 } else {
15898 Permission::UpdateColumns { table, columns }
15899 };
15900 permissions.push(permission);
15901}
15902
15903fn revoke_permission_from(
15904 permissions: &mut Vec<crate::auth::Permission>,
15905 revoked: &crate::auth::Permission,
15906) {
15907 use crate::auth::Permission;
15908 let revoked_columns = match revoked {
15909 Permission::SelectColumns { table, columns } => Some((0, table, columns)),
15910 Permission::InsertColumns { table, columns } => Some((1, table, columns)),
15911 Permission::UpdateColumns { table, columns } => Some((2, table, columns)),
15912 _ => None,
15913 };
15914 let Some((kind, table, columns)) = revoked_columns else {
15915 permissions.retain(|permission| permission != revoked);
15916 return;
15917 };
15918 for permission in permissions.iter_mut() {
15919 let current = match permission {
15920 Permission::SelectColumns {
15921 table: current_table,
15922 columns,
15923 } if kind == 0 && current_table == table => Some(columns),
15924 Permission::InsertColumns {
15925 table: current_table,
15926 columns,
15927 } if kind == 1 && current_table == table => Some(columns),
15928 Permission::UpdateColumns {
15929 table: current_table,
15930 columns,
15931 } if kind == 2 && current_table == table => Some(columns),
15932 _ => None,
15933 };
15934 if let Some(current) = current {
15935 current.retain(|column| !columns.contains(column));
15936 }
15937 }
15938 permissions.retain(|permission| match permission {
15939 Permission::SelectColumns { columns, .. }
15940 | Permission::InsertColumns { columns, .. }
15941 | Permission::UpdateColumns { columns, .. } => !columns.is_empty(),
15942 _ => true,
15943 });
15944}
15945
15946fn validate_security_catalog(
15947 catalog: &Catalog,
15948 security: &crate::security::SecurityCatalog,
15949) -> Result<()> {
15950 let mut policy_names = HashSet::new();
15951 for table in &security.rls_tables {
15952 if catalog.live(table).is_none() {
15953 return Err(MongrelError::NotFound(format!(
15954 "RLS table {table:?} not found"
15955 )));
15956 }
15957 }
15958 for policy in &security.policies {
15959 if !policy_names.insert((policy.table.clone(), policy.name.clone())) {
15960 return Err(MongrelError::InvalidArgument(format!(
15961 "duplicate policy {:?} on {:?}",
15962 policy.name, policy.table
15963 )));
15964 }
15965 let schema = &catalog
15966 .live(&policy.table)
15967 .ok_or_else(|| {
15968 MongrelError::NotFound(format!("policy table {:?} not found", policy.table))
15969 })?
15970 .schema;
15971 if let Some(expression) = &policy.using {
15972 validate_security_expression(expression, schema)?;
15973 }
15974 if let Some(expression) = &policy.with_check {
15975 validate_security_expression(expression, schema)?;
15976 }
15977 }
15978 let mut mask_names = HashSet::new();
15979 for mask in &security.masks {
15980 if !mask_names.insert((mask.table.clone(), mask.name.clone())) {
15981 return Err(MongrelError::InvalidArgument(format!(
15982 "duplicate mask {:?} on {:?}",
15983 mask.name, mask.table
15984 )));
15985 }
15986 let column = catalog
15987 .live(&mask.table)
15988 .and_then(|entry| {
15989 entry
15990 .schema
15991 .columns
15992 .iter()
15993 .find(|column| column.id == mask.column)
15994 })
15995 .ok_or_else(|| {
15996 MongrelError::NotFound(format!(
15997 "mask column {} on {:?} not found",
15998 mask.column, mask.table
15999 ))
16000 })?;
16001 if matches!(
16002 mask.strategy,
16003 crate::security::MaskStrategy::Redact { .. } | crate::security::MaskStrategy::Sha256
16004 ) && !matches!(column.ty, TypeId::Bytes | TypeId::Enum { .. })
16005 {
16006 return Err(MongrelError::InvalidArgument(format!(
16007 "mask {:?} requires a string/bytes column",
16008 mask.name
16009 )));
16010 }
16011 }
16012 Ok(())
16013}
16014
16015fn validate_security_expression(
16016 expression: &crate::security::SecurityExpr,
16017 schema: &Schema,
16018) -> Result<()> {
16019 use crate::security::SecurityExpr;
16020 match expression {
16021 SecurityExpr::True => Ok(()),
16022 SecurityExpr::ColumnEqCurrentUser { column }
16023 | SecurityExpr::ColumnEqValue { column, .. } => {
16024 if schema
16025 .columns
16026 .iter()
16027 .any(|candidate| candidate.id == *column)
16028 {
16029 Ok(())
16030 } else {
16031 Err(MongrelError::InvalidArgument(format!(
16032 "security expression references unknown column id {column}"
16033 )))
16034 }
16035 }
16036 SecurityExpr::And { left, right } | SecurityExpr::Or { left, right } => {
16037 validate_security_expression(left, schema)?;
16038 validate_security_expression(right, schema)
16039 }
16040 SecurityExpr::Not { expression } => validate_security_expression(expression, schema),
16041 }
16042}
16043
16044fn sweep_unreferenced_table_dirs(root: &Path, cat: &Catalog) -> Result<()> {
16046 let referenced = cat
16047 .tables
16048 .iter()
16049 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
16050 .map(|entry| entry.table_id)
16051 .collect::<HashSet<_>>();
16052 let tables_dir = root.join(TABLES_DIR);
16053 let entries = match std::fs::read_dir(&tables_dir) {
16054 Ok(entries) => entries,
16055 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
16056 Err(error) => return Err(error.into()),
16057 };
16058 for entry in entries {
16059 let entry = entry?;
16060 if !entry.file_type()?.is_dir() {
16061 continue;
16062 }
16063 let file_name = entry.file_name();
16064 let Some(name) = file_name.to_str() else {
16065 continue;
16066 };
16067 let Ok(table_id) = name.parse::<u64>() else {
16068 continue;
16069 };
16070 if name != table_id.to_string() {
16071 continue;
16072 }
16073 if !referenced.contains(&table_id) {
16074 crate::durable_file::remove_directory_all(&entry.path())?;
16075 }
16076 }
16077 Ok(())
16078}
16079
16080fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
16085 for entry in &cat.tables {
16086 let txn_dir = root
16087 .join(TABLES_DIR)
16088 .join(entry.table_id.to_string())
16089 .join("_txn");
16090 if txn_dir.exists() {
16091 let _ = std::fs::remove_dir_all(&txn_dir);
16092 }
16093 }
16094}
16095
16096#[cfg(test)]
16097mod write_permission_tests {
16098 use super::*;
16099 use crate::txn::Staged;
16100
16101 struct NoopExternalBridge;
16102
16103 impl ExternalTriggerBridge for NoopExternalBridge {
16104 fn apply_trigger_external_write(
16105 &self,
16106 _entry: &ExternalTableEntry,
16107 base_state: Vec<u8>,
16108 _op: ExternalTriggerWrite,
16109 ) -> Result<ExternalTriggerWriteResult> {
16110 Ok(ExternalTriggerWriteResult::new(base_state))
16111 }
16112 }
16113
16114 fn assert_txn_namespace_full<T>(result: Result<T>) {
16115 assert!(matches!(result, Err(MongrelError::Full(_))));
16116 }
16117
16118 #[test]
16119 fn every_begin_api_preserves_transaction_id_exhaustion_without_wal_mutation() {
16120 let directory = tempfile::tempdir().unwrap();
16121 let database = Database::create(directory.path()).unwrap();
16122 let generation = (*database.next_txn_id.lock() >> 32).saturating_add(1);
16123 *database.next_txn_id.lock() = generation << 32;
16124 let before = crate::wal::SharedWal::replay(directory.path())
16125 .unwrap()
16126 .len();
16127 let bridge = NoopExternalBridge;
16128
16129 assert_txn_namespace_full(database.begin().commit());
16130 assert_txn_namespace_full(database.begin_as(None).commit_with_row_ids());
16131 assert_txn_namespace_full(
16132 database
16133 .begin_with_isolation(crate::txn::IsolationLevel::Serializable)
16134 .commit(),
16135 );
16136 assert_txn_namespace_full(
16137 database
16138 .begin_with_external_trigger_bridge(&bridge)
16139 .commit(),
16140 );
16141 assert_txn_namespace_full(
16142 database
16143 .begin_with_external_trigger_bridge_as(&bridge, None)
16144 .commit_controlled(&crate::ExecutionControl::new(None), || Ok(())),
16145 );
16146
16147 assert_eq!(
16148 crate::wal::SharedWal::replay(directory.path())
16149 .unwrap()
16150 .len(),
16151 before
16152 );
16153 drop(database);
16154 Database::open(directory.path()).unwrap();
16155 }
16156
16157 #[test]
16158 fn recovered_storage_identity_mismatch_does_not_mutate_directory() {
16159 let directory = tempfile::tempdir().unwrap();
16160 let table_dir = directory.path().join("7");
16161 crate::durable_file::create_directory_all(&table_dir).unwrap();
16162 let original_schema = test_schema();
16163 crate::engine::write_schema(&table_dir, &original_schema).unwrap();
16164 let mut manifest = crate::manifest::Manifest::new(8, original_schema.schema_id);
16165 crate::manifest::write_atomic(&table_dir, &mut manifest, None).unwrap();
16166 let schema_path = table_dir.join(crate::engine::SCHEMA_FILENAME);
16167 let original_bytes = std::fs::read(&schema_path).unwrap();
16168
16169 let mut replacement_schema = original_schema;
16170 replacement_schema.schema_id += 1;
16171 assert!(matches!(
16172 ensure_recovered_table_storage(None, None, &table_dir, 7, &replacement_schema, None,),
16173 Err(MongrelError::Conflict(_))
16174 ));
16175
16176 assert_eq!(std::fs::read(schema_path).unwrap(), original_bytes);
16177 assert!(!table_dir.join(crate::engine::WAL_DIR).exists());
16178 assert!(!table_dir.join(crate::engine::RUNS_DIR).exists());
16179 assert_eq!(crate::manifest::read(&table_dir, None).unwrap().table_id, 8);
16180 }
16181
16182 #[test]
16183 fn catalog_table_missing_storage_fails_without_recreating_it() {
16184 let directory = tempfile::tempdir().unwrap();
16185 let table_dir = {
16186 let database = Database::create(directory.path()).unwrap();
16187 database.create_table("docs", test_schema()).unwrap();
16188 directory
16189 .path()
16190 .join(TABLES_DIR)
16191 .join(database.table_id("docs").unwrap().to_string())
16192 };
16193 std::fs::remove_dir_all(&table_dir).unwrap();
16194
16195 assert!(matches!(
16196 Database::open(directory.path()),
16197 Err(MongrelError::NotFound(_))
16198 ));
16199 assert!(!table_dir.exists());
16200 }
16201
16202 #[test]
16203 fn authentication_and_principal_resolution_share_one_catalog_snapshot() {
16204 let directory = tempfile::tempdir().unwrap();
16205 let database = std::sync::Arc::new(
16206 Database::create_with_credentials(directory.path(), "admin", "admin-password").unwrap(),
16207 );
16208 database.create_user("alice", "old-password").unwrap();
16209 let old_identity = database.user_identity("alice").unwrap();
16210 let (verified_tx, verified_rx) = std::sync::mpsc::channel();
16211 let (resume_tx, resume_rx) = std::sync::mpsc::channel();
16212 let (mutation_started_tx, mutation_started_rx) = std::sync::mpsc::channel();
16213 let (mutation_done_tx, mutation_done_rx) = std::sync::mpsc::channel();
16214
16215 std::thread::scope(|scope| {
16216 let authenticate = {
16217 let database = std::sync::Arc::clone(&database);
16218 scope.spawn(move || {
16219 database.authenticate_principal_inner("alice", "old-password", || {
16220 verified_tx.send(()).unwrap();
16221 resume_rx.recv().unwrap();
16222 })
16223 })
16224 };
16225 verified_rx.recv().unwrap();
16226 let mutate = {
16227 let database = std::sync::Arc::clone(&database);
16228 scope.spawn(move || {
16229 mutation_started_tx.send(()).unwrap();
16230 database.drop_user("alice").unwrap();
16231 database.create_user("alice", "new-password").unwrap();
16232 mutation_done_tx.send(()).unwrap();
16233 })
16234 };
16235 mutation_started_rx.recv().unwrap();
16236 assert!(mutation_done_rx
16237 .recv_timeout(std::time::Duration::from_millis(50))
16238 .is_err());
16239 resume_tx.send(()).unwrap();
16240 let principal = authenticate.join().unwrap().unwrap().unwrap();
16241 assert_eq!((principal.user_id, principal.created_epoch), old_identity);
16242 mutate.join().unwrap();
16243 });
16244
16245 assert_ne!(database.user_identity("alice").unwrap(), old_identity);
16246 assert!(database
16247 .authenticate_principal("alice", "old-password")
16248 .unwrap()
16249 .is_none());
16250 assert!(database
16251 .authenticate_principal("alice", "new-password")
16252 .unwrap()
16253 .is_some());
16254 }
16255
16256 #[test]
16257 fn homogeneous_batch_summarizes_to_one_permission_decision() {
16258 let staging = (0..10_050)
16259 .map(|_| {
16260 (
16261 7,
16262 Staged::Put(vec![(2, Value::Int64(2)), (1, Value::Int64(1))]),
16263 )
16264 })
16265 .collect::<Vec<_>>();
16266
16267 let needs = summarize_write_permissions(&staging);
16268 let table = needs.get(&7).unwrap();
16269 assert_eq!(needs.len(), 1);
16270 assert!(table.insert);
16271 assert_eq!(table.insert_columns, [1, 2]);
16272 assert!(!table.update);
16273 assert!(!table.delete);
16274 assert!(!table.truncate);
16275 }
16276
16277 #[test]
16278 fn mixed_writes_union_columns_and_preserve_empty_operations() {
16279 let staging = vec![
16280 (7, Staged::Put(vec![(2, Value::Int64(2))])),
16281 (7, Staged::Put(vec![(1, Value::Int64(1))])),
16282 (
16283 7,
16284 Staged::Update {
16285 row_id: RowId(1),
16286 new_row: vec![(1, Value::Int64(1)), (2, Value::Int64(2))],
16287 changed_columns: vec![2],
16288 },
16289 ),
16290 (7, Staged::Delete(RowId(2))),
16291 (8, Staged::Truncate),
16292 ];
16293
16294 let needs = summarize_write_permissions(&staging);
16295 let table = needs.get(&7).unwrap();
16296 assert_eq!(table.insert_columns, [1, 2]);
16297 assert!(table.update);
16298 assert_eq!(table.update_columns, [2]);
16299 assert!(table.delete);
16300 assert!(needs.get(&8).unwrap().truncate);
16301 }
16302
16303 #[test]
16304 fn final_permission_decisions_do_not_scale_with_rows() {
16305 let credentialless_dir = tempfile::tempdir().unwrap();
16306 let credentialless = Database::create(credentialless_dir.path()).unwrap();
16307 credentialless.create_table("docs", test_schema()).unwrap();
16308 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16309 credentialless
16310 .validate_write_permissions(&puts(credentialless.table_id("docs").unwrap()), None, None)
16311 .unwrap();
16312 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 0));
16313
16314 let authenticated_dir = tempfile::tempdir().unwrap();
16315 let authenticated =
16316 Database::create_with_credentials(authenticated_dir.path(), "admin", "admin-password")
16317 .unwrap();
16318 authenticated.create_table("docs", test_schema()).unwrap();
16319 let admin = authenticated.resolve_principal("admin").unwrap();
16320 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16321 authenticated
16322 .validate_write_permissions(
16323 &puts(authenticated.table_id("docs").unwrap()),
16324 Some(&admin),
16325 None,
16326 )
16327 .unwrap();
16328 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
16329 }
16330
16331 #[test]
16332 fn delete_batch_checks_permission_once_when_staged_and_once_when_committed() {
16333 let dir = tempfile::tempdir().unwrap();
16334 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
16335 db.create_table("docs", test_schema()).unwrap();
16336 let admin = db.resolve_principal("admin").unwrap();
16337 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16338
16339 let mut transaction = db.begin_as(Some(admin));
16340 transaction
16341 .delete_batch("docs", (0..100).map(RowId).collect())
16342 .unwrap();
16343 transaction.commit().unwrap();
16344
16345 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 2));
16346 }
16347
16348 #[test]
16349 fn truncate_validation_checks_admin_once_for_all_tables() {
16350 let dir = tempfile::tempdir().unwrap();
16351 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
16352 db.create_table("first", test_schema()).unwrap();
16353 db.create_table("second", test_schema()).unwrap();
16354 let admin = db.resolve_principal("admin").unwrap();
16355 let staging = vec![
16356 (db.table_id("first").unwrap(), Staged::Truncate),
16357 (db.table_id("second").unwrap(), Staged::Truncate),
16358 ];
16359
16360 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16361 db.validate_write_permissions(&staging, Some(&admin), None)
16362 .unwrap();
16363 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
16364 }
16365
16366 #[test]
16367 fn one_table_commit_batches_structural_work() {
16368 let dir = tempfile::tempdir().unwrap();
16369 let db = Database::create(dir.path()).unwrap();
16370 db.create_table("docs", test_schema()).unwrap();
16371 let table_id = db.table_id("docs").unwrap();
16372
16373 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(0));
16374 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
16375 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
16376 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
16377 db.transaction(|transaction| {
16378 for id in 0..100 {
16379 transaction.put("docs", vec![(1, Value::Int64(id))])?;
16380 }
16381 Ok(())
16382 })
16383 .unwrap();
16384
16385 AUTO_INCREMENT_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 2));
16386 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16387 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16388 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
16389
16390 let puts = crate::wal::SharedWal::replay(dir.path())
16391 .unwrap()
16392 .into_iter()
16393 .filter_map(|record| match record.op {
16394 crate::wal::Op::Put { table_id: id, rows } if id == table_id => Some(
16395 bincode::deserialize::<Vec<crate::memtable::Row>>(&rows)
16396 .unwrap()
16397 .len(),
16398 ),
16399 _ => None,
16400 })
16401 .collect::<Vec<_>>();
16402 assert_eq!(puts, [100]);
16403
16404 let row_ids = db
16405 .table("docs")
16406 .unwrap()
16407 .lock()
16408 .visible_rows(db.snapshot().0)
16409 .unwrap()
16410 .into_iter()
16411 .take(2)
16412 .map(|row| row.row_id)
16413 .collect::<Vec<_>>();
16414 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
16415 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
16416 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
16417 db.transaction(|transaction| {
16418 for row_id in row_ids {
16419 transaction.delete("docs", row_id)?;
16420 }
16421 Ok(())
16422 })
16423 .unwrap();
16424 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16425 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16426 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
16427
16428 let deletes = crate::wal::SharedWal::replay(dir.path())
16429 .unwrap()
16430 .into_iter()
16431 .filter_map(|record| match record.op {
16432 crate::wal::Op::Delete {
16433 table_id: id,
16434 row_ids,
16435 } if id == table_id => Some(row_ids.len()),
16436 _ => None,
16437 })
16438 .collect::<Vec<_>>();
16439 assert_eq!(deletes, [2]);
16440 }
16441
16442 fn puts(table_id: u64) -> Vec<(u64, Staged)> {
16443 (0..10_050)
16444 .map(|id| (table_id, Staged::Put(vec![(1, Value::Int64(id))])))
16445 .collect()
16446 }
16447
16448 fn test_schema() -> Schema {
16449 Schema {
16450 columns: vec![ColumnDef {
16451 id: 1,
16452 name: "id".into(),
16453 ty: TypeId::Int64,
16454 flags: crate::schema::ColumnFlags::empty()
16455 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
16456 default_value: None,
16457 }],
16458 ..Schema::default()
16459 }
16460 }
16461}
16462
16463#[cfg(test)]
16464mod cdc_bounds_tests {
16465 use super::*;
16466
16467 #[test]
16468 fn retained_byte_limit_rejects_without_allocating_payload() {
16469 let mut retained = 0;
16470 let error = charge_cdc_bytes(
16471 &mut retained,
16472 CDC_MAX_RETAINED_BYTES.saturating_add(1),
16473 "CDC retained bytes",
16474 )
16475 .unwrap_err();
16476 assert!(matches!(
16477 error,
16478 MongrelError::ResourceLimitExceeded {
16479 resource: "CDC retained bytes",
16480 ..
16481 }
16482 ));
16483 }
16484
16485 #[test]
16486 fn row_json_estimate_accounts_for_byte_array_expansion() {
16487 let row = crate::memtable::Row::new(RowId(1), Epoch(1))
16488 .with_column(1, Value::Bytes(vec![0; 1024]));
16489 assert!(cdc_row_json_bytes(&row) >= 1024 * std::mem::size_of::<serde_json::Value>());
16490 }
16491}
16492
16493#[cfg(test)]
16494mod generation_metrics_tests {
16495 use super::*;
16496 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
16497
16498 #[test]
16499 fn legacy_cow_fallback_is_measured() {
16500 let dir = tempfile::tempdir().unwrap();
16501 let table = Table::create(
16502 dir.path(),
16503 Schema {
16504 columns: vec![ColumnDef {
16505 id: 1,
16506 name: "id".into(),
16507 ty: TypeId::Int64,
16508 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
16509 default_value: None,
16510 }],
16511 ..Schema::default()
16512 },
16513 1,
16514 )
16515 .unwrap();
16516 let handle = TableHandle::from_table(table);
16517 let held = match &handle.inner {
16518 TableHandleInner::CopyOnWrite(slot) => Arc::clone(&slot.read()),
16519 TableHandleInner::Direct(_) => unreachable!(),
16520 };
16521
16522 handle.lock().set_sync_byte_threshold(1);
16523
16524 let stats = handle.generation_stats();
16525 assert_eq!(stats.cow_clone_count, 1);
16526 assert!(stats.estimated_cow_clone_bytes > 0);
16527 drop(held);
16528 }
16529}
16530
16531#[cfg(test)]
16532mod trigger_engine_tests {
16533 use super::*;
16534
16535 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
16536 WriteEvent {
16537 table: "test".into(),
16538 kind: TriggerEvent::Insert,
16539 new: Some(TriggerRowImage {
16540 columns: new_cells.iter().cloned().collect(),
16541 }),
16542 old: Some(TriggerRowImage {
16543 columns: old_cells.iter().cloned().collect(),
16544 }),
16545 changed_columns: Vec::new(),
16546 op_indices: Vec::new(),
16547 put_idx: None,
16548 trigger_stack: Vec::new(),
16549 }
16550 }
16551
16552 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
16553 WriteEvent {
16554 table: "test".into(),
16555 kind: TriggerEvent::Insert,
16556 new: Some(TriggerRowImage {
16557 columns: new_cells.iter().cloned().collect(),
16558 }),
16559 old: None,
16560 changed_columns: Vec::new(),
16561 op_indices: Vec::new(),
16562 put_idx: None,
16563 trigger_stack: Vec::new(),
16564 }
16565 }
16566
16567 #[test]
16568 fn value_order_int64_vs_float64() {
16569 assert_eq!(
16570 value_order(&Value::Int64(5), &Value::Float64(5.0)),
16571 Some(std::cmp::Ordering::Equal)
16572 );
16573 assert_eq!(
16574 value_order(&Value::Int64(5), &Value::Float64(3.0)),
16575 Some(std::cmp::Ordering::Greater)
16576 );
16577 assert_eq!(
16578 value_order(&Value::Int64(2), &Value::Float64(3.0)),
16579 Some(std::cmp::Ordering::Less)
16580 );
16581 }
16582
16583 #[test]
16584 fn value_order_null_returns_none() {
16585 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
16586 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
16587 assert_eq!(value_order(&Value::Null, &Value::Null), None);
16588 }
16589
16590 #[test]
16591 fn value_order_cross_group_returns_none() {
16592 assert_eq!(
16593 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
16594 None
16595 );
16596 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
16597 assert_eq!(
16598 value_order(
16599 &Value::Embedding(vec![1.0, 2.0]),
16600 &Value::Embedding(vec![1.0, 2.0])
16601 ),
16602 None
16603 );
16604 }
16605
16606 #[test]
16607 fn eval_trigger_expr_ranges_and_booleans() {
16608 let expr = TriggerExpr::And {
16609 left: Box::new(TriggerExpr::Gt {
16610 left: TriggerValue::NewColumn(1),
16611 right: TriggerValue::Literal(Value::Int64(0)),
16612 }),
16613 right: Box::new(TriggerExpr::Lte {
16614 left: TriggerValue::NewColumn(1),
16615 right: TriggerValue::Literal(Value::Int64(100)),
16616 }),
16617 };
16618 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
16619 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
16620 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
16621
16622 let or_expr = TriggerExpr::Or {
16623 left: Box::new(TriggerExpr::Lt {
16624 left: TriggerValue::NewColumn(1),
16625 right: TriggerValue::Literal(Value::Int64(0)),
16626 }),
16627 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
16628 TriggerValue::OldColumn(2),
16629 )))),
16630 };
16631 assert!(eval_trigger_expr(
16632 &or_expr,
16633 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
16634 )
16635 .unwrap());
16636 assert!(!eval_trigger_expr(
16637 &or_expr,
16638 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
16639 )
16640 .unwrap());
16641
16642 assert!(eval_trigger_expr(
16643 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
16644 &event_insert(&[])
16645 )
16646 .unwrap());
16647 assert!(!eval_trigger_expr(
16648 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
16649 &event_insert(&[])
16650 )
16651 .unwrap());
16652 assert!(!eval_trigger_expr(
16653 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
16654 &event_insert(&[])
16655 )
16656 .unwrap());
16657 }
16658}