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 approx_aggregate_for_current_principal(
3904 &self,
3905 table_name: &str,
3906 conditions: &[crate::query::Condition],
3907 column: Option<u16>,
3908 agg: crate::engine::ApproxAgg,
3909 z: f64,
3910 ) -> Result<Option<crate::engine::ApproxResult>> {
3911 if !z.is_finite() || z <= 0.0 {
3912 return Err(MongrelError::InvalidArgument(
3913 "z must be finite and > 0".into(),
3914 ));
3915 }
3916 let mut columns = crate::query::condition_columns(conditions);
3917 columns.extend(column);
3918 columns.sort_unstable();
3919 columns.dedup();
3920 self.with_authorized_aggregate_table(
3921 table_name,
3922 &columns,
3923 None,
3924 true,
3925 true,
3926 |table, authorization, _, _| {
3927 table.approx_aggregate_with_candidate_authorization(
3928 conditions,
3929 column,
3930 agg,
3931 z,
3932 authorization,
3933 )
3934 },
3935 )
3936 }
3937
3938 pub fn incremental_aggregate_for_current_principal(
3942 &self,
3943 table_name: &str,
3944 conditions: &[crate::query::Condition],
3945 column: Option<u16>,
3946 agg: crate::engine::NativeAgg,
3947 ) -> Result<crate::engine::IncrementalAggResult> {
3948 self.incremental_aggregate_for_principal(table_name, conditions, column, agg, None, true)
3949 }
3950
3951 pub fn incremental_aggregate_for_principal(
3955 &self,
3956 table_name: &str,
3957 conditions: &[crate::query::Condition],
3958 column: Option<u16>,
3959 agg: crate::engine::NativeAgg,
3960 principal: Option<&crate::auth::Principal>,
3961 catalog_bound: bool,
3962 ) -> Result<crate::engine::IncrementalAggResult> {
3963 let mut columns = crate::query::condition_columns(conditions);
3964 columns.extend(column);
3965 columns.sort_unstable();
3966 columns.dedup();
3967 self.with_authorized_aggregate_table(
3968 table_name,
3969 &columns,
3970 principal,
3971 catalog_bound,
3972 false,
3973 |table, _, principal, security_version| {
3974 let cache_key = incremental_aggregate_cache_key(
3975 table_name,
3976 conditions,
3977 column,
3978 agg,
3979 principal,
3980 security_version,
3981 );
3982 table.aggregate_incremental(cache_key, conditions, column, agg)
3983 },
3984 )
3985 }
3986
3987 pub fn get_for_current_principal(
3990 &self,
3991 table_name: &str,
3992 row_id: RowId,
3993 ) -> Result<Option<crate::memtable::Row>> {
3994 self.with_authorized_read(
3995 table_name,
3996 None,
3997 true,
3998 |table, snapshot, allowed, principal| {
3999 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
4000 let Some(row) = table.get(row_id, snapshot) else {
4001 return Ok(None);
4002 };
4003 if allowed.is_some_and(|allowed| !allowed.contains(&row.row_id)) {
4004 return Ok(None);
4005 }
4006 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
4007 if let Some(row) = rows.first_mut() {
4008 row.columns
4009 .retain(|column, _| allowed_columns.contains(column));
4010 }
4011 Ok(rows.pop())
4012 },
4013 )
4014 }
4015
4016 pub fn ann_rerank_for_current_principal(
4019 &self,
4020 table_name: &str,
4021 request: &crate::query::AnnRerankRequest,
4022 ) -> Result<Vec<crate::query::AnnRerankHit>> {
4023 self.with_authorized_scored_read_context_at(
4024 table_name,
4025 None,
4026 true,
4027 Some(&ReadAuthorization {
4028 operation: crate::auth::ColumnOperation::Select,
4029 columns: vec![request.column_id],
4030 permissions: Vec::new(),
4031 }),
4032 None,
4033 None,
4034 |table, snapshot, authorization, principal| {
4035 self.require_columns_for(
4036 table_name,
4037 crate::auth::ColumnOperation::Select,
4038 &[request.column_id],
4039 principal,
4040 )?;
4041 table.ann_rerank_at_with_candidate_authorization_on_generation(
4042 request,
4043 snapshot,
4044 authorization,
4045 None,
4046 )
4047 },
4048 )
4049 }
4050
4051 pub fn authorized_read_snapshot(
4054 &self,
4055 table: &str,
4056 principal: Option<&crate::auth::Principal>,
4057 ) -> Result<AuthorizedReadSnapshot> {
4058 let (security, security_version, effective_principal) = {
4059 let catalog = self.catalog.read();
4060 (
4061 catalog.security.clone(),
4062 catalog.security_version,
4063 self.principal_for_authorized_read(&catalog, principal, false)?,
4064 )
4065 };
4066 let handle = self.table(table)?;
4067 let (table_snapshot, data_generation, allowed_row_ids) = {
4068 let table_handle = handle.lock();
4069 let table_snapshot = table_handle.snapshot();
4070 let data_generation = table_handle.data_generation();
4071 let allowed = self.allowed_row_ids_locked(
4072 table,
4073 &table_handle,
4074 table_snapshot,
4075 (&security, security_version),
4076 effective_principal.as_ref(),
4077 None,
4078 )?;
4079 (
4080 table_snapshot,
4081 data_generation,
4082 allowed.map(|allowed| (*allowed).clone()),
4083 )
4084 };
4085 Ok(AuthorizedReadSnapshot {
4086 table: table.to_string(),
4087 table_snapshot,
4088 data_generation,
4089 security_version,
4090 allowed_row_ids,
4091 })
4092 }
4093
4094 pub fn authorized_read_snapshot_valid(&self, snapshot: &AuthorizedReadSnapshot) -> bool {
4095 if self.catalog.read().security_version != snapshot.security_version {
4096 return false;
4097 }
4098 self.table(&snapshot.table)
4099 .ok()
4100 .is_some_and(|table| table.lock().data_generation() == snapshot.data_generation)
4101 }
4102
4103 pub fn rls_cache_stats(&self) -> RlsCacheStats {
4104 self.rls_cache.lock().stats()
4105 }
4106
4107 pub fn rows_for(
4109 &self,
4110 table: &str,
4111 principal: Option<&crate::auth::Principal>,
4112 ) -> Result<Vec<crate::memtable::Row>> {
4113 if principal.is_none() && self.principal.read().is_some() {
4114 self.refresh_principal()?;
4115 }
4116 let allowed = self.select_column_ids_for(table, principal)?;
4117 let handle = self.table(table)?;
4118 let rows = {
4119 let table = handle.lock();
4120 table.visible_rows(table.snapshot())?
4121 };
4122 let mut rows = self.secure_rows_for(table, rows, principal)?;
4123 for row in &mut rows {
4124 row.columns.retain(|column, _| allowed.contains(column));
4125 }
4126 Ok(rows)
4127 }
4128
4129 pub fn rows_at_epoch_for_current_principal(
4132 &self,
4133 table_name: &str,
4134 snapshot: Snapshot,
4135 ) -> Result<Vec<crate::memtable::Row>> {
4136 self.with_authorized_read_context(
4137 table_name,
4138 None,
4139 true,
4140 Some(&ReadAuthorization {
4141 operation: crate::auth::ColumnOperation::Select,
4142 columns: Vec::new(),
4143 permissions: Vec::new(),
4144 }),
4145 None,
4146 Some(snapshot),
4147 |table, snapshot, allowed, principal| {
4148 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
4149 let mut rows = table.visible_rows(snapshot)?;
4150 if let Some(allowed) = allowed {
4151 rows.retain(|row| allowed.contains(&row.row_id));
4152 }
4153 rows = self.secure_rows_for(table_name, rows, principal)?;
4154 for row in &mut rows {
4155 row.columns
4156 .retain(|column, _| allowed_columns.contains(column));
4157 }
4158 Ok(rows)
4159 },
4160 )
4161 }
4162
4163 pub fn count_for(
4165 &self,
4166 table: &str,
4167 principal: Option<&crate::auth::Principal>,
4168 ) -> Result<u64> {
4169 if principal.is_none() && self.principal.read().is_some() {
4170 self.refresh_principal()?;
4171 }
4172 self.select_column_ids_for(table, principal)?;
4173 if self.security_active_for(table) {
4174 Ok(self.rows_for(table, principal)?.len() as u64)
4175 } else {
4176 Ok(self.table(table)?.lock().count())
4177 }
4178 }
4179
4180 pub fn put_for(
4182 &self,
4183 table: &str,
4184 mut cells: Vec<(u16, crate::memtable::Value)>,
4185 principal: Option<&crate::auth::Principal>,
4186 ) -> Result<RowId> {
4187 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
4188 self.require_columns_for(
4189 table,
4190 crate::auth::ColumnOperation::Insert,
4191 &columns,
4192 principal,
4193 )?;
4194 let handle = self.table(table)?;
4195 let mut table_handle = handle.lock();
4196 table_handle.fill_auto_inc(&mut cells)?;
4197 table_handle.apply_defaults(&mut cells)?;
4198 let mut row = crate::memtable::Row::new(RowId(0), self.epoch.visible());
4199 row.columns.extend(cells.iter().cloned());
4200 self.check_row_policy_for(
4201 table,
4202 crate::security::PolicyCommand::Insert,
4203 &row,
4204 true,
4205 principal,
4206 )?;
4207 table_handle.put(cells)
4208 }
4209
4210 pub fn check_row_policy_for(
4211 &self,
4212 table: &str,
4213 command: crate::security::PolicyCommand,
4214 row: &crate::memtable::Row,
4215 check_new: bool,
4216 principal: Option<&crate::auth::Principal>,
4217 ) -> Result<()> {
4218 let security = self.catalog.read().security.clone();
4219 if !security.rls_enabled(table) {
4220 return Ok(());
4221 }
4222 let cached = self.principal.read().clone();
4223 let principal = principal
4224 .or(cached.as_ref())
4225 .ok_or(MongrelError::AuthRequired)?;
4226 if security.row_allowed(table, command, row, principal, check_new) {
4227 return Ok(());
4228 }
4229 let required = match command {
4230 crate::security::PolicyCommand::Insert => crate::auth::Permission::Insert {
4231 table: table.to_string(),
4232 },
4233 crate::security::PolicyCommand::Update => crate::auth::Permission::Update {
4234 table: table.to_string(),
4235 },
4236 crate::security::PolicyCommand::Select => crate::auth::Permission::Select {
4237 table: table.to_string(),
4238 },
4239 crate::security::PolicyCommand::Delete | crate::security::PolicyCommand::All => {
4240 crate::auth::Permission::Delete {
4241 table: table.to_string(),
4242 }
4243 }
4244 };
4245 Err(MongrelError::PermissionDenied {
4246 required,
4247 principal: principal.username.clone(),
4248 })
4249 }
4250
4251 pub fn set_materialized_view(
4254 &self,
4255 definition: crate::catalog::MaterializedViewEntry,
4256 ) -> Result<()> {
4257 self.set_materialized_view_with_epoch(definition)
4258 .map(|_| ())
4259 }
4260
4261 pub fn set_materialized_view_with_epoch(
4263 &self,
4264 definition: crate::catalog::MaterializedViewEntry,
4265 ) -> Result<Epoch> {
4266 use crate::wal::DdlOp;
4267 use std::sync::atomic::Ordering;
4268
4269 self.require(&crate::auth::Permission::Ddl)?;
4270 if self.poisoned.load(Ordering::Relaxed) {
4271 return Err(MongrelError::Other(
4272 "database poisoned by fsync error".into(),
4273 ));
4274 }
4275 if definition.name.is_empty() || definition.query.trim().is_empty() {
4276 return Err(MongrelError::InvalidArgument(
4277 "materialized view name and query must not be empty".into(),
4278 ));
4279 }
4280
4281 let _ddl = self.ddl_lock.lock();
4282 let _security_write = self.security_write()?;
4283 self.require(&crate::auth::Permission::Ddl)?;
4284 let table_id = self
4285 .catalog
4286 .read()
4287 .live(&definition.name)
4288 .ok_or_else(|| {
4289 MongrelError::NotFound(format!(
4290 "materialized view table {:?} not found",
4291 definition.name
4292 ))
4293 })?
4294 .table_id;
4295 let definition_json = DdlOp::encode_materialized_view(&definition)?;
4296 let _commit = self.commit_lock.lock();
4297 let epoch = self.epoch.bump_assigned();
4298 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
4299 let txn_id = self.alloc_txn_id()?;
4300 let mut next_catalog = self.catalog.read().clone();
4301 if let Some(existing) = next_catalog
4302 .materialized_views
4303 .iter_mut()
4304 .find(|existing| existing.name == definition.name)
4305 {
4306 *existing = definition.clone();
4307 } else {
4308 next_catalog.materialized_views.push(definition.clone());
4309 }
4310 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
4311 let commit_seq = {
4312 let mut wal = self.shared_wal.lock();
4313 let append: Result<u64> = (|| {
4314 wal.append(
4315 txn_id,
4316 table_id,
4317 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
4318 name: definition.name.clone(),
4319 definition_json,
4320 }),
4321 )?;
4322 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
4323 wal.append_commit(txn_id, epoch, &[])
4324 })();
4325 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
4326 };
4327 self.await_durable_commit(commit_seq, epoch)?;
4328
4329 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
4330 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
4331 Ok(epoch)
4332 }
4333
4334 pub fn root(&self) -> &Path {
4336 self.durable_root.canonical_path()
4337 }
4338
4339 pub fn durable_root(&self) -> Arc<crate::durable_file::DurableRoot> {
4342 Arc::clone(&self.durable_root)
4343 }
4344
4345 #[cfg(feature = "encryption")]
4349 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
4350 self.kek
4351 .as_deref()
4352 .map(|kek| kek.derive_subkey(b"mongreldb/server/idempotency/v1"))
4353 }
4354
4355 #[cfg(not(feature = "encryption"))]
4356 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
4357 None
4358 }
4359
4360 pub fn is_read_only_replica(&self) -> bool {
4361 self.read_only
4362 }
4363
4364 pub fn ensure_consistent_read(&self) -> Result<()> {
4368 if self.poisoned.load(Ordering::Relaxed) {
4369 return Err(MongrelError::Other(
4370 "database poisoned by post-commit failure; reopen required".into(),
4371 ));
4372 }
4373 Ok(())
4374 }
4375
4376 pub fn set_replication_wal_retention_segments(&self, segments: usize) {
4377 self.replication_wal_retention_segments
4378 .store(segments, std::sync::atomic::Ordering::Relaxed);
4379 }
4380
4381 pub fn replication_snapshot(&self) -> Result<crate::replication::ReplicationSnapshot> {
4386 let admin = crate::auth::Permission::Admin;
4387 self.require(&admin)?;
4388 let operation_principal = self.principal_snapshot();
4389 let _barrier = self.replication_barrier.write();
4390 let _ddl = self.ddl_lock.lock();
4391 let _security = self.security_coordinator.gate.read();
4392 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4393 let mut handles: Vec<_> = self
4394 .tables
4395 .read()
4396 .iter()
4397 .map(|(id, handle)| (*id, handle.clone()))
4398 .collect();
4399 handles.sort_by_key(|(id, _)| *id);
4400 let _table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
4401 let _commit = self.commit_lock.lock();
4402 let mut wal = self.shared_wal.lock();
4403 wal.group_sync()?;
4404 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
4405 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
4406 let epoch = records
4407 .iter()
4408 .filter_map(|record| match &record.op {
4409 crate::wal::Op::TxnCommit { epoch, .. } => Some(*epoch),
4410 _ => None,
4411 })
4412 .max()
4413 .unwrap_or(0)
4414 .max(self.epoch.committed().0);
4415 let files = crate::replication::capture_files(&self.root)?;
4416 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
4417 drop(wal);
4418 Ok(crate::replication::ReplicationSnapshot::new(
4419 source_id, epoch, files,
4420 ))
4421 }
4422
4423 pub fn hot_backup(&self, destination: impl AsRef<Path>) -> Result<crate::backup::BackupReport> {
4431 let control = crate::ExecutionControl::new(None);
4432 self.hot_backup_controlled(destination, &control, || true)
4433 }
4434
4435 pub(crate) fn hot_backup_to_durable_child(
4436 &self,
4437 parent: &crate::durable_file::DurableRoot,
4438 child: &Path,
4439 control: &crate::ExecutionControl,
4440 ) -> Result<crate::backup::BackupReport> {
4441 let mut components = child.components();
4442 if !matches!(components.next(), Some(std::path::Component::Normal(_)))
4443 || components.next().is_some()
4444 {
4445 return Err(MongrelError::InvalidArgument(
4446 "durable backup child must be one relative path component".into(),
4447 ));
4448 }
4449 let destination_name = child.file_name().ok_or_else(|| {
4450 MongrelError::InvalidArgument("durable backup child has no filename".into())
4451 })?;
4452 let prepared = prepare_backup_destination_in(&self.root, parent, destination_name)?;
4453 self.hot_backup_prepared(prepared, control, || true)
4454 }
4455
4456 #[doc(hidden)]
4459 pub fn hot_backup_controlled<F>(
4460 &self,
4461 destination: impl AsRef<Path>,
4462 control: &crate::ExecutionControl,
4463 before_publish: F,
4464 ) -> Result<crate::backup::BackupReport>
4465 where
4466 F: FnOnce() -> bool,
4467 {
4468 let prepared = prepare_backup_destination(&self.root, destination.as_ref())?;
4469 self.hot_backup_prepared(prepared, control, before_publish)
4470 }
4471
4472 fn hot_backup_prepared<F>(
4473 &self,
4474 mut prepared: PreparedBackupDestination,
4475 control: &crate::ExecutionControl,
4476 before_publish: F,
4477 ) -> Result<crate::backup::BackupReport>
4478 where
4479 F: FnOnce() -> bool,
4480 {
4481 let admin = crate::auth::Permission::Admin;
4482 self.require(&admin)?;
4483 let operation_principal = self.principal_snapshot();
4484 control.checkpoint()?;
4485 let destination = prepared.destination_path.clone();
4486 let mut before_publish = Some(before_publish);
4487
4488 let outcome = (|| {
4489 control.checkpoint()?;
4490 let barrier = self.replication_barrier.write();
4491 let ddl = self.ddl_lock.lock();
4492 let security = self.security_coordinator.gate.read();
4493 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4494 let mut handles: Vec<_> = self
4495 .tables
4496 .read()
4497 .iter()
4498 .map(|(id, handle)| (*id, handle.clone()))
4499 .collect();
4500 handles.sort_by_key(|(id, _)| *id);
4501 let table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
4502 let commit = self.commit_lock.lock();
4503 let mut wal = self.shared_wal.lock();
4504 wal.group_sync()?;
4505 let epoch = self.epoch.committed().0;
4506 let boundary_unix_nanos = current_unix_nanos();
4507
4508 let pin_nonce = std::time::SystemTime::now()
4509 .duration_since(std::time::UNIX_EPOCH)
4510 .unwrap_or_default()
4511 .as_nanos();
4512 let file_pin_root = self
4513 .root
4514 .join(META_DIR)
4515 .join("backup-pins")
4516 .join(format!("{}-{pin_nonce}", std::process::id()));
4517 std::fs::create_dir_all(&file_pin_root)?;
4518 let _file_pins = BackupFilePins {
4519 root: file_pin_root.clone(),
4520 };
4521 let mut run_files = Vec::new();
4522 for (index, (table_id, _)) in handles.iter().enumerate() {
4523 if index % 256 == 0 {
4524 control.checkpoint()?;
4525 }
4526 let table = &table_guards[index];
4527 for (run_index, run) in table.run_refs().iter().enumerate() {
4528 if run_index % 256 == 0 {
4529 control.checkpoint()?;
4530 }
4531 let source = table.run_path(run.run_id as u64);
4532 let relative = Path::new(TABLES_DIR)
4533 .join(table_id.to_string())
4534 .join(crate::engine::RUNS_DIR)
4535 .join(format!("r-{}.sr", run.run_id));
4536 let pinned = file_pin_root.join(format!("{table_id}-{}.sr", run.run_id));
4537 if std::fs::hard_link(&source, &pinned).is_err() {
4538 crate::backup::copy_file_synced(&source, &pinned)?;
4539 }
4540 run_files.push(((*table_id, run.run_id), pinned, relative));
4541 }
4542 }
4543 crate::durable_file::sync_directory(&file_pin_root)?;
4544 let run_keys: Vec<_> = run_files.iter().map(|(key, _, _)| *key).collect();
4545 {
4546 let mut pins = self.backup_pins.lock();
4547 for key in &run_keys {
4548 *pins.entry(*key).or_insert(0) += 1;
4549 }
4550 }
4551 let _run_pins = RunPins {
4552 pins: Arc::clone(&self.backup_pins),
4553 runs: run_keys,
4554 };
4555 let deferred: HashSet<_> = run_files
4556 .iter()
4557 .map(|(_, _, relative)| relative.clone())
4558 .collect();
4559 let mut copied = Vec::new();
4560 copy_backup_boundary(
4561 &self.root,
4562 prepared.stage.as_deref().ok_or_else(|| {
4563 MongrelError::Other("backup staging root was already released".into())
4564 })?,
4565 &deferred,
4566 &mut copied,
4567 Some(control),
4568 )?;
4569
4570 drop(wal);
4571 drop(commit);
4572 drop(table_guards);
4573 drop(security);
4574 drop(ddl);
4575 drop(barrier);
4576
4577 if let Some(hook) = self.backup_hook.lock().as_ref() {
4578 hook();
4579 }
4580 for (index, (_, source, relative)) in run_files.into_iter().enumerate() {
4581 if index % 256 == 0 {
4582 control.checkpoint()?;
4583 }
4584 let mut source = crate::durable_file::open_regular_nofollow(&source)?;
4585 prepared
4586 .stage
4587 .as_deref()
4588 .ok_or_else(|| {
4589 MongrelError::Other("backup staging root was already released".into())
4590 })?
4591 .copy_new_from(&relative, &mut source)?;
4592 copied.push(relative);
4593 }
4594
4595 let manifest = crate::backup::BackupManifest::create_controlled_durable(
4596 prepared.stage.as_deref().ok_or_else(|| {
4597 MongrelError::Other("backup staging root was already released".into())
4598 })?,
4599 epoch,
4600 &copied,
4601 control,
4602 )?;
4603 manifest.write_to_durable(prepared.stage.as_deref().ok_or_else(|| {
4604 MongrelError::Other("backup staging root was already released".into())
4605 })?)?;
4606 control.checkpoint()?;
4607 let publish = before_publish.take().ok_or_else(|| {
4608 MongrelError::Other("backup publication callback already consumed".into())
4609 })?;
4610 if !publish() {
4611 return Err(MongrelError::Cancelled);
4612 }
4613 let final_security = self.security_coordinator.gate.read();
4614 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4615 drop(prepared.stage.take().ok_or_else(|| {
4619 MongrelError::Other("backup staging root was already released".into())
4620 })?);
4621 let published = std::cell::Cell::new(false);
4622 if let Err(error) = prepared.parent.rename_directory_new_with_after(
4623 Path::new(&prepared.stage_name),
4624 &prepared.parent,
4625 Path::new(&prepared.destination_name),
4626 || published.set(true),
4627 ) {
4628 if published.get() {
4629 return Err(MongrelError::CommitOutcomeUnknown {
4630 epoch,
4631 message: format!("backup publication was not durable: {error}"),
4632 });
4633 }
4634 return Err(error.into());
4635 }
4636 drop(final_security);
4637 Ok(crate::backup::BackupReport {
4638 destination,
4639 epoch,
4640 boundary_unix_nanos,
4641 files: manifest.files.len(),
4642 bytes: manifest.total_bytes(),
4643 })
4644 })();
4645
4646 if outcome.is_err() {
4647 drop(prepared.stage.take());
4648 let _ = prepared
4649 .parent
4650 .remove_directory_all(Path::new(&prepared.stage_name));
4651 }
4652 outcome
4653 }
4654
4655 pub fn replication_batch_since(
4658 &self,
4659 since_epoch: u64,
4660 ) -> Result<crate::replication::ReplicationBatch> {
4661 use crate::wal::Op;
4662
4663 let admin = crate::auth::Permission::Admin;
4664 self.require(&admin)?;
4665 let operation_principal = self.principal_snapshot();
4666
4667 let mut wal = self.shared_wal.lock();
4668 wal.group_sync()?;
4669 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
4670 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
4671 drop(wal);
4672
4673 let commits: HashMap<u64, u64> = records
4674 .iter()
4675 .filter_map(|record| match &record.op {
4676 Op::TxnCommit { epoch, .. } => Some((record.txn_id, *epoch)),
4677 _ => None,
4678 })
4679 .collect();
4680 let earliest_epoch = commits.values().copied().min();
4681 let current_epoch = commits
4682 .values()
4683 .copied()
4684 .max()
4685 .unwrap_or(0)
4686 .max(self.epoch.committed().0);
4687 let selected: HashSet<u64> = commits
4688 .iter()
4689 .filter_map(|(txn_id, epoch)| (*epoch > since_epoch).then_some(*txn_id))
4690 .collect();
4691 let retention_gap = since_epoch < current_epoch
4692 && since_epoch < crate::replication::replication_wal_floor(&self.root)?;
4693 let spilled = records.iter().any(|record| {
4694 selected.contains(&record.txn_id)
4695 && matches!(
4696 &record.op,
4697 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty()
4698 )
4699 });
4700 let records = records
4701 .into_iter()
4702 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
4703 .filter(|record| selected.contains(&record.txn_id))
4704 .collect();
4705 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
4706 let batch = crate::replication::ReplicationBatch::complete_for_source(
4707 source_id,
4708 since_epoch,
4709 current_epoch,
4710 earliest_epoch,
4711 retention_gap,
4712 spilled,
4713 records,
4714 )?;
4715 if let Some(hook) = self.replication_hook.lock().as_ref() {
4716 hook();
4717 }
4718 let _security = self.security_coordinator.gate.read();
4719 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4720 Ok(batch)
4721 }
4722
4723 pub fn append_replication_batch(
4727 &self,
4728 batch: &crate::replication::ReplicationBatch,
4729 ) -> Result<u64> {
4730 use crate::wal::Op;
4731
4732 if !self.read_only {
4733 return Err(MongrelError::InvalidArgument(
4734 "replication batches may only target a marked replica".into(),
4735 ));
4736 }
4737 let current = crate::replication::replica_epoch(&self.root)?;
4738 if batch.is_source_bound() {
4739 let source_id = crate::replication::replica_source_id_durable(&self.durable_root)?;
4740 if batch.source_id != source_id {
4741 return Err(MongrelError::Conflict(
4742 "replication batch source does not match follower binding".into(),
4743 ));
4744 }
4745 }
4746 if batch.requires_snapshot {
4747 return Err(MongrelError::Conflict(
4748 "replication snapshot required for this batch".into(),
4749 ));
4750 }
4751 batch.validate_proof()?;
4752 if batch.from_epoch != current {
4753 if batch.from_epoch < current && batch.current_epoch == current {
4754 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
4755 let _wal = self.shared_wal.lock();
4756 let existing: HashSet<(u64, u64)> =
4757 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
4758 .into_iter()
4759 .filter_map(|record| match record.op {
4760 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
4761 _ => None,
4762 })
4763 .collect();
4764 let already_applied = batch.records.iter().all(|record| match &record.op {
4765 Op::TxnCommit { epoch, .. } => existing.contains(&(record.txn_id, *epoch)),
4766 _ => true,
4767 });
4768 if already_applied {
4769 return Ok(current);
4770 }
4771 }
4772 return Err(MongrelError::Conflict(format!(
4773 "replication batch starts at epoch {}, follower is at epoch {current}",
4774 batch.from_epoch
4775 )));
4776 }
4777 if batch.current_epoch < current {
4778 return Err(MongrelError::InvalidArgument(format!(
4779 "replication batch current epoch {} precedes follower epoch {current}",
4780 batch.current_epoch
4781 )));
4782 }
4783 let records = &batch.records;
4784 let mut commits = HashMap::new();
4785 let mut commit_epochs = HashSet::new();
4786 let mut commit_timestamps = HashMap::new();
4787 for record in records {
4788 match &record.op {
4789 Op::TxnCommit { epoch, added_runs } => {
4790 if !added_runs.is_empty() {
4791 return Err(MongrelError::Conflict(
4792 "replication snapshot required for spilled-run transaction".into(),
4793 ));
4794 }
4795 if commits.insert(record.txn_id, *epoch).is_some() {
4796 return Err(MongrelError::InvalidArgument(format!(
4797 "duplicate commit for replication transaction {}",
4798 record.txn_id
4799 )));
4800 }
4801 if *epoch <= current || *epoch > batch.current_epoch {
4802 return Err(MongrelError::InvalidArgument(format!(
4803 "replication commit epoch {epoch} is outside ({current}, {}]",
4804 batch.current_epoch
4805 )));
4806 }
4807 if !commit_epochs.insert(*epoch) {
4808 return Err(MongrelError::InvalidArgument(format!(
4809 "duplicate replication commit epoch {epoch}"
4810 )));
4811 }
4812 }
4813 Op::CommitTimestamp { unix_nanos } => {
4814 commit_timestamps.insert(record.txn_id, *unix_nanos);
4815 }
4816 _ => {}
4817 }
4818 }
4819 for record in records {
4820 if record.txn_id == crate::wal::SYSTEM_TXN_ID
4821 || matches!(&record.op, Op::TxnAbort | Op::Flush { .. })
4822 {
4823 return Err(MongrelError::InvalidArgument(
4824 "replication batch contains a non-committed record".into(),
4825 ));
4826 }
4827 if !commits.contains_key(&record.txn_id) {
4828 return Err(MongrelError::InvalidArgument(format!(
4829 "incomplete replication transaction {}",
4830 record.txn_id
4831 )));
4832 }
4833 }
4834 let target_epoch = commits
4835 .values()
4836 .copied()
4837 .filter(|epoch| *epoch > current)
4838 .max()
4839 .unwrap_or(current);
4840 if target_epoch != batch.current_epoch {
4841 return Err(MongrelError::InvalidArgument(format!(
4842 "replication batch ends at epoch {target_epoch}, expected {}",
4843 batch.current_epoch
4844 )));
4845 }
4846 let mut selected: HashSet<u64> = commits
4847 .iter()
4848 .filter_map(|(txn_id, epoch)| (*epoch > current).then_some(*txn_id))
4849 .collect();
4850 if selected.is_empty() {
4851 return Ok(current);
4852 }
4853 let mut wal = self.shared_wal.lock();
4854 wal.group_sync()?;
4855 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
4856 let existing: HashSet<(u64, u64)> =
4857 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
4858 .into_iter()
4859 .filter_map(|record| match record.op {
4860 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
4861 _ => None,
4862 })
4863 .collect();
4864 selected.retain(|txn_id| {
4865 commits
4866 .get(txn_id)
4867 .is_some_and(|epoch| !existing.contains(&(*txn_id, *epoch)))
4868 });
4869 for record in records {
4870 if !selected.contains(&record.txn_id) {
4871 continue;
4872 }
4873 match &record.op {
4874 Op::TxnCommit { epoch, added_runs } => {
4875 let timestamp = commit_timestamps
4876 .get(&record.txn_id)
4877 .copied()
4878 .unwrap_or_else(current_unix_nanos);
4879 wal.append_commit_at(record.txn_id, Epoch(*epoch), added_runs, timestamp)?;
4880 }
4881 Op::TxnAbort | Op::Flush { .. } | Op::CommitTimestamp { .. } => {}
4882 op => {
4883 wal.append(record.txn_id, 0, op.clone())?;
4884 }
4885 }
4886 }
4887 if !selected.is_empty() {
4888 wal.group_sync()?;
4889 }
4890 drop(wal);
4891
4892 let mut recovered_catalog = self.catalog.read().clone();
4896 if let Err(error) = recover_ddl_from_wal(
4897 &self.root,
4898 Some(&self.durable_root),
4899 &mut recovered_catalog,
4900 self.meta_dek.as_ref(),
4901 wal_dek.as_ref(),
4902 true,
4903 None,
4904 ) {
4905 return Err(MongrelError::DurableCommit {
4906 epoch: target_epoch,
4907 message: format!(
4908 "replication WAL is durable but catalog checkpoint failed: {error}"
4909 ),
4910 });
4911 }
4912 let _security = self.security_coordinator.gate.write();
4913 let old_security_version = self.catalog.read().security_version;
4914 let security_changed = old_security_version != recovered_catalog.security_version
4915 || self.catalog.read().require_auth != recovered_catalog.require_auth;
4916 let require_auth = recovered_catalog.require_auth;
4917 let principal = if security_changed {
4918 None
4919 } else {
4920 self.principal.read().as_ref().and_then(|principal| {
4921 Self::resolve_bound_principal_from_catalog(&recovered_catalog, principal)
4922 })
4923 };
4924 if require_auth {
4925 self.auth_state.set_require_auth(true);
4926 }
4927 self.auth_state.set_principal(principal.clone());
4928 *self.principal.write() = principal;
4929 let security_version = recovered_catalog.security_version;
4930 *self.catalog.write() = recovered_catalog;
4931 self.security_coordinator
4932 .version
4933 .store(security_version, Ordering::Release);
4934 if !require_auth {
4935 self.auth_state.set_require_auth(false);
4936 }
4937 if let Err(error) =
4938 crate::replication::reconcile_replica_epoch_durable(&self.durable_root, target_epoch)
4939 {
4940 return Err(MongrelError::DurableCommit {
4941 epoch: target_epoch,
4942 message: format!(
4943 "replication WAL and catalog are durable but follower watermark failed: {error}"
4944 ),
4945 });
4946 }
4947 Ok(target_epoch)
4948 }
4949
4950 pub fn table_id(&self, name: &str) -> Result<u64> {
4953 let cat = self.catalog.read();
4954 cat.live(name)
4955 .map(|e| e.table_id)
4956 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
4957 }
4958
4959 pub fn table_identity(&self, name: &str) -> Result<(u64, u64)> {
4964 let catalog = self.catalog.read();
4965 catalog
4966 .live(name)
4967 .map(|entry| (entry.table_id, entry.schema.schema_id))
4968 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
4969 }
4970
4971 pub(crate) fn building_table_id(&self, name: &str) -> Result<u64> {
4972 self.catalog
4973 .read()
4974 .building(name)
4975 .map(|entry| entry.table_id)
4976 .ok_or_else(|| MongrelError::NotFound(format!("building table {name:?} not found")))
4977 }
4978
4979 pub fn procedures(&self) -> Vec<StoredProcedure> {
4980 self.catalog
4981 .read()
4982 .procedures
4983 .iter()
4984 .map(|p| p.procedure.clone())
4985 .collect()
4986 }
4987
4988 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
4989 self.catalog
4990 .read()
4991 .procedures
4992 .iter()
4993 .find(|p| p.procedure.name == name)
4994 .map(|p| p.procedure.clone())
4995 }
4996
4997 pub fn create_procedure(&self, procedure: StoredProcedure) -> Result<StoredProcedure> {
4998 self.create_procedure_inner(procedure, None)
4999 }
5000
5001 pub fn create_procedure_controlled<F>(
5002 &self,
5003 procedure: StoredProcedure,
5004 mut before_publish: F,
5005 ) -> Result<StoredProcedure>
5006 where
5007 F: FnMut() -> Result<()>,
5008 {
5009 self.create_procedure_inner(procedure, Some(&mut before_publish))
5010 }
5011
5012 fn create_procedure_inner(
5013 &self,
5014 mut procedure: StoredProcedure,
5015 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5016 ) -> Result<StoredProcedure> {
5017 self.require(&crate::auth::Permission::Ddl)?;
5018 let _g = self.ddl_lock.lock();
5019 let _security_write = self.security_write()?;
5020 self.require(&crate::auth::Permission::Ddl)?;
5021 procedure.validate()?;
5022 self.validate_procedure_references(&procedure)?;
5023 {
5024 let cat = self.catalog.read();
5025 if cat
5026 .procedures
5027 .iter()
5028 .any(|p| p.procedure.name == procedure.name)
5029 {
5030 return Err(MongrelError::InvalidArgument(format!(
5031 "procedure {:?} already exists",
5032 procedure.name
5033 )));
5034 }
5035 }
5036 let commit_lock = Arc::clone(&self.commit_lock);
5037 let _c = commit_lock.lock();
5038 let epoch = self.epoch.bump_assigned();
5039 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5040 procedure.created_epoch = epoch.0;
5041 procedure.updated_epoch = epoch.0;
5042 let mut next_catalog = self.catalog.read().clone();
5043 next_catalog
5044 .procedures
5045 .push(ProcedureEntry::from(procedure.clone()));
5046 next_catalog.db_epoch = epoch.0;
5047 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5048 Ok(procedure)
5049 }
5050
5051 pub fn create_or_replace_procedure(
5052 &self,
5053 procedure: StoredProcedure,
5054 ) -> Result<StoredProcedure> {
5055 self.create_or_replace_procedure_inner(procedure, None)
5056 }
5057
5058 pub fn create_or_replace_procedure_controlled<F>(
5059 &self,
5060 procedure: StoredProcedure,
5061 mut before_publish: F,
5062 ) -> Result<StoredProcedure>
5063 where
5064 F: FnMut() -> Result<()>,
5065 {
5066 self.create_or_replace_procedure_inner(procedure, Some(&mut before_publish))
5067 }
5068
5069 fn create_or_replace_procedure_inner(
5070 &self,
5071 procedure: StoredProcedure,
5072 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5073 ) -> Result<StoredProcedure> {
5074 self.require(&crate::auth::Permission::Ddl)?;
5075 let _g = self.ddl_lock.lock();
5076 let _security_write = self.security_write()?;
5077 self.require(&crate::auth::Permission::Ddl)?;
5078 procedure.validate()?;
5079 self.validate_procedure_references(&procedure)?;
5080 let commit_lock = Arc::clone(&self.commit_lock);
5081 let _c = commit_lock.lock();
5082 let epoch = self.epoch.bump_assigned();
5083 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5084 let mut next_catalog = self.catalog.read().clone();
5085 let replaced = {
5086 let next = match next_catalog
5087 .procedures
5088 .iter()
5089 .position(|p| p.procedure.name == procedure.name)
5090 {
5091 Some(idx) => {
5092 let next = next_catalog.procedures[idx]
5093 .procedure
5094 .replaced(procedure.clone(), epoch.0)?;
5095 next_catalog.procedures[idx] = ProcedureEntry::from(next.clone());
5096 next
5097 }
5098 None => {
5099 let mut next = procedure;
5100 next.created_epoch = epoch.0;
5101 next.updated_epoch = epoch.0;
5102 next_catalog
5103 .procedures
5104 .push(ProcedureEntry::from(next.clone()));
5105 next
5106 }
5107 };
5108 next_catalog.db_epoch = epoch.0;
5109 next
5110 };
5111 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5112 Ok(replaced)
5113 }
5114
5115 pub fn drop_procedure(&self, name: &str) -> Result<()> {
5116 self.drop_procedure_with_epoch(name).map(|_| ())
5117 }
5118
5119 pub fn drop_procedure_with_epoch(&self, name: &str) -> Result<Epoch> {
5120 self.drop_procedure_with_epoch_inner(name, None)
5121 }
5122
5123 pub fn drop_procedure_with_epoch_controlled<F>(
5124 &self,
5125 name: &str,
5126 mut before_publish: F,
5127 ) -> Result<Epoch>
5128 where
5129 F: FnMut() -> Result<()>,
5130 {
5131 self.drop_procedure_with_epoch_inner(name, Some(&mut before_publish))
5132 }
5133
5134 fn drop_procedure_with_epoch_inner(
5135 &self,
5136 name: &str,
5137 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5138 ) -> Result<Epoch> {
5139 self.require(&crate::auth::Permission::Ddl)?;
5140 let _g = self.ddl_lock.lock();
5141 let _security_write = self.security_write()?;
5142 self.require(&crate::auth::Permission::Ddl)?;
5143 let commit_lock = Arc::clone(&self.commit_lock);
5144 let _c = commit_lock.lock();
5145 let epoch = self.epoch.bump_assigned();
5146 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5147 let mut next_catalog = self.catalog.read().clone();
5148 let before = next_catalog.procedures.len();
5149 next_catalog.procedures.retain(|p| p.procedure.name != name);
5150 if next_catalog.procedures.len() == before {
5151 return Err(MongrelError::NotFound(format!(
5152 "procedure {name:?} not found"
5153 )));
5154 }
5155 next_catalog.db_epoch = epoch.0;
5156 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5157 Ok(epoch)
5158 }
5159
5160 pub fn users(&self) -> Vec<crate::auth::UserEntry> {
5165 self.catalog.read().users.clone()
5166 }
5167
5168 pub fn user_identity(&self, username: &str) -> Option<(u64, u64)> {
5171 self.catalog
5172 .read()
5173 .users
5174 .iter()
5175 .find(|user| user.username == username)
5176 .map(|user| (user.id, user.created_epoch))
5177 }
5178
5179 pub fn security_version(&self) -> u64 {
5182 self.catalog.read().security_version
5183 }
5184
5185 pub fn roles(&self) -> Vec<crate::auth::RoleEntry> {
5187 self.catalog.read().roles.clone()
5188 }
5189
5190 pub fn create_user(&self, username: &str, password: &str) -> Result<crate::auth::UserEntry> {
5192 self.require(&crate::auth::Permission::Admin)?;
5193 let hash = crate::auth::hash_password(password).map_err(MongrelError::Other)?;
5194 self.create_user_with_password_hash(username, hash)
5195 }
5196
5197 pub fn create_user_with_password_hash(
5199 &self,
5200 username: &str,
5201 hash: String,
5202 ) -> Result<crate::auth::UserEntry> {
5203 self.create_user_with_password_hash_inner(username, hash, None)
5204 }
5205
5206 pub fn create_user_with_password_hash_controlled<F>(
5207 &self,
5208 username: &str,
5209 hash: String,
5210 mut before_publish: F,
5211 ) -> Result<crate::auth::UserEntry>
5212 where
5213 F: FnMut() -> Result<()>,
5214 {
5215 self.create_user_with_password_hash_inner(username, hash, Some(&mut before_publish))
5216 }
5217
5218 fn create_user_with_password_hash_inner(
5219 &self,
5220 username: &str,
5221 hash: String,
5222 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5223 ) -> Result<crate::auth::UserEntry> {
5224 self.require(&crate::auth::Permission::Admin)?;
5225 let _ddl = self.ddl_lock.lock();
5226 let _security_write = self.security_write()?;
5227 self.require(&crate::auth::Permission::Admin)?;
5228 let _commit = self.commit_lock.lock();
5229 let epoch = self.epoch.bump_assigned();
5230 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5231 let mut next_catalog = self.catalog.read().clone();
5232 if next_catalog.users.iter().any(|u| u.username == username) {
5233 return Err(MongrelError::InvalidArgument(format!(
5234 "user {username:?} already exists"
5235 )));
5236 }
5237 next_catalog.next_user_id = next_catalog.next_user_id.max(1);
5238 let id = next_catalog.next_user_id;
5239 next_catalog.next_user_id = id
5240 .checked_add(1)
5241 .ok_or_else(|| MongrelError::Full("user-id namespace exhausted".into()))?;
5242 let entry = crate::auth::UserEntry {
5243 id,
5244 username: username.into(),
5245 password_hash: hash,
5246 roles: Vec::new(),
5247 is_admin: false,
5248 created_epoch: epoch.0,
5249 };
5250 next_catalog.users.push(entry.clone());
5251 advance_security_version(&mut next_catalog)?;
5252 next_catalog.db_epoch = epoch.0;
5253 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5254 Ok(entry)
5255 }
5256
5257 pub fn drop_user(&self, username: &str) -> Result<()> {
5259 self.drop_user_with_epoch(username).map(|_| ())
5260 }
5261
5262 pub fn drop_user_with_epoch(&self, username: &str) -> Result<Epoch> {
5263 self.drop_user_with_epoch_inner(username, None)
5264 }
5265
5266 pub fn drop_user_with_epoch_controlled<F>(
5267 &self,
5268 username: &str,
5269 mut before_publish: F,
5270 ) -> Result<Epoch>
5271 where
5272 F: FnMut() -> Result<()>,
5273 {
5274 self.drop_user_with_epoch_inner(username, Some(&mut before_publish))
5275 }
5276
5277 fn drop_user_with_epoch_inner(
5278 &self,
5279 username: &str,
5280 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5281 ) -> Result<Epoch> {
5282 self.require(&crate::auth::Permission::Admin)?;
5283 let _ddl = self.ddl_lock.lock();
5284 let _security_write = self.security_write()?;
5285 self.require(&crate::auth::Permission::Admin)?;
5286 let _commit = self.commit_lock.lock();
5287 let epoch = self.epoch.bump_assigned();
5288 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5289 let mut next_catalog = self.catalog.read().clone();
5290 let before = next_catalog.users.len();
5291 next_catalog.users.retain(|u| u.username != username);
5292 if next_catalog.users.len() == before {
5293 return Err(MongrelError::NotFound(format!(
5294 "user {username:?} not found"
5295 )));
5296 }
5297 advance_security_version(&mut next_catalog)?;
5298 next_catalog.db_epoch = epoch.0;
5299 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5300 Ok(epoch)
5301 }
5302
5303 pub fn alter_user_password(&self, username: &str, new_password: &str) -> Result<()> {
5305 self.alter_user_password_with_epoch(username, new_password)
5306 .map(|_| ())
5307 }
5308
5309 pub fn alter_user_password_with_epoch(
5310 &self,
5311 username: &str,
5312 new_password: &str,
5313 ) -> Result<Epoch> {
5314 self.require(&crate::auth::Permission::Admin)?;
5315 let hash = crate::auth::hash_password(new_password).map_err(MongrelError::Other)?;
5316 self.alter_user_password_hash_with_epoch(username, hash)
5317 }
5318
5319 pub fn alter_user_password_hash_with_epoch(
5320 &self,
5321 username: &str,
5322 hash: String,
5323 ) -> Result<Epoch> {
5324 self.alter_user_password_hash_with_epoch_inner(username, hash, None)
5325 }
5326
5327 pub fn alter_user_password_hash_with_epoch_controlled<F>(
5328 &self,
5329 username: &str,
5330 hash: String,
5331 mut before_publish: F,
5332 ) -> Result<Epoch>
5333 where
5334 F: FnMut() -> Result<()>,
5335 {
5336 self.alter_user_password_hash_with_epoch_inner(username, hash, Some(&mut before_publish))
5337 }
5338
5339 fn alter_user_password_hash_with_epoch_inner(
5340 &self,
5341 username: &str,
5342 hash: String,
5343 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5344 ) -> Result<Epoch> {
5345 self.require(&crate::auth::Permission::Admin)?;
5346 let _ddl = self.ddl_lock.lock();
5347 let _security_write = self.security_write()?;
5348 self.require(&crate::auth::Permission::Admin)?;
5349 let _commit = self.commit_lock.lock();
5350 let epoch = self.epoch.bump_assigned();
5351 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5352 let mut next_catalog = self.catalog.read().clone();
5353 let user = next_catalog
5354 .users
5355 .iter_mut()
5356 .find(|u| u.username == username)
5357 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5358 user.password_hash = hash;
5359 advance_security_version(&mut next_catalog)?;
5360 next_catalog.db_epoch = epoch.0;
5361 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5362 Ok(epoch)
5363 }
5364
5365 pub fn verify_user(
5368 &self,
5369 username: &str,
5370 password: &str,
5371 ) -> Result<Option<crate::auth::UserEntry>> {
5372 let cat = self.catalog.read();
5373 let Some(user) = cat.users.iter().find(|u| u.username == username) else {
5374 return Ok(None);
5375 };
5376 if user.password_hash.is_empty() {
5377 return Ok(None);
5378 }
5379 let ok = crate::auth::verify_password(password, &user.password_hash)
5380 .map_err(MongrelError::Other)?;
5381 if ok {
5382 Ok(Some(user.clone()))
5383 } else {
5384 Ok(None)
5385 }
5386 }
5387
5388 pub fn authenticate_principal(
5392 &self,
5393 username: &str,
5394 password: &str,
5395 ) -> Result<Option<crate::auth::Principal>> {
5396 self.authenticate_principal_inner(username, password, || {})
5397 }
5398
5399 fn authenticate_principal_inner<F>(
5400 &self,
5401 username: &str,
5402 password: &str,
5403 after_verify: F,
5404 ) -> Result<Option<crate::auth::Principal>>
5405 where
5406 F: FnOnce(),
5407 {
5408 let catalog = self.catalog.read();
5409 let Some(user) = catalog.users.iter().find(|user| user.username == username) else {
5410 return Ok(None);
5411 };
5412 if user.password_hash.is_empty()
5413 || !crate::auth::verify_password(password, &user.password_hash)
5414 .map_err(MongrelError::Other)?
5415 {
5416 return Ok(None);
5417 }
5418 after_verify();
5419 Ok(Self::resolve_user_principal_from_catalog(&catalog, user))
5420 }
5421
5422 pub fn set_user_admin(&self, username: &str, is_admin: bool) -> Result<()> {
5424 self.set_user_admin_with_epoch(username, is_admin)
5425 .map(|_| ())
5426 }
5427
5428 pub fn set_user_admin_with_epoch(
5429 &self,
5430 username: &str,
5431 is_admin: bool,
5432 ) -> Result<Option<Epoch>> {
5433 self.set_user_admin_with_epoch_inner(username, is_admin, None)
5434 }
5435
5436 pub fn set_user_admin_with_epoch_controlled<F>(
5437 &self,
5438 username: &str,
5439 is_admin: bool,
5440 mut before_publish: F,
5441 ) -> Result<Option<Epoch>>
5442 where
5443 F: FnMut() -> Result<()>,
5444 {
5445 self.set_user_admin_with_epoch_inner(username, is_admin, Some(&mut before_publish))
5446 }
5447
5448 fn set_user_admin_with_epoch_inner(
5449 &self,
5450 username: &str,
5451 is_admin: bool,
5452 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5453 ) -> Result<Option<Epoch>> {
5454 self.require(&crate::auth::Permission::Admin)?;
5455 let _ddl = self.ddl_lock.lock();
5456 let _security_write = self.security_write()?;
5457 self.require(&crate::auth::Permission::Admin)?;
5458 let _commit = self.commit_lock.lock();
5459 let mut next_catalog = self.catalog.read().clone();
5460 let user = next_catalog
5461 .users
5462 .iter_mut()
5463 .find(|u| u.username == username)
5464 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5465 if user.is_admin == is_admin {
5466 return Ok(None);
5467 }
5468 user.is_admin = is_admin;
5469 let epoch = self.epoch.bump_assigned();
5470 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5471 advance_security_version(&mut next_catalog)?;
5472 next_catalog.db_epoch = epoch.0;
5473 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5474 Ok(Some(epoch))
5475 }
5476
5477 pub fn create_role(&self, name: &str) -> Result<crate::auth::RoleEntry> {
5479 self.create_role_inner(name, None)
5480 }
5481
5482 pub fn create_role_controlled<F>(
5483 &self,
5484 name: &str,
5485 mut before_publish: F,
5486 ) -> Result<crate::auth::RoleEntry>
5487 where
5488 F: FnMut() -> Result<()>,
5489 {
5490 self.create_role_inner(name, Some(&mut before_publish))
5491 }
5492
5493 fn create_role_inner(
5494 &self,
5495 name: &str,
5496 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5497 ) -> Result<crate::auth::RoleEntry> {
5498 self.require(&crate::auth::Permission::Admin)?;
5499 let _ddl = self.ddl_lock.lock();
5500 let _security_write = self.security_write()?;
5501 self.require(&crate::auth::Permission::Admin)?;
5502 let _commit = self.commit_lock.lock();
5503 let epoch = self.epoch.bump_assigned();
5504 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5505 let mut next_catalog = self.catalog.read().clone();
5506 if next_catalog.roles.iter().any(|r| r.name == name) {
5507 return Err(MongrelError::InvalidArgument(format!(
5508 "role {name:?} already exists"
5509 )));
5510 }
5511 let entry = crate::auth::RoleEntry {
5512 name: name.into(),
5513 permissions: Vec::new(),
5514 created_epoch: epoch.0,
5515 };
5516 next_catalog.roles.push(entry.clone());
5517 advance_security_version(&mut next_catalog)?;
5518 next_catalog.db_epoch = epoch.0;
5519 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5520 Ok(entry)
5521 }
5522
5523 pub fn drop_role(&self, name: &str) -> Result<()> {
5525 self.drop_role_with_epoch(name).map(|_| ())
5526 }
5527
5528 pub fn drop_role_with_epoch(&self, name: &str) -> Result<Epoch> {
5529 self.drop_role_with_epoch_inner(name, None)
5530 }
5531
5532 pub fn drop_role_with_epoch_controlled<F>(
5533 &self,
5534 name: &str,
5535 mut before_publish: F,
5536 ) -> Result<Epoch>
5537 where
5538 F: FnMut() -> Result<()>,
5539 {
5540 self.drop_role_with_epoch_inner(name, Some(&mut before_publish))
5541 }
5542
5543 fn drop_role_with_epoch_inner(
5544 &self,
5545 name: &str,
5546 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5547 ) -> Result<Epoch> {
5548 self.require(&crate::auth::Permission::Admin)?;
5549 let _ddl = self.ddl_lock.lock();
5550 let _security_write = self.security_write()?;
5551 self.require(&crate::auth::Permission::Admin)?;
5552 let _commit = self.commit_lock.lock();
5553 let epoch = self.epoch.bump_assigned();
5554 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5555 let mut next_catalog = self.catalog.read().clone();
5556 let before = next_catalog.roles.len();
5557 next_catalog.roles.retain(|r| r.name != name);
5558 if next_catalog.roles.len() == before {
5559 return Err(MongrelError::NotFound(format!("role {name:?} not found")));
5560 }
5561 for user in &mut next_catalog.users {
5562 user.roles.retain(|r| r != name);
5563 }
5564 advance_security_version(&mut next_catalog)?;
5565 next_catalog.db_epoch = epoch.0;
5566 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5567 Ok(epoch)
5568 }
5569
5570 pub fn grant_role(&self, username: &str, role_name: &str) -> Result<()> {
5572 self.grant_role_with_epoch(username, role_name).map(|_| ())
5573 }
5574
5575 pub fn grant_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
5576 self.grant_role_with_epoch_inner(username, role_name, None)
5577 }
5578
5579 pub fn grant_role_with_epoch_controlled<F>(
5580 &self,
5581 username: &str,
5582 role_name: &str,
5583 mut before_publish: F,
5584 ) -> Result<Option<Epoch>>
5585 where
5586 F: FnMut() -> Result<()>,
5587 {
5588 self.grant_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
5589 }
5590
5591 fn grant_role_with_epoch_inner(
5592 &self,
5593 username: &str,
5594 role_name: &str,
5595 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5596 ) -> Result<Option<Epoch>> {
5597 self.require(&crate::auth::Permission::Admin)?;
5598 let _ddl = self.ddl_lock.lock();
5599 let _security_write = self.security_write()?;
5600 self.require(&crate::auth::Permission::Admin)?;
5601 let _commit = self.commit_lock.lock();
5602 let mut next_catalog = self.catalog.read().clone();
5603 if !next_catalog.roles.iter().any(|r| r.name == role_name) {
5604 return Err(MongrelError::NotFound(format!(
5605 "role {role_name:?} not found"
5606 )));
5607 }
5608 let user = next_catalog
5609 .users
5610 .iter_mut()
5611 .find(|u| u.username == username)
5612 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5613 if user.roles.iter().any(|role| role == role_name) {
5614 return Ok(None);
5615 }
5616 user.roles.push(role_name.into());
5617 let epoch = self.epoch.bump_assigned();
5618 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5619 advance_security_version(&mut next_catalog)?;
5620 next_catalog.db_epoch = epoch.0;
5621 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5622 Ok(Some(epoch))
5623 }
5624
5625 pub fn revoke_role(&self, username: &str, role_name: &str) -> Result<()> {
5627 self.revoke_role_with_epoch(username, role_name).map(|_| ())
5628 }
5629
5630 pub fn revoke_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
5631 self.revoke_role_with_epoch_inner(username, role_name, None)
5632 }
5633
5634 pub fn revoke_role_with_epoch_controlled<F>(
5635 &self,
5636 username: &str,
5637 role_name: &str,
5638 mut before_publish: F,
5639 ) -> Result<Option<Epoch>>
5640 where
5641 F: FnMut() -> Result<()>,
5642 {
5643 self.revoke_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
5644 }
5645
5646 fn revoke_role_with_epoch_inner(
5647 &self,
5648 username: &str,
5649 role_name: &str,
5650 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5651 ) -> Result<Option<Epoch>> {
5652 self.require(&crate::auth::Permission::Admin)?;
5653 let _ddl = self.ddl_lock.lock();
5654 let _security_write = self.security_write()?;
5655 self.require(&crate::auth::Permission::Admin)?;
5656 let _commit = self.commit_lock.lock();
5657 let mut next_catalog = self.catalog.read().clone();
5658 let user = next_catalog
5659 .users
5660 .iter_mut()
5661 .find(|u| u.username == username)
5662 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5663 let before = user.roles.len();
5664 user.roles.retain(|r| r != role_name);
5665 if user.roles.len() == before {
5666 return Ok(None);
5667 }
5668 let epoch = self.epoch.bump_assigned();
5669 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5670 advance_security_version(&mut next_catalog)?;
5671 next_catalog.db_epoch = epoch.0;
5672 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5673 Ok(Some(epoch))
5674 }
5675
5676 pub fn grant_permission(
5678 &self,
5679 role_name: &str,
5680 permission: crate::auth::Permission,
5681 ) -> Result<()> {
5682 self.grant_permission_with_epoch(role_name, permission)
5683 .map(|_| ())
5684 }
5685
5686 pub fn grant_permission_with_epoch(
5687 &self,
5688 role_name: &str,
5689 permission: crate::auth::Permission,
5690 ) -> Result<Option<Epoch>> {
5691 self.grant_permission_with_epoch_inner(role_name, permission, None)
5692 }
5693
5694 pub fn grant_permission_with_epoch_controlled<F>(
5695 &self,
5696 role_name: &str,
5697 permission: crate::auth::Permission,
5698 mut before_publish: F,
5699 ) -> Result<Option<Epoch>>
5700 where
5701 F: FnMut() -> Result<()>,
5702 {
5703 self.grant_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
5704 }
5705
5706 fn grant_permission_with_epoch_inner(
5707 &self,
5708 role_name: &str,
5709 permission: crate::auth::Permission,
5710 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5711 ) -> Result<Option<Epoch>> {
5712 self.require(&crate::auth::Permission::Admin)?;
5713 let _ddl = self.ddl_lock.lock();
5714 let _security_write = self.security_write()?;
5715 self.require(&crate::auth::Permission::Admin)?;
5716 let _commit = self.commit_lock.lock();
5717 let mut next_catalog = self.catalog.read().clone();
5718 let role = next_catalog
5719 .roles
5720 .iter_mut()
5721 .find(|r| r.name == role_name)
5722 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
5723 let before = role.permissions.clone();
5724 merge_permission(&mut role.permissions, permission);
5725 if role.permissions == before {
5726 return Ok(None);
5727 }
5728 let epoch = self.epoch.bump_assigned();
5729 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5730 advance_security_version(&mut next_catalog)?;
5731 next_catalog.db_epoch = epoch.0;
5732 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5733 Ok(Some(epoch))
5734 }
5735
5736 pub fn revoke_permission(
5738 &self,
5739 role_name: &str,
5740 permission: crate::auth::Permission,
5741 ) -> Result<()> {
5742 self.revoke_permission_with_epoch(role_name, permission)
5743 .map(|_| ())
5744 }
5745
5746 pub fn revoke_permission_with_epoch(
5747 &self,
5748 role_name: &str,
5749 permission: crate::auth::Permission,
5750 ) -> Result<Option<Epoch>> {
5751 self.revoke_permission_with_epoch_inner(role_name, permission, None)
5752 }
5753
5754 pub fn revoke_permission_with_epoch_controlled<F>(
5755 &self,
5756 role_name: &str,
5757 permission: crate::auth::Permission,
5758 mut before_publish: F,
5759 ) -> Result<Option<Epoch>>
5760 where
5761 F: FnMut() -> Result<()>,
5762 {
5763 self.revoke_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
5764 }
5765
5766 fn revoke_permission_with_epoch_inner(
5767 &self,
5768 role_name: &str,
5769 permission: crate::auth::Permission,
5770 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5771 ) -> Result<Option<Epoch>> {
5772 self.require(&crate::auth::Permission::Admin)?;
5773 let _ddl = self.ddl_lock.lock();
5774 let _security_write = self.security_write()?;
5775 self.require(&crate::auth::Permission::Admin)?;
5776 let _commit = self.commit_lock.lock();
5777 let mut next_catalog = self.catalog.read().clone();
5778 let role = next_catalog
5779 .roles
5780 .iter_mut()
5781 .find(|r| r.name == role_name)
5782 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
5783 let before = role.permissions.clone();
5784 revoke_permission_from(&mut role.permissions, &permission);
5785 if role.permissions == before {
5786 return Ok(None);
5787 }
5788 let epoch = self.epoch.bump_assigned();
5789 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5790 advance_security_version(&mut next_catalog)?;
5791 next_catalog.db_epoch = epoch.0;
5792 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5793 Ok(Some(epoch))
5794 }
5795
5796 pub fn resolve_principal(&self, username: &str) -> Option<crate::auth::Principal> {
5799 let cat = self.catalog.read();
5800 Self::resolve_principal_from_catalog(&cat, username)
5801 }
5802
5803 pub fn resolve_current_principal(
5806 &self,
5807 principal: &crate::auth::Principal,
5808 ) -> Option<crate::auth::Principal> {
5809 Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
5810 }
5811
5812 fn resolve_principal_from_catalog(
5817 cat: &Catalog,
5818 username: &str,
5819 ) -> Option<crate::auth::Principal> {
5820 let user = cat.users.iter().find(|u| u.username == username)?;
5821 Self::resolve_user_principal_from_catalog(cat, user)
5822 }
5823
5824 fn resolve_bound_principal_from_catalog(
5825 cat: &Catalog,
5826 principal: &crate::auth::Principal,
5827 ) -> Option<crate::auth::Principal> {
5828 let user = cat.users.iter().find(|user| {
5829 user.id == principal.user_id
5830 && user.created_epoch == principal.created_epoch
5831 && user.username == principal.username
5832 })?;
5833 Self::resolve_user_principal_from_catalog(cat, user)
5834 }
5835
5836 fn resolve_user_principal_from_catalog(
5837 cat: &Catalog,
5838 user: &crate::auth::UserEntry,
5839 ) -> Option<crate::auth::Principal> {
5840 let mut permissions = Vec::new();
5841 for role_name in &user.roles {
5842 if let Some(role) = cat.roles.iter().find(|r| &r.name == role_name) {
5843 permissions.extend(role.permissions.iter().cloned());
5844 }
5845 }
5846 Some(crate::auth::Principal {
5847 user_id: user.id,
5848 created_epoch: user.created_epoch,
5849 username: user.username.clone(),
5850 is_admin: user.is_admin,
5851 roles: user.roles.clone(),
5852 permissions,
5853 })
5854 }
5855
5856 pub fn check_permission(&self, username: &str, permission: &crate::auth::Permission) -> bool {
5858 match self.resolve_principal(username) {
5859 Some(p) => p.has_permission(permission),
5860 None => false,
5861 }
5862 }
5863
5864 pub fn require_auth_enabled(&self) -> bool {
5868 self.catalog.read().require_auth
5869 }
5870
5871 pub fn principal(&self) -> Option<crate::auth::Principal> {
5875 self.principal.read().clone()
5876 }
5877
5878 fn table_auth_checker(&self) -> Option<Arc<dyn crate::auth_state::TableAuthChecker>> {
5884 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
5885 self.auth_state.clone(),
5886 )))
5887 }
5888
5889 pub fn refresh_principal(&self) -> Result<()> {
5903 let previous = match self.principal.read().clone() {
5904 Some(principal) => principal,
5905 None => return Ok(()),
5906 };
5907 let observed_version = self.security_coordinator.version.load(Ordering::Acquire);
5908 self.refresh_security_catalog_if_stale(observed_version)?;
5909 let cat = self.catalog.read();
5910 match Self::resolve_bound_principal_from_catalog(&cat, &previous) {
5911 Some(p) => {
5912 *self.principal.write() = Some(p.clone());
5913 self.auth_state.set_principal(Some(p));
5917 Ok(())
5918 }
5919 None => Err(MongrelError::InvalidCredentials {
5920 username: previous.username,
5921 }),
5922 }
5923 }
5924
5925 pub fn security_catalog_disk_read_count(&self) -> u64 {
5928 self.security_catalog_disk_reads.load(Ordering::Relaxed)
5929 }
5930
5931 pub fn enable_auth(&self, admin_username: &str, admin_password: &str) -> Result<()> {
5943 let password_hash =
5944 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
5945 let _ddl = self.ddl_lock.lock();
5946 let _security_write = self.security_write()?;
5947 let _commit = self.commit_lock.lock();
5948 let epoch = self.epoch.bump_assigned();
5949 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5950 let mut next_catalog = self.catalog.read().clone();
5951 if next_catalog.require_auth {
5952 return Err(MongrelError::InvalidArgument(
5953 "database already has require_auth enabled".into(),
5954 ));
5955 }
5956 if next_catalog
5957 .users
5958 .iter()
5959 .any(|u| u.username == admin_username)
5960 {
5961 return Err(MongrelError::InvalidArgument(format!(
5962 "user {admin_username:?} already exists"
5963 )));
5964 }
5965 next_catalog.next_user_id = next_catalog.next_user_id.max(1);
5966 let id = next_catalog.next_user_id;
5967 next_catalog.next_user_id = id
5968 .checked_add(1)
5969 .ok_or_else(|| MongrelError::Full("user-id namespace exhausted".into()))?;
5970 next_catalog.users.push(crate::auth::UserEntry {
5971 id,
5972 username: admin_username.to_string(),
5973 password_hash,
5974 roles: Vec::new(),
5975 is_admin: true,
5976 created_epoch: epoch.0,
5977 });
5978 next_catalog.require_auth = true;
5979 advance_security_version(&mut next_catalog)?;
5980 next_catalog.db_epoch = epoch.0;
5981 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
5982 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
5986 let principal = crate::auth::Principal {
5987 user_id: id,
5988 created_epoch: epoch.0,
5989 username: admin_username.to_string(),
5990 is_admin: true,
5991 roles: Vec::new(),
5992 permissions: Vec::new(),
5993 };
5994 *self.principal.write() = Some(principal.clone());
5995 self.auth_state.set_principal(Some(principal));
5996 }
5997 publish
5998 }
5999
6000 pub fn disable_auth(&self) -> Result<()> {
6017 let _ddl = self.ddl_lock.lock();
6018 let _security_write = self.security_write()?;
6019 let _commit = self.commit_lock.lock();
6020 let epoch = self.epoch.bump_assigned();
6021 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6022 let mut next_catalog = self.catalog.read().clone();
6023 if !next_catalog.require_auth {
6024 return Err(MongrelError::InvalidArgument(
6025 "database does not have require_auth enabled".into(),
6026 ));
6027 }
6028 next_catalog.require_auth = false;
6029 advance_security_version(&mut next_catalog)?;
6030 next_catalog.db_epoch = epoch.0;
6031 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
6032 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
6034 *self.principal.write() = None;
6035 }
6036 publish
6037 }
6038
6039 pub fn require(&self, perm: &crate::auth::Permission) -> Result<()> {
6046 if self.read_only && !matches!(perm, crate::auth::Permission::Select { .. }) {
6047 return Err(MongrelError::ReadOnlyReplica);
6048 }
6049 if self.principal.read().is_some() {
6050 self.refresh_principal().map_err(|error| match error {
6051 MongrelError::InvalidCredentials { .. } => MongrelError::AuthRequired,
6052 error => error,
6053 })?;
6054 }
6055 if !self.catalog.read().require_auth {
6056 return Ok(());
6057 }
6058 let guard = self.principal.read();
6059 let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
6060 if p.has_permission(perm) {
6061 Ok(())
6062 } else {
6063 Err(MongrelError::PermissionDenied {
6064 required: perm.clone(),
6065 principal: p.username.clone(),
6066 })
6067 }
6068 }
6069
6070 pub fn require_table(
6075 &self,
6076 table: &str,
6077 perm: crate::auth_state::RequiredPermission,
6078 ) -> Result<()> {
6079 self.require(&perm.into_permission(table))
6080 }
6081
6082 pub fn triggers(&self) -> Vec<StoredTrigger> {
6083 self.catalog
6084 .read()
6085 .triggers
6086 .iter()
6087 .map(|t| t.trigger.clone())
6088 .collect()
6089 }
6090
6091 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
6092 self.catalog
6093 .read()
6094 .triggers
6095 .iter()
6096 .find(|t| t.trigger.name == name)
6097 .map(|t| t.trigger.clone())
6098 }
6099
6100 pub fn create_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
6101 self.create_trigger_inner(trigger, None, None)
6102 }
6103
6104 pub fn create_trigger_controlled<F>(
6105 &self,
6106 trigger: StoredTrigger,
6107 mut before_publish: F,
6108 ) -> Result<StoredTrigger>
6109 where
6110 F: FnMut() -> Result<()>,
6111 {
6112 self.create_trigger_inner(trigger, None, Some(&mut before_publish))
6113 }
6114
6115 pub fn create_trigger_as_controlled<F>(
6116 &self,
6117 trigger: StoredTrigger,
6118 principal: Option<&crate::auth::Principal>,
6119 mut before_publish: F,
6120 ) -> Result<StoredTrigger>
6121 where
6122 F: FnMut() -> Result<()>,
6123 {
6124 self.create_trigger_inner(trigger, principal, Some(&mut before_publish))
6125 }
6126
6127 fn create_trigger_inner(
6128 &self,
6129 mut trigger: StoredTrigger,
6130 principal: Option<&crate::auth::Principal>,
6131 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6132 ) -> Result<StoredTrigger> {
6133 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6134 let _g = self.ddl_lock.lock();
6135 let _security_write = self.security_write()?;
6136 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6137 trigger.validate()?;
6138 self.validate_trigger_references(&trigger)
6139 .map_err(trigger_validation_error)?;
6140 {
6141 let cat = self.catalog.read();
6142 if cat.triggers.iter().any(|t| t.trigger.name == trigger.name) {
6143 return Err(MongrelError::TriggerValidation(format!(
6144 "trigger {:?} already exists",
6145 trigger.name
6146 )));
6147 }
6148 }
6149 let commit_lock = Arc::clone(&self.commit_lock);
6150 let _c = commit_lock.lock();
6151 let epoch = self.epoch.bump_assigned();
6152 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6153 trigger.created_epoch = epoch.0;
6154 trigger.updated_epoch = epoch.0;
6155 let mut next_catalog = self.catalog.read().clone();
6156 next_catalog
6157 .triggers
6158 .push(TriggerEntry::from(trigger.clone()));
6159 next_catalog.db_epoch = epoch.0;
6160 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6161 Ok(trigger)
6162 }
6163
6164 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
6165 self.create_or_replace_trigger_inner(trigger, None, None)
6166 }
6167
6168 pub fn create_or_replace_trigger_controlled<F>(
6169 &self,
6170 trigger: StoredTrigger,
6171 mut before_publish: F,
6172 ) -> Result<StoredTrigger>
6173 where
6174 F: FnMut() -> Result<()>,
6175 {
6176 self.create_or_replace_trigger_inner(trigger, None, Some(&mut before_publish))
6177 }
6178
6179 pub fn create_or_replace_trigger_as_controlled<F>(
6180 &self,
6181 trigger: StoredTrigger,
6182 principal: Option<&crate::auth::Principal>,
6183 mut before_publish: F,
6184 ) -> Result<StoredTrigger>
6185 where
6186 F: FnMut() -> Result<()>,
6187 {
6188 self.create_or_replace_trigger_inner(trigger, principal, Some(&mut before_publish))
6189 }
6190
6191 fn create_or_replace_trigger_inner(
6192 &self,
6193 trigger: StoredTrigger,
6194 principal: Option<&crate::auth::Principal>,
6195 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6196 ) -> Result<StoredTrigger> {
6197 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6198 let _g = self.ddl_lock.lock();
6199 let _security_write = self.security_write()?;
6200 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6201 trigger.validate()?;
6202 self.validate_trigger_references(&trigger)
6203 .map_err(trigger_validation_error)?;
6204 let commit_lock = Arc::clone(&self.commit_lock);
6205 let _c = commit_lock.lock();
6206 let epoch = self.epoch.bump_assigned();
6207 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6208 let mut next_catalog = self.catalog.read().clone();
6209 let replaced = {
6210 let next = match next_catalog
6211 .triggers
6212 .iter()
6213 .position(|t| t.trigger.name == trigger.name)
6214 {
6215 Some(idx) => {
6216 let next = next_catalog.triggers[idx]
6217 .trigger
6218 .replaced(trigger.clone(), epoch.0)?;
6219 next_catalog.triggers[idx] = TriggerEntry::from(next.clone());
6220 next
6221 }
6222 None => {
6223 let mut next = trigger;
6224 next.created_epoch = epoch.0;
6225 next.updated_epoch = epoch.0;
6226 next_catalog.triggers.push(TriggerEntry::from(next.clone()));
6227 next
6228 }
6229 };
6230 next_catalog.db_epoch = epoch.0;
6231 next
6232 };
6233 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6234 Ok(replaced)
6235 }
6236
6237 pub fn drop_trigger(&self, name: &str) -> Result<()> {
6238 self.drop_trigger_with_epoch(name).map(|_| ())
6239 }
6240
6241 pub fn drop_trigger_with_epoch(&self, name: &str) -> Result<Epoch> {
6243 self.drop_triggers_with_epoch(&[name.to_string()])
6244 }
6245
6246 pub fn drop_trigger_with_epoch_controlled<F>(
6247 &self,
6248 name: &str,
6249 before_publish: F,
6250 ) -> Result<Epoch>
6251 where
6252 F: FnMut() -> Result<()>,
6253 {
6254 self.drop_triggers_with_epoch_controlled(&[name.to_string()], before_publish)
6255 }
6256
6257 pub fn drop_triggers_with_epoch(&self, names: &[String]) -> Result<Epoch> {
6259 self.drop_triggers_with_epoch_inner(names, None, None)
6260 }
6261
6262 pub fn drop_triggers_with_epoch_controlled<F>(
6263 &self,
6264 names: &[String],
6265 mut before_publish: F,
6266 ) -> Result<Epoch>
6267 where
6268 F: FnMut() -> Result<()>,
6269 {
6270 self.drop_triggers_with_epoch_inner(names, None, Some(&mut before_publish))
6271 }
6272
6273 pub fn drop_triggers_with_epoch_as_controlled<F>(
6274 &self,
6275 names: &[String],
6276 principal: Option<&crate::auth::Principal>,
6277 mut before_publish: F,
6278 ) -> Result<Epoch>
6279 where
6280 F: FnMut() -> Result<()>,
6281 {
6282 self.drop_triggers_with_epoch_inner(names, principal, Some(&mut before_publish))
6283 }
6284
6285 fn drop_triggers_with_epoch_inner(
6286 &self,
6287 names: &[String],
6288 principal: Option<&crate::auth::Principal>,
6289 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6290 ) -> Result<Epoch> {
6291 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6292 if names.is_empty() {
6293 return Err(MongrelError::InvalidArgument(
6294 "at least one trigger name is required".into(),
6295 ));
6296 }
6297 let _g = self.ddl_lock.lock();
6298 let _security_write = self.security_write()?;
6299 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6300 {
6301 let cat = self.catalog.read();
6302 for name in names {
6303 if !cat.triggers.iter().any(|t| t.trigger.name == *name) {
6304 return Err(MongrelError::NotFound(format!(
6305 "trigger {name:?} not found"
6306 )));
6307 }
6308 }
6309 }
6310 let commit_lock = Arc::clone(&self.commit_lock);
6311 let _c = commit_lock.lock();
6312 let epoch = self.epoch.bump_assigned();
6313 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6314 let mut next_catalog = self.catalog.read().clone();
6315 next_catalog
6316 .triggers
6317 .retain(|trigger| !names.contains(&trigger.trigger.name));
6318 next_catalog.db_epoch = epoch.0;
6319 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6320 Ok(epoch)
6321 }
6322
6323 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
6324 self.catalog.read().external_tables.clone()
6325 }
6326
6327 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
6328 self.catalog
6329 .read()
6330 .external_tables
6331 .iter()
6332 .find(|t| t.name == name)
6333 .cloned()
6334 }
6335
6336 pub fn create_external_table(&self, entry: ExternalTableEntry) -> Result<ExternalTableEntry> {
6337 self.create_external_table_inner(entry, None)
6338 }
6339
6340 pub fn create_external_table_controlled<F>(
6341 &self,
6342 entry: ExternalTableEntry,
6343 mut before_publish: F,
6344 ) -> Result<ExternalTableEntry>
6345 where
6346 F: FnMut() -> Result<()>,
6347 {
6348 self.create_external_table_inner(entry, Some(&mut before_publish))
6349 }
6350
6351 fn create_external_table_inner(
6352 &self,
6353 mut entry: ExternalTableEntry,
6354 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6355 ) -> Result<ExternalTableEntry> {
6356 self.require(&crate::auth::Permission::Ddl)?;
6357 let _g = self.ddl_lock.lock();
6358 let _security_write = self.security_write()?;
6359 self.require(&crate::auth::Permission::Ddl)?;
6360 entry.validate()?;
6361 {
6362 let cat = self.catalog.read();
6363 if cat.live(&entry.name).is_some()
6364 || cat.external_tables.iter().any(|t| t.name == entry.name)
6365 {
6366 return Err(MongrelError::InvalidArgument(format!(
6367 "table {:?} already exists",
6368 entry.name
6369 )));
6370 }
6371 }
6372 let commit_lock = Arc::clone(&self.commit_lock);
6373 let _c = commit_lock.lock();
6374 crate::durable_file::create_directory(&self.root.join(VTAB_DIR))?;
6378 crate::durable_file::remove_directory_all(&self.root.join(VTAB_DIR).join(&entry.name))?;
6379 let epoch = self.epoch.bump_assigned();
6380 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6381 entry.created_epoch = epoch.0;
6382 let mut next_catalog = self.catalog.read().clone();
6383 next_catalog.external_tables.push(entry.clone());
6384 next_catalog.db_epoch = epoch.0;
6385 self.publish_catalog_candidate_with_prelude(
6386 next_catalog,
6387 epoch,
6388 &mut _epoch_guard,
6389 before_publish,
6390 vec![(
6391 EXTERNAL_TABLE_ID,
6392 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
6393 name: entry.name.clone(),
6394 generation_epoch: epoch.0,
6395 }),
6396 )],
6397 )?;
6398 Ok(entry)
6399 }
6400
6401 pub fn drop_external_table(&self, name: &str) -> Result<()> {
6402 self.drop_external_table_with_epoch(name).map(|_| ())
6403 }
6404
6405 pub fn drop_external_table_with_epoch(&self, name: &str) -> Result<Epoch> {
6407 self.drop_external_table_with_epoch_inner(name, None)
6408 }
6409
6410 pub fn drop_external_table_with_epoch_controlled<F>(
6411 &self,
6412 name: &str,
6413 mut before_publish: F,
6414 ) -> Result<Epoch>
6415 where
6416 F: FnMut() -> Result<()>,
6417 {
6418 self.drop_external_table_with_epoch_inner(name, Some(&mut before_publish))
6419 }
6420
6421 fn drop_external_table_with_epoch_inner(
6422 &self,
6423 name: &str,
6424 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6425 ) -> Result<Epoch> {
6426 self.require(&crate::auth::Permission::Ddl)?;
6427 let _g = self.ddl_lock.lock();
6428 let _security_write = self.security_write()?;
6429 self.require(&crate::auth::Permission::Ddl)?;
6430 let commit_lock = Arc::clone(&self.commit_lock);
6431 let _c = commit_lock.lock();
6432 let epoch = self.epoch.bump_assigned();
6433 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6434 let mut next_catalog = self.catalog.read().clone();
6435 let before = next_catalog.external_tables.len();
6436 next_catalog.external_tables.retain(|t| t.name != name);
6437 if next_catalog.external_tables.len() == before {
6438 return Err(MongrelError::NotFound(format!(
6439 "external table {name:?} not found"
6440 )));
6441 }
6442 next_catalog.db_epoch = epoch.0;
6443 self.publish_catalog_candidate_with_prelude(
6444 next_catalog,
6445 epoch,
6446 &mut _epoch_guard,
6447 before_publish,
6448 vec![(
6449 EXTERNAL_TABLE_ID,
6450 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
6451 name: name.to_string(),
6452 generation_epoch: epoch.0,
6453 }),
6454 )],
6455 )?;
6456 let state_dir = self.root.join(VTAB_DIR).join(name);
6457 if let Err(error) = crate::durable_file::remove_directory_all(&state_dir) {
6458 return Err(MongrelError::DurableCommit {
6459 epoch: epoch.0,
6460 message: format!(
6461 "external table was dropped but connector-state cleanup failed: {error}"
6462 ),
6463 });
6464 }
6465 Ok(epoch)
6466 }
6467
6468 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
6469 let txn_id = self.alloc_txn_id()?;
6470 let (principal, catalog_bound) = self.transaction_principal_snapshot();
6471 self.commit_transaction_with_external_states(
6472 txn_id,
6473 self.epoch.visible(),
6474 Vec::new(),
6475 vec![(name.to_string(), state.to_vec())],
6476 Vec::new(),
6477 principal,
6478 catalog_bound,
6479 None,
6480 )
6481 .map(|(epoch, _)| epoch)
6482 }
6483
6484 pub fn trigger_config(&self) -> TriggerConfig {
6485 use std::sync::atomic::Ordering;
6486 TriggerConfig {
6487 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
6488 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
6489 max_loop_iterations: self.trigger_max_loop_iterations.load(Ordering::Relaxed),
6490 }
6491 }
6492
6493 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
6494 use std::sync::atomic::Ordering;
6495 if config.max_depth == 0 {
6496 return Err(MongrelError::InvalidArgument(
6497 "trigger max_depth must be greater than 0".into(),
6498 ));
6499 }
6500 self.trigger_recursive
6501 .store(config.recursive_triggers, Ordering::Relaxed);
6502 self.trigger_max_depth
6503 .store(config.max_depth, Ordering::Relaxed);
6504 self.trigger_max_loop_iterations
6505 .store(config.max_loop_iterations, Ordering::Relaxed);
6506 Ok(())
6507 }
6508
6509 pub fn set_recursive_triggers(&self, recursive: bool) {
6510 use std::sync::atomic::Ordering;
6511 self.trigger_recursive.store(recursive, Ordering::Relaxed);
6512 }
6513
6514 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent> {
6518 self.notify.subscribe()
6519 }
6520
6521 pub fn subscribe_change_commits(&self) -> tokio::sync::broadcast::Receiver<()> {
6522 self.change_wake.subscribe()
6523 }
6524
6525 pub fn change_events_since(&self, last_event_id: Option<&str>) -> Result<CdcBatch> {
6530 let control = crate::ExecutionControl::new(None);
6531 self.change_events_since_controlled(last_event_id, &control)
6532 }
6533
6534 pub fn change_events_since_controlled(
6536 &self,
6537 last_event_id: Option<&str>,
6538 control: &crate::ExecutionControl,
6539 ) -> Result<CdcBatch> {
6540 use crate::wal::Op;
6541
6542 control.checkpoint()?;
6543 let resume = match last_event_id {
6544 Some(id) => {
6545 let (epoch, index) = id.split_once(':').ok_or_else(|| {
6546 MongrelError::InvalidArgument(format!(
6547 "invalid CDC event id {id:?}; expected <epoch>:<index>"
6548 ))
6549 })?;
6550 Some((
6551 epoch.parse::<u64>().map_err(|error| {
6552 MongrelError::InvalidArgument(format!("invalid CDC epoch: {error}"))
6553 })?,
6554 index.parse::<u32>().map_err(|error| {
6555 MongrelError::InvalidArgument(format!("invalid CDC index: {error}"))
6556 })?,
6557 ))
6558 }
6559 None => None,
6560 };
6561
6562 let mut wal = self.shared_wal.lock();
6563 wal.group_sync()?;
6564 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
6565 let records = crate::wal::SharedWal::replay_with_dek_controlled(
6566 &self.root,
6567 wal_dek.as_ref(),
6568 control,
6569 CDC_MAX_WAL_RECORDS,
6570 CDC_MAX_WAL_REPLAY_BYTES,
6571 )?;
6572 drop(wal);
6573 control.checkpoint()?;
6574
6575 let mut commits: HashMap<u64, (u64, Vec<crate::wal::AddedRun>)> = HashMap::new();
6576 let mut spilled_payloads: HashMap<(u64, u64), Vec<&[u8]>> = HashMap::new();
6577 for (index, record) in records.iter().enumerate() {
6578 if index % 256 == 0 {
6579 control.checkpoint()?;
6580 }
6581 if let Op::TxnCommit { epoch, added_runs } = &record.op {
6582 commits.insert(record.txn_id, (*epoch, added_runs.clone()));
6583 }
6584 if let Op::SpilledRows { table_id, rows } = &record.op {
6585 spilled_payloads
6586 .entry((record.txn_id, *table_id))
6587 .or_default()
6588 .push(rows);
6589 }
6590 }
6591 let earliest_epoch = commits.values().map(|(epoch, _)| *epoch).min();
6592 let current_epoch = self.epoch.committed().0;
6593 let retention_floor = crate::replication::replication_wal_floor(&self.root)?;
6594 let gap = resume.is_some_and(|(epoch, _)| {
6595 retention_floor != 0 && epoch <= retention_floor && epoch <= current_epoch
6596 });
6597 if gap {
6598 return Ok(CdcBatch {
6599 events: Vec::new(),
6600 current_epoch,
6601 earliest_epoch,
6602 gap: true,
6603 });
6604 }
6605
6606 let table_names: HashMap<u64, String> = self
6607 .catalog
6608 .read()
6609 .tables
6610 .iter()
6611 .map(|entry| (entry.table_id, entry.name.clone()))
6612 .collect();
6613 let mut before_images: HashMap<(u64, u64, u64), crate::memtable::Row> = HashMap::new();
6614 let mut retained_bytes = 0_usize;
6615 for (index, record) in records.iter().enumerate() {
6616 if index % 256 == 0 {
6617 control.checkpoint()?;
6618 }
6619 if !commits.contains_key(&record.txn_id) {
6620 continue;
6621 }
6622 let Op::BeforeImage {
6623 table_id,
6624 row_id,
6625 row,
6626 } = &record.op
6627 else {
6628 continue;
6629 };
6630 if row.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
6631 return Err(MongrelError::ResourceLimitExceeded {
6632 resource: "CDC before-image bytes",
6633 requested: row.len(),
6634 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
6635 });
6636 }
6637 let before: crate::memtable::Row = bincode::deserialize(row)?;
6638 if before_images.len() >= CDC_MAX_ROWS {
6639 return Err(MongrelError::ResourceLimitExceeded {
6640 resource: "CDC before-image rows",
6641 requested: before_images.len().saturating_add(1),
6642 limit: CDC_MAX_ROWS,
6643 });
6644 }
6645 charge_cdc_bytes(
6646 &mut retained_bytes,
6647 cdc_row_storage_bytes(&before),
6648 "CDC retained bytes",
6649 )?;
6650 before_images.insert((record.txn_id, *table_id, row_id.0), before);
6651 }
6652 let mut operation_indices: HashMap<u64, u32> = HashMap::new();
6653 let mut events = Vec::new();
6654 let mut decoded_rows = before_images.len();
6655 for (record_index, record) in records.iter().enumerate() {
6656 if record_index % 256 == 0 {
6657 control.checkpoint()?;
6658 }
6659 let Some((commit_epoch, _)) = commits.get(&record.txn_id) else {
6660 continue;
6661 };
6662 let event = match &record.op {
6663 Op::Put { table_id, rows } => {
6664 if rows.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
6665 return Err(MongrelError::ResourceLimitExceeded {
6666 resource: "CDC inline row bytes",
6667 requested: rows.len(),
6668 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
6669 });
6670 }
6671 let rows: Vec<crate::memtable::Row> = bincode::deserialize(rows)?;
6672 decoded_rows = decoded_rows.saturating_add(rows.len());
6673 if decoded_rows > CDC_MAX_ROWS {
6674 return Err(MongrelError::ResourceLimitExceeded {
6675 resource: "CDC decoded rows",
6676 requested: decoded_rows,
6677 limit: CDC_MAX_ROWS,
6678 });
6679 }
6680 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(512);
6681 let mut peak_bytes = retained_bytes;
6682 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
6683 let data = serde_json::to_value(rows)
6684 .map_err(|error| MongrelError::Other(format!("CDC JSON: {error}")))?;
6685 Some((*table_id, "put", data, event_bytes))
6686 }
6687 Op::Delete { table_id, row_ids } => {
6688 let before = row_ids
6689 .iter()
6690 .filter_map(|row_id| {
6691 before_images
6692 .get(&(record.txn_id, *table_id, row_id.0))
6693 .cloned()
6694 })
6695 .collect::<Vec<_>>();
6696 let event_bytes = cdc_rows_json_bytes(&before)
6697 .saturating_add(
6698 row_ids
6699 .len()
6700 .saturating_mul(std::mem::size_of::<serde_json::Value>()),
6701 )
6702 .saturating_add(512);
6703 let mut peak_bytes = retained_bytes;
6704 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
6705 Some((
6706 *table_id,
6707 "delete",
6708 serde_json::json!({
6709 "row_ids": row_ids.iter().map(|row_id| row_id.0).collect::<Vec<_>>(),
6710 "before": before,
6711 }),
6712 event_bytes,
6713 ))
6714 }
6715 Op::TruncateTable { table_id } => {
6716 Some((*table_id, "truncate", serde_json::Value::Null, 512))
6717 }
6718 _ => None,
6719 };
6720 if let Some((table_id, op, data, event_bytes)) = event {
6721 let index = operation_indices.entry(record.txn_id).or_insert(0);
6722 let event_position = (*commit_epoch, *index);
6723 *index = index.saturating_add(1);
6724 if resume.is_some_and(|position| event_position <= position) {
6725 continue;
6726 }
6727 if events.len() >= CDC_MAX_EVENTS {
6728 return Err(MongrelError::ResourceLimitExceeded {
6729 resource: "CDC events",
6730 requested: events.len().saturating_add(1),
6731 limit: CDC_MAX_EVENTS,
6732 });
6733 }
6734 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
6735 events.push(ChangeEvent {
6736 id: Some(format!("{}:{}", event_position.0, event_position.1)),
6737 channel: "changes".into(),
6738 table_id: Some(table_id),
6739 table: table_names.get(&table_id).cloned().unwrap_or_default(),
6740 op: op.into(),
6741 epoch: *commit_epoch,
6742 txn_id: Some(record.txn_id),
6743 message: None,
6744 data: Some(data),
6745 });
6746 }
6747 if let Op::TxnCommit { added_runs, .. } = &record.op {
6748 for run in added_runs {
6749 control.checkpoint()?;
6750 let index = operation_indices.entry(record.txn_id).or_insert(0);
6751 let event_position = (*commit_epoch, *index);
6752 *index = index.saturating_add(1);
6753 if resume.is_some_and(|position| event_position <= position) {
6754 continue;
6755 }
6756 let mut rows = if let Some(payloads) =
6757 spilled_payloads.get(&(record.txn_id, run.table_id))
6758 {
6759 let mut rows = Vec::new();
6760 for payload in payloads {
6761 control.checkpoint()?;
6762 if payload.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
6763 return Err(MongrelError::ResourceLimitExceeded {
6764 resource: "CDC spilled row bytes",
6765 requested: payload.len(),
6766 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
6767 });
6768 }
6769 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(payload)?;
6770 if decoded_rows
6771 .saturating_add(rows.len())
6772 .saturating_add(chunk.len())
6773 > CDC_MAX_ROWS
6774 {
6775 return Err(MongrelError::ResourceLimitExceeded {
6776 resource: "CDC decoded rows",
6777 requested: decoded_rows
6778 .saturating_add(rows.len())
6779 .saturating_add(chunk.len()),
6780 limit: CDC_MAX_ROWS,
6781 });
6782 }
6783 rows.extend(chunk);
6784 }
6785 rows
6786 } else {
6787 let Some(handle) = self.tables.read().get(&run.table_id).cloned() else {
6788 return Ok(CdcBatch {
6789 events: Vec::new(),
6790 current_epoch,
6791 earliest_epoch,
6792 gap: true,
6793 });
6794 };
6795 let table = handle.lock();
6796 let mut reader = match table.open_reader(run.run_id) {
6797 Ok(reader) => reader,
6798 Err(_) => {
6799 return Ok(CdcBatch {
6800 events: Vec::new(),
6801 current_epoch,
6802 earliest_epoch,
6803 gap: true,
6804 })
6805 }
6806 };
6807 let remaining = CDC_MAX_ROWS.saturating_sub(decoded_rows);
6808 let rows = reader.all_rows_controlled(control, remaining)?;
6809 drop(reader);
6810 drop(table);
6811 rows
6812 };
6813 for row in &mut rows {
6814 row.committed_epoch = Epoch(*commit_epoch);
6815 }
6816 decoded_rows = decoded_rows.saturating_add(rows.len());
6817 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(768);
6818 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
6819 if events.len() >= CDC_MAX_EVENTS {
6820 return Err(MongrelError::ResourceLimitExceeded {
6821 resource: "CDC events",
6822 requested: events.len().saturating_add(1),
6823 limit: CDC_MAX_EVENTS,
6824 });
6825 }
6826 events.push(ChangeEvent {
6827 id: Some(format!("{}:{}", event_position.0, event_position.1)),
6828 channel: "changes".into(),
6829 table_id: Some(run.table_id),
6830 table: table_names.get(&run.table_id).cloned().unwrap_or_default(),
6831 op: "put_run".into(),
6832 epoch: *commit_epoch,
6833 txn_id: Some(record.txn_id),
6834 message: None,
6835 data: Some(serde_json::json!({
6836 "run_id": run.run_id.to_string(),
6837 "row_count": run.row_count,
6838 "min_row_id": run.min_row_id,
6839 "max_row_id": run.max_row_id,
6840 "rows": rows,
6841 })),
6842 });
6843 }
6844 }
6845 }
6846 control.checkpoint()?;
6847 Ok(CdcBatch {
6848 events,
6849 current_epoch,
6850 earliest_epoch,
6851 gap: false,
6852 })
6853 }
6854
6855 pub fn notify(&self, channel: &str, message: Option<String>) {
6858 let _ = self.notify.send(ChangeEvent {
6859 id: None,
6860 channel: channel.to_string(),
6861 table_id: None,
6862 table: String::new(),
6863 op: "notify".into(),
6864 epoch: self.epoch.visible().0,
6865 txn_id: None,
6866 message,
6867 data: None,
6868 });
6869 }
6870
6871 pub fn call_procedure(
6872 &self,
6873 name: &str,
6874 args: HashMap<String, crate::Value>,
6875 ) -> Result<ProcedureCallResult> {
6876 self.call_procedure_as(name, args, None)
6877 }
6878
6879 pub fn call_procedure_as(
6880 &self,
6881 name: &str,
6882 args: HashMap<String, crate::Value>,
6883 principal: Option<&crate::auth::Principal>,
6884 ) -> Result<ProcedureCallResult> {
6885 let control = crate::ExecutionControl::new(None);
6886 self.call_procedure_as_controlled(name, args, principal, &control, || true)
6887 }
6888
6889 #[doc(hidden)]
6892 pub fn call_procedure_as_bound(
6893 &self,
6894 expected: &StoredProcedure,
6895 args: HashMap<String, crate::Value>,
6896 principal: Option<&crate::auth::Principal>,
6897 ) -> Result<ProcedureCallResult> {
6898 self.require_for(principal, &crate::auth::Permission::All)?;
6899 let procedure = self.procedure(&expected.name).ok_or_else(|| {
6900 MongrelError::NotFound(format!("procedure {:?} not found", expected.name))
6901 })?;
6902 if &procedure != expected {
6903 return Err(MongrelError::Conflict(format!(
6904 "procedure {:?} changed after request authorization",
6905 expected.name
6906 )));
6907 }
6908 let control = crate::ExecutionControl::new(None);
6909 self.execute_procedure_as_controlled(procedure, args, principal, &control, || true)
6910 }
6911
6912 #[doc(hidden)]
6917 pub fn call_procedure_as_controlled<F>(
6918 &self,
6919 name: &str,
6920 args: HashMap<String, crate::Value>,
6921 principal: Option<&crate::auth::Principal>,
6922 control: &crate::ExecutionControl,
6923 before_commit: F,
6924 ) -> Result<ProcedureCallResult>
6925 where
6926 F: FnOnce() -> bool,
6927 {
6928 self.require_for(principal, &crate::auth::Permission::All)?;
6932 let procedure = self
6933 .procedure(name)
6934 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
6935 self.execute_procedure_as_controlled(procedure, args, principal, control, before_commit)
6936 }
6937
6938 fn execute_procedure_as_controlled<F>(
6939 &self,
6940 procedure: StoredProcedure,
6941 args: HashMap<String, crate::Value>,
6942 principal: Option<&crate::auth::Principal>,
6943 control: &crate::ExecutionControl,
6944 before_commit: F,
6945 ) -> Result<ProcedureCallResult>
6946 where
6947 F: FnOnce() -> bool,
6948 {
6949 let args = bind_procedure_args(&procedure, args)?;
6950 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
6951 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
6952 if has_writes {
6953 let mut tx = self.begin_as(principal.cloned());
6954 let run = (|| {
6955 for (step_index, step) in procedure.body.steps.iter().enumerate() {
6956 if step_index % 256 == 0 {
6957 control.checkpoint()?;
6958 }
6959 let output = self.execute_procedure_step(
6960 step,
6961 &args,
6962 &outputs,
6963 Some(&mut tx),
6964 principal,
6965 Some(control),
6966 )?;
6967 outputs.insert(step.id().to_string(), output);
6968 }
6969 control.checkpoint()?;
6970 eval_return_output(&procedure.body.return_value, &args, &outputs)
6971 })();
6972 match run {
6973 Ok(output) => {
6974 control.checkpoint()?;
6975 if !before_commit() {
6976 tx.rollback();
6977 return Err(MongrelError::Cancelled);
6978 }
6979 let epoch = tx.commit()?.0;
6980 Ok(ProcedureCallResult {
6981 epoch: Some(epoch),
6982 output,
6983 })
6984 }
6985 Err(e) => {
6986 tx.rollback();
6987 Err(e)
6988 }
6989 }
6990 } else {
6991 for (step_index, step) in procedure.body.steps.iter().enumerate() {
6992 if step_index % 256 == 0 {
6993 control.checkpoint()?;
6994 }
6995 let output = self.execute_procedure_step(
6996 step,
6997 &args,
6998 &outputs,
6999 None,
7000 principal,
7001 Some(control),
7002 )?;
7003 outputs.insert(step.id().to_string(), output);
7004 }
7005 control.checkpoint()?;
7006 Ok(ProcedureCallResult {
7007 epoch: None,
7008 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
7009 })
7010 }
7011 }
7012
7013 fn execute_procedure_step(
7014 &self,
7015 step: &ProcedureStep,
7016 args: &HashMap<String, crate::Value>,
7017 outputs: &HashMap<String, ProcedureCallOutput>,
7018 tx: Option<&mut crate::txn::Transaction<'_>>,
7019 principal: Option<&crate::auth::Principal>,
7020 control: Option<&crate::ExecutionControl>,
7021 ) -> Result<ProcedureCallOutput> {
7022 if let Some(control) = control {
7023 control.checkpoint()?;
7024 }
7025 match step {
7026 ProcedureStep::NativeQuery {
7027 table,
7028 conditions,
7029 projection,
7030 limit,
7031 ..
7032 } => {
7033 let mut q = crate::Query::new();
7034 for condition in conditions {
7035 q = q.and(eval_condition(condition, args, outputs)?);
7036 }
7037 let handle = self.table(table)?;
7038 let rows = handle.lock().query(&q)?;
7039 let mut rows = self.secure_rows_for(table, rows, principal)?;
7040 if let Some(limit) = limit {
7041 rows.truncate(*limit);
7042 }
7043 let projection = projection.as_ref();
7044 let mut output = Vec::with_capacity(rows.len());
7045 for (row_index, row) in rows.into_iter().enumerate() {
7046 if row_index % 256 == 0 {
7047 if let Some(control) = control {
7048 control.checkpoint()?;
7049 }
7050 }
7051 output.push(ProcedureCallRow {
7052 row_id: Some(row.row_id),
7053 columns: match projection {
7054 Some(ids) => row
7055 .columns
7056 .into_iter()
7057 .filter(|(id, _)| ids.contains(id))
7058 .collect(),
7059 None => row.columns,
7060 },
7061 });
7062 }
7063 Ok(ProcedureCallOutput::Rows(output))
7064 }
7065 ProcedureStep::Put {
7066 table,
7067 cells,
7068 returning,
7069 ..
7070 } => {
7071 let tx = tx.ok_or_else(|| {
7072 MongrelError::InvalidArgument(
7073 "write procedure step requires a transaction".into(),
7074 )
7075 })?;
7076 let cells = eval_cells(cells, args, outputs)?;
7077 if *returning {
7078 let out = tx.put_returning(table, cells)?;
7079 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
7080 row_id: None,
7081 columns: out.row.columns.into_iter().collect(),
7082 }))
7083 } else {
7084 tx.put(table, cells)?;
7085 Ok(ProcedureCallOutput::Null)
7086 }
7087 }
7088 ProcedureStep::Upsert {
7089 table,
7090 cells,
7091 update_cells,
7092 returning,
7093 ..
7094 } => {
7095 let tx = tx.ok_or_else(|| {
7096 MongrelError::InvalidArgument(
7097 "write procedure step requires a transaction".into(),
7098 )
7099 })?;
7100 let cells = eval_cells(cells, args, outputs)?;
7101 let action = match update_cells {
7102 Some(update_cells) => {
7103 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
7104 }
7105 None => crate::UpsertAction::DoNothing,
7106 };
7107 let out = tx.upsert(table, cells, action)?;
7108 if *returning {
7109 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
7110 row_id: None,
7111 columns: out.row.columns.into_iter().collect(),
7112 }))
7113 } else {
7114 Ok(ProcedureCallOutput::Null)
7115 }
7116 }
7117 ProcedureStep::DeleteByPk { table, pk, .. } => {
7118 let tx = tx.ok_or_else(|| {
7119 MongrelError::InvalidArgument(
7120 "write procedure step requires a transaction".into(),
7121 )
7122 })?;
7123 let pk = eval_value(pk, args, outputs)?;
7124 let handle = self.table(table)?;
7125 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
7126 MongrelError::NotFound("procedure delete_by_pk target not found".into())
7127 })?;
7128 tx.delete(table, row_id)?;
7129 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
7130 }
7131 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
7132 "DeleteRows procedure step is not supported by the core executor yet".into(),
7133 )),
7134 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
7135 "SqlQuery procedure step must be executed by mongreldb-query".into(),
7136 )),
7137 }
7138 }
7139
7140 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
7141 let cat = self.catalog.read();
7142 for step in &procedure.body.steps {
7143 let Some(table_name) = step.table() else {
7144 continue;
7145 };
7146 let schema = &cat
7147 .live(table_name)
7148 .ok_or_else(|| {
7149 MongrelError::InvalidArgument(format!(
7150 "procedure {:?} references unknown table {table_name:?}",
7151 procedure.name
7152 ))
7153 })?
7154 .schema;
7155 match step {
7156 ProcedureStep::NativeQuery {
7157 conditions,
7158 projection,
7159 ..
7160 } => {
7161 for condition in conditions {
7162 validate_condition_columns(condition, schema)?;
7163 }
7164 if let Some(projection) = projection {
7165 for id in projection {
7166 validate_column_id(*id, schema)?;
7167 }
7168 }
7169 }
7170 ProcedureStep::Put { cells, .. } => {
7171 for cell in cells {
7172 validate_column_id(cell.column_id, schema)?;
7173 }
7174 }
7175 ProcedureStep::Upsert {
7176 cells,
7177 update_cells,
7178 ..
7179 } => {
7180 for cell in cells {
7181 validate_column_id(cell.column_id, schema)?;
7182 }
7183 if let Some(update_cells) = update_cells {
7184 for cell in update_cells {
7185 validate_column_id(cell.column_id, schema)?;
7186 }
7187 }
7188 }
7189 ProcedureStep::DeleteByPk { .. } => {
7190 if schema.primary_key().is_none() {
7191 return Err(MongrelError::InvalidArgument(format!(
7192 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
7193 procedure.name
7194 )));
7195 }
7196 }
7197 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
7198 }
7199 }
7200 Ok(())
7201 }
7202
7203 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
7204 let cat = self.catalog.read();
7205 let target_schema = match &trigger.target {
7206 TriggerTarget::Table(target_name) => cat
7207 .live(target_name)
7208 .ok_or_else(|| {
7209 MongrelError::InvalidArgument(format!(
7210 "trigger {:?} references unknown target table {target_name:?}",
7211 trigger.name
7212 ))
7213 })?
7214 .schema
7215 .clone(),
7216 TriggerTarget::View(_) => Schema {
7217 columns: trigger.target_columns.clone(),
7218 ..Schema::default()
7219 },
7220 };
7221 for col in &trigger.update_of {
7222 if target_schema.column(col).is_none() {
7223 return Err(MongrelError::InvalidArgument(format!(
7224 "trigger {:?} UPDATE OF references unknown column {col:?}",
7225 trigger.name
7226 )));
7227 }
7228 }
7229 if let Some(expr) = &trigger.when {
7230 validate_trigger_expr(expr, &target_schema, trigger.event)?;
7231 }
7232 let mut select_schemas: HashMap<String, &Schema> = HashMap::new();
7233 for step in &trigger.program.steps {
7234 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
7235 {
7236 return Err(MongrelError::InvalidArgument(
7237 "SetNew trigger steps are only valid in BEFORE triggers".into(),
7238 ));
7239 }
7240 validate_trigger_step(
7241 step,
7242 &cat,
7243 &target_schema,
7244 trigger.event,
7245 &mut select_schemas,
7246 )?;
7247 }
7248 Ok(())
7249 }
7250
7251 pub fn begin(&self) -> crate::txn::Transaction<'_> {
7253 self.begin_with_isolation(crate::txn::IsolationLevel::default())
7254 }
7255
7256 fn transaction_principal_snapshot(&self) -> (Option<crate::auth::Principal>, bool) {
7257 let principal = self.principal.read().clone();
7258 let catalog_bound = principal.as_ref().is_some_and(|principal| {
7259 let catalog = self.catalog.read();
7260 catalog.require_auth || principal.user_id != 0
7261 });
7262 (principal, catalog_bound)
7263 }
7264
7265 pub fn begin_as(
7266 &self,
7267 principal: Option<crate::auth::Principal>,
7268 ) -> crate::txn::Transaction<'_> {
7269 let catalog_bound = principal.as_ref().is_some_and(|principal| {
7270 let catalog = self.catalog.read();
7271 catalog.require_auth || principal.user_id != 0
7272 });
7273 let txn_id = self.alloc_txn_id();
7274 let read = Snapshot::at(self.epoch.visible());
7275 crate::txn::Transaction::new(self, txn_id, read).with_principal(principal, catalog_bound)
7276 }
7277
7278 pub fn begin_with_isolation(
7280 &self,
7281 level: crate::txn::IsolationLevel,
7282 ) -> crate::txn::Transaction<'_> {
7283 let txn_id = self.alloc_txn_id();
7284 let epoch = match level {
7285 crate::txn::IsolationLevel::ReadCommitted => self.epoch.visible(),
7286 _ => self.epoch.visible(),
7287 };
7288 let read = Snapshot::at(epoch);
7289 let (principal, catalog_bound) = self.transaction_principal_snapshot();
7290 crate::txn::Transaction::new(self, txn_id, read).with_principal(principal, catalog_bound)
7291 }
7292
7293 pub fn begin_with_external_trigger_bridge<'a>(
7296 &'a self,
7297 bridge: &'a dyn ExternalTriggerBridge,
7298 ) -> crate::txn::Transaction<'a> {
7299 let txn_id = self.alloc_txn_id();
7300 let read = Snapshot::at(self.epoch.visible());
7301 let (principal, catalog_bound) = self.transaction_principal_snapshot();
7302 crate::txn::Transaction::new(self, txn_id, read)
7303 .with_external_trigger_bridge(bridge)
7304 .with_principal(principal, catalog_bound)
7305 }
7306
7307 pub fn begin_with_external_trigger_bridge_as<'a>(
7308 &'a self,
7309 bridge: &'a dyn ExternalTriggerBridge,
7310 principal: Option<crate::auth::Principal>,
7311 ) -> crate::txn::Transaction<'a> {
7312 let catalog_bound = principal.as_ref().is_some_and(|principal| {
7313 let catalog = self.catalog.read();
7314 catalog.require_auth || principal.user_id != 0
7315 });
7316 let txn_id = self.alloc_txn_id();
7317 let read = Snapshot::at(self.epoch.visible());
7318 crate::txn::Transaction::new(self, txn_id, read)
7319 .with_external_trigger_bridge(bridge)
7320 .with_principal(principal, catalog_bound)
7321 }
7322
7323 pub fn transaction<T>(
7325 &self,
7326 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7327 ) -> Result<T> {
7328 let mut tx = self.begin();
7329 match f(&mut tx) {
7330 Ok(out) => {
7331 tx.commit()?;
7332 Ok(out)
7333 }
7334 Err(e) => {
7335 tx.rollback();
7336 Err(e)
7337 }
7338 }
7339 }
7340
7341 pub fn transaction_with_row_ids<T>(
7342 &self,
7343 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7344 ) -> Result<(T, Vec<RowId>)> {
7345 let mut tx = self.begin();
7346 match f(&mut tx) {
7347 Ok(output) => {
7348 let (_, row_ids) = tx.commit_with_row_ids()?;
7349 Ok((output, row_ids))
7350 }
7351 Err(error) => {
7352 tx.rollback();
7353 Err(error)
7354 }
7355 }
7356 }
7357
7358 pub fn transaction_for_current_principal<T>(
7359 &self,
7360 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7361 ) -> Result<T> {
7362 if self.principal.read().is_some() {
7363 self.refresh_principal()?;
7364 }
7365 let mut transaction = self.begin_as(self.principal.read().clone());
7366 match f(&mut transaction) {
7367 Ok(output) => {
7368 transaction.commit()?;
7369 Ok(output)
7370 }
7371 Err(error) => {
7372 transaction.rollback();
7373 Err(error)
7374 }
7375 }
7376 }
7377
7378 pub fn transaction_for_current_principal_with_epoch<T>(
7379 &self,
7380 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7381 ) -> Result<(Epoch, T)> {
7382 if self.principal.read().is_some() {
7383 self.refresh_principal()?;
7384 }
7385 let mut transaction = self.begin_as(self.principal.read().clone());
7386 match f(&mut transaction) {
7387 Ok(output) => {
7388 let epoch = transaction.commit()?;
7389 Ok((epoch, output))
7390 }
7391 Err(error) => {
7392 transaction.rollback();
7393 Err(error)
7394 }
7395 }
7396 }
7397
7398 pub fn transaction_with_row_ids_for_current_principal<T>(
7399 &self,
7400 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7401 ) -> Result<(T, Vec<RowId>)> {
7402 if self.principal.read().is_some() {
7403 self.refresh_principal()?;
7404 }
7405 let mut transaction = self.begin_as(self.principal.read().clone());
7406 match f(&mut transaction) {
7407 Ok(output) => {
7408 let (_, row_ids) = transaction.commit_with_row_ids()?;
7409 Ok((output, row_ids))
7410 }
7411 Err(error) => {
7412 transaction.rollback();
7413 Err(error)
7414 }
7415 }
7416 }
7417
7418 pub fn transaction_with_external_trigger_bridge<'a, T>(
7421 &'a self,
7422 bridge: &'a dyn ExternalTriggerBridge,
7423 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7424 ) -> Result<T> {
7425 let mut tx = self.begin_with_external_trigger_bridge(bridge);
7426 match f(&mut tx) {
7427 Ok(out) => {
7428 tx.commit()?;
7429 Ok(out)
7430 }
7431 Err(e) => {
7432 tx.rollback();
7433 Err(e)
7434 }
7435 }
7436 }
7437
7438 pub fn transaction_with_external_trigger_bridge_as<'a, T>(
7439 &'a self,
7440 bridge: &'a dyn ExternalTriggerBridge,
7441 principal: Option<crate::auth::Principal>,
7442 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7443 ) -> Result<T> {
7444 let mut tx = self.begin_with_external_trigger_bridge_as(bridge, principal);
7445 match f(&mut tx) {
7446 Ok(output) => {
7447 tx.commit()?;
7448 Ok(output)
7449 }
7450 Err(error) => {
7451 tx.rollback();
7452 Err(error)
7453 }
7454 }
7455 }
7456
7457 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
7460 self.active_txns.register(epoch)
7461 }
7462
7463 fn fill_auto_increment_for_staging(
7464 &self,
7465 staging: &mut [(u64, crate::txn::Staged)],
7466 control: Option<&crate::ExecutionControl>,
7467 ) -> Result<()> {
7468 let mut puts_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
7469 for (index, (table_id, staged)) in staging.iter().enumerate() {
7470 commit_prepare_checkpoint(control, index)?;
7471 if matches!(staged, crate::txn::Staged::Put(_)) {
7472 puts_by_table.entry(*table_id).or_default().push(index);
7473 }
7474 }
7475
7476 let tables = self.tables.read();
7477 for (table_index, (table_id, indexes)) in puts_by_table.into_iter().enumerate() {
7478 commit_prepare_checkpoint(control, table_index)?;
7479 if let Some(handle) = tables.get(&table_id) {
7480 #[cfg(test)]
7481 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
7482 let mut t = handle.lock();
7483 for (fill_index, index) in indexes.into_iter().enumerate() {
7484 commit_prepare_checkpoint(control, fill_index)?;
7485 if let crate::txn::Staged::Put(cells) = &mut staging[index].1 {
7486 t.fill_auto_inc(cells)?;
7487 }
7488 }
7489 }
7490 }
7491 Ok(())
7492 }
7493
7494 fn expand_table_triggers(
7495 &self,
7496 staging: &mut Vec<(u64, crate::txn::Staged)>,
7497 read_epoch: Epoch,
7498 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
7499 external_states: &mut Vec<(String, Vec<u8>)>,
7500 control: Option<&crate::ExecutionControl>,
7501 ) -> Result<()> {
7502 commit_prepare_checkpoint(control, 0)?;
7503 let mut external_writes = Vec::new();
7504 let config = self.trigger_config();
7505 if config.recursive_triggers {
7506 let chunk = std::mem::take(staging);
7507 let stacks = vec![Vec::new(); chunk.len()];
7508 *staging = self.expand_trigger_chunk(
7509 chunk,
7510 stacks,
7511 read_epoch,
7512 0,
7513 config.max_depth,
7514 &mut external_writes,
7515 &config,
7516 control,
7517 )?;
7518 self.apply_external_trigger_writes(
7519 external_writes,
7520 external_trigger_bridge,
7521 external_states,
7522 staging,
7523 control,
7524 )?;
7525 return Ok(());
7526 }
7527
7528 let mut expansion =
7529 self.expand_table_triggers_once(staging, read_epoch, None, &config, control)?;
7530 if !expansion.before.is_empty() {
7531 let mut final_staging = expansion.before;
7532 final_staging.extend(filter_ignored_staging(
7533 std::mem::take(staging),
7534 &expansion.ignored_indices,
7535 ));
7536 *staging = final_staging;
7537 } else if !expansion.ignored_indices.is_empty() {
7538 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
7539 }
7540 staging.append(&mut expansion.after);
7541 external_writes.append(&mut expansion.before_external);
7542 external_writes.append(&mut expansion.after_external);
7543 self.apply_external_trigger_writes(
7544 external_writes,
7545 external_trigger_bridge,
7546 external_states,
7547 staging,
7548 control,
7549 )?;
7550 Ok(())
7551 }
7552
7553 #[allow(clippy::too_many_arguments)]
7554 fn expand_trigger_chunk(
7555 &self,
7556 mut chunk: Vec<(u64, crate::txn::Staged)>,
7557 stacks: Vec<Vec<String>>,
7558 read_epoch: Epoch,
7559 depth: u32,
7560 max_depth: u32,
7561 external_writes: &mut Vec<ExternalTriggerWrite>,
7562 config: &TriggerConfig,
7563 control: Option<&crate::ExecutionControl>,
7564 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
7565 if chunk.is_empty() {
7566 return Ok(Vec::new());
7567 }
7568 commit_prepare_checkpoint(control, 0)?;
7569 self.fill_auto_increment_for_staging(&mut chunk, control)?;
7570 let expansion = self.expand_table_triggers_once(
7571 &mut chunk,
7572 read_epoch,
7573 Some(&stacks),
7574 config,
7575 control,
7576 )?;
7577 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
7578 let stack = expansion
7579 .before_stacks
7580 .first()
7581 .or_else(|| expansion.after_stacks.first())
7582 .cloned()
7583 .unwrap_or_default();
7584 return Err(MongrelError::TriggerValidation(format!(
7585 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
7586 Self::format_trigger_stack(&stack)
7587 )));
7588 }
7589
7590 let mut out = Vec::new();
7591 external_writes.extend(expansion.before_external);
7592 out.extend(self.expand_trigger_chunk(
7593 expansion.before,
7594 expansion.before_stacks,
7595 read_epoch,
7596 depth + 1,
7597 max_depth,
7598 external_writes,
7599 config,
7600 control,
7601 )?);
7602 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
7603 external_writes.extend(expansion.after_external);
7604 out.extend(self.expand_trigger_chunk(
7605 expansion.after,
7606 expansion.after_stacks,
7607 read_epoch,
7608 depth + 1,
7609 max_depth,
7610 external_writes,
7611 config,
7612 control,
7613 )?);
7614 Ok(out)
7615 }
7616
7617 fn apply_external_trigger_writes(
7618 &self,
7619 writes: Vec<ExternalTriggerWrite>,
7620 bridge: Option<&dyn ExternalTriggerBridge>,
7621 external_states: &mut Vec<(String, Vec<u8>)>,
7622 staging: &mut Vec<(u64, crate::txn::Staged)>,
7623 control: Option<&crate::ExecutionControl>,
7624 ) -> Result<()> {
7625 if writes.is_empty() {
7626 return Ok(());
7627 }
7628 let bridge = bridge.ok_or_else(|| {
7629 MongrelError::TriggerValidation(
7630 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
7631 )
7632 })?;
7633 for (write_index, write) in writes.into_iter().enumerate() {
7634 commit_prepare_checkpoint(control, write_index)?;
7635 let table = write.table().to_string();
7636 let entry = self.external_table(&table).ok_or_else(|| {
7637 MongrelError::NotFound(format!("external table {table:?} not found"))
7638 })?;
7639 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
7640 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
7641 external_states.push((table, result.state));
7642 for (base_index, base_write) in result.base_writes.into_iter().enumerate() {
7643 commit_prepare_checkpoint(control, base_index)?;
7644 match base_write {
7645 ExternalTriggerBaseWrite::Put { table, cells } => {
7646 let table_id = self.table_id(&table)?;
7647 staging.push((table_id, crate::txn::Staged::Put(cells)));
7648 }
7649 ExternalTriggerBaseWrite::Delete { table, row_id } => {
7650 let table_id = self.table_id(&table)?;
7651 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
7652 }
7653 }
7654 }
7655 }
7656 dedup_external_states_in_place(external_states);
7657 Ok(())
7658 }
7659
7660 fn expand_table_triggers_once(
7661 &self,
7662 staging: &mut Vec<(u64, crate::txn::Staged)>,
7663 read_epoch: Epoch,
7664 trigger_stacks: Option<&[Vec<String>]>,
7665 config: &TriggerConfig,
7666 control: Option<&crate::ExecutionControl>,
7667 ) -> Result<TriggerExpansion> {
7668 commit_prepare_checkpoint(control, 0)?;
7669 let triggers: Vec<StoredTrigger> = self
7670 .catalog
7671 .read()
7672 .triggers
7673 .iter()
7674 .filter(|entry| {
7675 entry.trigger.enabled
7676 && matches!(
7677 entry.trigger.timing,
7678 TriggerTiming::Before | TriggerTiming::After
7679 )
7680 && matches!(entry.trigger.target, TriggerTarget::Table(_))
7681 })
7682 .map(|entry| entry.trigger.clone())
7683 .collect();
7684 if triggers.is_empty() || staging.is_empty() {
7685 return Ok(TriggerExpansion::default());
7686 }
7687
7688 let before_triggers = triggers
7689 .iter()
7690 .filter(|trigger| trigger.timing == TriggerTiming::Before)
7691 .cloned()
7692 .collect::<Vec<_>>();
7693 let after_triggers = triggers
7694 .iter()
7695 .filter(|trigger| trigger.timing == TriggerTiming::After)
7696 .cloned()
7697 .collect::<Vec<_>>();
7698
7699 let mut before_added = Vec::new();
7700 let mut before_stacks = Vec::new();
7701 let mut before_external = Vec::new();
7702 let mut ignored_indices = std::collections::BTreeSet::new();
7703 if !before_triggers.is_empty() {
7704 let before_events =
7705 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?;
7706 let mut out = TriggerProgramOutput {
7707 added: &mut before_added,
7708 added_stacks: &mut before_stacks,
7709 added_external: &mut before_external,
7710 ignored_indices: &mut ignored_indices,
7711 };
7712 self.execute_triggers_for_events(
7713 &before_triggers,
7714 &before_events,
7715 Some(staging),
7716 &mut out,
7717 config,
7718 read_epoch,
7719 control,
7720 )?;
7721 }
7722
7723 let after_events = if after_triggers.is_empty() {
7724 Vec::new()
7725 } else {
7726 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?
7727 .into_iter()
7728 .filter(|event| {
7729 !event
7730 .op_indices
7731 .iter()
7732 .any(|idx| ignored_indices.contains(idx))
7733 })
7734 .collect()
7735 };
7736
7737 let mut after_added = Vec::new();
7738 let mut after_stacks = Vec::new();
7739 let mut after_external = Vec::new();
7740 let mut out = TriggerProgramOutput {
7741 added: &mut after_added,
7742 added_stacks: &mut after_stacks,
7743 added_external: &mut after_external,
7744 ignored_indices: &mut ignored_indices,
7745 };
7746 self.execute_triggers_for_events(
7747 &after_triggers,
7748 &after_events,
7749 None,
7750 &mut out,
7751 config,
7752 read_epoch,
7753 control,
7754 )?;
7755 Ok(TriggerExpansion {
7756 before: before_added,
7757 before_stacks,
7758 before_external,
7759 after: after_added,
7760 after_stacks,
7761 after_external,
7762 ignored_indices,
7763 })
7764 }
7765
7766 #[allow(clippy::too_many_arguments)]
7767 fn execute_triggers_for_events(
7768 &self,
7769 triggers: &[StoredTrigger],
7770 events: &[WriteEvent],
7771 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
7772 out: &mut TriggerProgramOutput<'_>,
7773 config: &TriggerConfig,
7774 read_epoch: Epoch,
7775 control: Option<&crate::ExecutionControl>,
7776 ) -> Result<()> {
7777 let mut checkpoint_index = 0_usize;
7778 for event in events {
7779 for trigger in triggers {
7780 commit_prepare_checkpoint(control, checkpoint_index)?;
7781 checkpoint_index += 1;
7782 if event
7783 .op_indices
7784 .iter()
7785 .any(|idx| out.ignored_indices.contains(idx))
7786 {
7787 break;
7788 }
7789 let matches = {
7790 let cat = self.catalog.read();
7791 trigger_matches_event(trigger, event, &cat)?
7792 };
7793 if !matches {
7794 continue;
7795 }
7796 if let Some(when) = &trigger.when {
7797 if !eval_trigger_expr(when, event)? {
7798 continue;
7799 }
7800 }
7801 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
7802 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
7803 return Err(MongrelError::TriggerValidation(format!(
7804 "trigger recursion cycle detected; trigger stack: {}",
7805 Self::format_trigger_stack(&trigger_stack)
7806 )));
7807 }
7808 let outcome = match staging.as_mut() {
7809 Some(staging) => self.execute_trigger_program(
7810 trigger,
7811 event,
7812 Some(&mut **staging),
7813 out,
7814 &trigger_stack,
7815 config,
7816 read_epoch,
7817 control,
7818 )?,
7819 None => self.execute_trigger_program(
7820 trigger,
7821 event,
7822 None,
7823 out,
7824 &trigger_stack,
7825 config,
7826 read_epoch,
7827 control,
7828 )?,
7829 };
7830 if outcome == TriggerProgramOutcome::Ignore {
7831 out.ignored_indices.extend(event.op_indices.iter().copied());
7832 break;
7833 }
7834 }
7835 }
7836 Ok(())
7837 }
7838
7839 fn trigger_events_for_staging(
7840 &self,
7841 staging: &[(u64, crate::txn::Staged)],
7842 read_epoch: Epoch,
7843 trigger_stacks: Option<&[Vec<String>]>,
7844 control: Option<&crate::ExecutionControl>,
7845 ) -> Result<Vec<WriteEvent>> {
7846 use crate::txn::Staged;
7847 use std::collections::{HashMap, VecDeque};
7848
7849 let snapshot = Snapshot::at(read_epoch);
7850 let cat = self.catalog.read();
7851 let mut table_names = HashMap::new();
7852 let mut table_schemas = HashMap::new();
7853 for entry in cat
7854 .tables
7855 .iter()
7856 .filter(|entry| matches!(entry.state, TableState::Live))
7857 {
7858 table_names.insert(entry.table_id, entry.name.clone());
7859 table_schemas.insert(entry.table_id, entry.schema.clone());
7860 }
7861 drop(cat);
7862
7863 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
7864 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
7865 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
7866
7867 for (idx, (table_id, staged)) in staging.iter().enumerate() {
7868 commit_prepare_checkpoint(control, idx)?;
7869 let Some(schema) = table_schemas.get(table_id) else {
7870 continue;
7871 };
7872 let Some(pk) = schema.primary_key() else {
7873 continue;
7874 };
7875 match staged {
7876 Staged::Delete(row_id) => {
7877 let handle = self.table_by_id(*table_id)?;
7878 let Some(row) = handle.lock().get(*row_id, snapshot) else {
7879 continue;
7880 };
7881 let Some(pk_value) = row.columns.get(&pk.id) else {
7882 continue;
7883 };
7884 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
7885 delete_by_key
7886 .entry((*table_id, pk_value.encode_key()))
7887 .or_default()
7888 .push_back(idx);
7889 }
7890 Staged::Put(cells) => {
7891 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
7892 put_by_key
7893 .entry((*table_id, value.encode_key()))
7894 .or_default()
7895 .push_back(idx);
7896 }
7897 }
7898 Staged::Update { row_id, .. } => {
7899 let handle = self.table_by_id(*table_id)?;
7900 let row = handle.lock().get(*row_id, snapshot);
7901 if let Some(row) = row {
7902 old_rows.insert(idx, TriggerRowImage::from_row(row));
7903 }
7904 }
7905 Staged::Truncate => {}
7906 }
7907 }
7908
7909 let mut paired_delete = std::collections::HashSet::new();
7910 let mut paired_put = std::collections::HashSet::new();
7911 let mut events = Vec::new();
7912
7913 for (pair_index, (key, deletes)) in delete_by_key.iter_mut().enumerate() {
7914 commit_prepare_checkpoint(control, pair_index)?;
7915 let Some(puts) = put_by_key.get_mut(key) else {
7916 continue;
7917 };
7918 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
7919 paired_delete.insert(delete_idx);
7920 paired_put.insert(put_idx);
7921 let (table_id, _) = &staging[put_idx];
7922 let Some(table_name) = table_names.get(table_id).cloned() else {
7923 continue;
7924 };
7925 let old = old_rows.get(&delete_idx).cloned();
7926 let new = match &staging[put_idx].1 {
7927 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
7928 _ => None,
7929 };
7930 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
7931 events.push(WriteEvent {
7932 table: table_name,
7933 kind: TriggerEvent::Update,
7934 old,
7935 new,
7936 changed_columns,
7937 op_indices: vec![delete_idx, put_idx],
7938 put_idx: Some(put_idx),
7939 trigger_stack: Self::trigger_stack_for_indices(
7940 trigger_stacks,
7941 &[delete_idx, put_idx],
7942 ),
7943 });
7944 }
7945 }
7946
7947 for (idx, (table_id, staged)) in staging.iter().enumerate() {
7948 commit_prepare_checkpoint(control, idx)?;
7949 let Some(table_name) = table_names.get(table_id).cloned() else {
7950 continue;
7951 };
7952 match staged {
7953 Staged::Put(cells) if !paired_put.contains(&idx) => {
7954 let new = Some(TriggerRowImage::from_cells(cells));
7955 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
7956 events.push(WriteEvent {
7957 table: table_name,
7958 kind: TriggerEvent::Insert,
7959 old: None,
7960 new,
7961 changed_columns,
7962 op_indices: vec![idx],
7963 put_idx: Some(idx),
7964 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
7965 });
7966 }
7967 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
7968 let old = match old_rows.get(&idx).cloned() {
7969 Some(old) => Some(old),
7970 None => {
7971 let handle = self.table_by_id(*table_id)?;
7972 let row = handle.lock().get(*row_id, snapshot);
7973 row.map(TriggerRowImage::from_row)
7974 }
7975 };
7976 let Some(old) = old else {
7977 continue;
7978 };
7979 let changed_columns = old.columns.keys().copied().collect();
7980 events.push(WriteEvent {
7981 table: table_name,
7982 kind: TriggerEvent::Delete,
7983 old: Some(old),
7984 new: None,
7985 changed_columns,
7986 op_indices: vec![idx],
7987 put_idx: None,
7988 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
7989 });
7990 }
7991 Staged::Update { new_row: cells, .. } => {
7992 let old = old_rows.get(&idx).cloned();
7993 let new = Some(TriggerRowImage::from_cells(cells));
7994 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
7995 events.push(WriteEvent {
7996 table: table_name,
7997 kind: TriggerEvent::Update,
7998 old,
7999 new,
8000 changed_columns,
8001 op_indices: vec![idx],
8002 put_idx: Some(idx),
8003 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
8004 });
8005 }
8006 Staged::Truncate => {}
8007 _ => {}
8008 }
8009 }
8010
8011 Ok(events)
8012 }
8013
8014 #[allow(clippy::too_many_arguments)]
8015 fn execute_trigger_program(
8016 &self,
8017 trigger: &StoredTrigger,
8018 event: &WriteEvent,
8019 staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
8020 out: &mut TriggerProgramOutput<'_>,
8021 trigger_stack: &[String],
8022 config: &TriggerConfig,
8023 read_epoch: Epoch,
8024 control: Option<&crate::ExecutionControl>,
8025 ) -> Result<TriggerProgramOutcome> {
8026 let mut event = event.clone();
8027 let mut select_results: HashMap<String, Vec<TriggerRowImage>> = HashMap::new();
8028 self.execute_trigger_steps(
8029 trigger,
8030 &trigger.program.steps,
8031 &mut event,
8032 staging,
8033 out,
8034 trigger_stack,
8035 config,
8036 &mut select_results,
8037 0,
8038 None,
8039 read_epoch,
8040 control,
8041 )
8042 }
8043
8044 #[allow(clippy::too_many_arguments)]
8045 fn execute_trigger_steps(
8046 &self,
8047 trigger: &StoredTrigger,
8048 steps: &[TriggerStep],
8049 event: &mut WriteEvent,
8050 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
8051 out: &mut TriggerProgramOutput<'_>,
8052 trigger_stack: &[String],
8053 config: &TriggerConfig,
8054 select_results: &mut HashMap<String, Vec<TriggerRowImage>>,
8055 depth: u32,
8056 selected: Option<&TriggerRowImage>,
8057 read_epoch: Epoch,
8058 control: Option<&crate::ExecutionControl>,
8059 ) -> Result<TriggerProgramOutcome> {
8060 let _ = depth;
8061 for (step_index, step) in steps.iter().enumerate() {
8062 commit_prepare_checkpoint(control, step_index)?;
8063 match step {
8064 TriggerStep::SetNew { cells } => {
8065 if trigger.timing != TriggerTiming::Before {
8066 return Err(MongrelError::InvalidArgument(
8067 "SetNew trigger step is only valid in BEFORE triggers".into(),
8068 ));
8069 }
8070 let put_idx = event.put_idx.ok_or_else(|| {
8071 MongrelError::InvalidArgument(
8072 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
8073 )
8074 })?;
8075 let staging = staging.as_deref_mut().ok_or_else(|| {
8076 MongrelError::InvalidArgument(
8077 "SetNew trigger step requires mutable trigger staging".into(),
8078 )
8079 })?;
8080 let mut update_changed_columns = None;
8081 let row_cells = match staging.get_mut(put_idx).map(|(_, op)| op) {
8082 Some(crate::txn::Staged::Put(cells)) => cells,
8083 Some(crate::txn::Staged::Update {
8084 new_row,
8085 changed_columns,
8086 ..
8087 }) => {
8088 update_changed_columns = Some(changed_columns);
8089 new_row
8090 }
8091 _ => {
8092 return Err(MongrelError::InvalidArgument(
8093 "SetNew trigger step target row is not mutable".into(),
8094 ))
8095 }
8096 };
8097 for (column_id, value) in eval_trigger_cells(cells, event, selected)? {
8098 row_cells.retain(|(id, _)| *id != column_id);
8099 row_cells.push((column_id, value.clone()));
8100 if let Some(changed_columns) = &mut update_changed_columns {
8101 changed_columns.push(column_id);
8102 }
8103 if let Some(new) = &mut event.new {
8104 new.columns.insert(column_id, value);
8105 }
8106 }
8107 row_cells.sort_by_key(|(id, _)| *id);
8108 if let Some(changed_columns) = update_changed_columns {
8109 changed_columns.sort_unstable();
8110 changed_columns.dedup();
8111 }
8112 }
8113 TriggerStep::Insert { table, cells } => {
8114 let cells = eval_trigger_cells(cells, event, selected)?;
8115 if let Ok(table_id) = self.table_id(table) {
8116 out.added.push((table_id, crate::txn::Staged::Put(cells)));
8117 out.added_stacks.push(trigger_stack.to_vec());
8118 } else if self.external_table(table).is_some() {
8119 out.added_external.push(ExternalTriggerWrite::Insert {
8120 table: table.clone(),
8121 cells,
8122 });
8123 } else {
8124 return Err(MongrelError::NotFound(format!(
8125 "trigger {:?} insert target {table:?} not found",
8126 trigger.name
8127 )));
8128 }
8129 }
8130 TriggerStep::UpdateByPk { table, pk, cells } => {
8131 let pk = eval_trigger_value(pk, event, selected)?;
8132 let cells = eval_trigger_cells(cells, event, selected)?;
8133 if self.external_table(table).is_some() {
8134 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
8135 table: table.clone(),
8136 pk,
8137 cells,
8138 });
8139 } else {
8140 let row_id = self
8141 .table(table)?
8142 .lock()
8143 .lookup_pk(&pk.encode_key())
8144 .ok_or_else(|| {
8145 MongrelError::NotFound(format!(
8146 "trigger {:?} update target not found",
8147 trigger.name
8148 ))
8149 })?;
8150 let handle = self.table(table)?;
8151 let snapshot = Snapshot::at(self.epoch.visible());
8152 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
8153 MongrelError::NotFound(format!(
8154 "trigger {:?} update target not visible",
8155 trigger.name
8156 ))
8157 })?;
8158 let mut changed_columns = cells
8159 .iter()
8160 .map(|(column_id, _)| *column_id)
8161 .collect::<Vec<_>>();
8162 changed_columns.sort_unstable();
8163 changed_columns.dedup();
8164 let mut merged = old.columns;
8165 for (column_id, value) in cells {
8166 merged.insert(column_id, value);
8167 }
8168 out.added.push((
8169 self.table_id(table)?,
8170 crate::txn::Staged::Update {
8171 row_id,
8172 new_row: merged.into_iter().collect(),
8173 changed_columns,
8174 },
8175 ));
8176 out.added_stacks.push(trigger_stack.to_vec());
8177 }
8178 }
8179 TriggerStep::DeleteByPk { table, pk } => {
8180 let pk = eval_trigger_value(pk, event, selected)?;
8181 if self.external_table(table).is_some() {
8182 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
8183 table: table.clone(),
8184 pk,
8185 });
8186 } else {
8187 let row_id = self
8188 .table(table)?
8189 .lock()
8190 .lookup_pk(&pk.encode_key())
8191 .ok_or_else(|| {
8192 MongrelError::NotFound(format!(
8193 "trigger {:?} delete target not found",
8194 trigger.name
8195 ))
8196 })?;
8197 out.added
8198 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
8199 out.added_stacks.push(trigger_stack.to_vec());
8200 }
8201 }
8202 TriggerStep::Select {
8203 id,
8204 table,
8205 conditions,
8206 } => {
8207 let schema = self.table(table)?.lock().schema().clone();
8208 let snapshot = Snapshot::at(read_epoch);
8209 let handle = self.table(table)?;
8210 let rows = match control {
8211 Some(control) => {
8212 handle.lock().visible_rows_controlled(snapshot, control)?
8213 }
8214 None => handle.lock().visible_rows(snapshot)?,
8215 };
8216 let mut matched = Vec::new();
8217 for (row_index, row) in rows.into_iter().enumerate() {
8218 commit_prepare_checkpoint(control, row_index)?;
8219 let image = TriggerRowImage::from_row(row);
8220 let passes = conditions
8221 .iter()
8222 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
8223 .collect::<Result<Vec<_>>>()?
8224 .into_iter()
8225 .all(|b| b);
8226 if passes {
8227 matched.push(image);
8228 }
8229 }
8230 if let Some(pk) = schema.primary_key() {
8231 matched.sort_by(|a, b| {
8232 let av = a.columns.get(&pk.id).unwrap_or(&Value::Null);
8233 let bv = b.columns.get(&pk.id).unwrap_or(&Value::Null);
8234 value_order(av, bv).unwrap_or(std::cmp::Ordering::Equal)
8235 });
8236 }
8237 select_results.insert(id.clone(), matched);
8238 }
8239 TriggerStep::Foreach { id, steps } => {
8240 let rows = select_results.get(id).ok_or_else(|| {
8241 MongrelError::InvalidArgument(format!(
8242 "trigger {:?} foreach references unknown select id {id:?}",
8243 trigger.name
8244 ))
8245 })?;
8246 if rows.len() > config.max_loop_iterations as usize {
8247 return Err(MongrelError::InvalidArgument(format!(
8248 "trigger {:?} foreach exceeded max_loop_iterations ({})",
8249 trigger.name, config.max_loop_iterations
8250 )));
8251 }
8252 for (row_index, row) in rows.clone().into_iter().enumerate() {
8253 commit_prepare_checkpoint(control, row_index)?;
8254 let result = self.execute_trigger_steps(
8255 trigger,
8256 steps,
8257 event,
8258 staging.as_deref_mut(),
8259 out,
8260 trigger_stack,
8261 config,
8262 select_results,
8263 depth + 1,
8264 Some(&row),
8265 read_epoch,
8266 control,
8267 )?;
8268 if result == TriggerProgramOutcome::Ignore {
8269 return Ok(TriggerProgramOutcome::Ignore);
8270 }
8271 }
8272 }
8273 TriggerStep::DeleteWhere { table, conditions } => {
8274 let schema = self.table(table)?.lock().schema().clone();
8275 let snapshot = Snapshot::at(read_epoch);
8276 let handle = self.table(table)?;
8277 let rows = match control {
8278 Some(control) => {
8279 handle.lock().visible_rows_controlled(snapshot, control)?
8280 }
8281 None => handle.lock().visible_rows(snapshot)?,
8282 };
8283 let table_id = self.table_id(table)?;
8284 let mut to_delete = Vec::new();
8285 for (row_index, row) in rows.into_iter().enumerate() {
8286 commit_prepare_checkpoint(control, row_index)?;
8287 let image = TriggerRowImage::from_row(row.clone());
8288 let passes = conditions
8289 .iter()
8290 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
8291 .collect::<Result<Vec<_>>>()?
8292 .into_iter()
8293 .all(|b| b);
8294 if passes {
8295 to_delete.push((table_id, row.row_id));
8296 }
8297 }
8298 for (row_index, (table_id, row_id)) in to_delete.into_iter().enumerate() {
8299 commit_prepare_checkpoint(control, row_index)?;
8300 out.added
8301 .push((table_id, crate::txn::Staged::Delete(row_id)));
8302 out.added_stacks.push(trigger_stack.to_vec());
8303 }
8304 }
8305 TriggerStep::UpdateWhere {
8306 table,
8307 conditions,
8308 cells,
8309 } => {
8310 let schema = self.table(table)?.lock().schema().clone();
8311 let snapshot = Snapshot::at(read_epoch);
8312 let handle = self.table(table)?;
8313 let rows = match control {
8314 Some(control) => {
8315 handle.lock().visible_rows_controlled(snapshot, control)?
8316 }
8317 None => handle.lock().visible_rows(snapshot)?,
8318 };
8319 let table_id = self.table_id(table)?;
8320 let mut changed_columns =
8321 cells.iter().map(|cell| cell.column_id).collect::<Vec<_>>();
8322 changed_columns.sort_unstable();
8323 changed_columns.dedup();
8324 let mut to_update = Vec::new();
8325 for (row_index, row) in rows.into_iter().enumerate() {
8326 commit_prepare_checkpoint(control, row_index)?;
8327 let image = TriggerRowImage::from_row(row.clone());
8328 let passes = conditions
8329 .iter()
8330 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
8331 .collect::<Result<Vec<_>>>()?
8332 .into_iter()
8333 .all(|b| b);
8334 if passes {
8335 let new_cells = cells
8336 .iter()
8337 .map(|cell| {
8338 Ok((
8339 cell.column_id,
8340 eval_trigger_value(&cell.value, event, Some(&image))?,
8341 ))
8342 })
8343 .collect::<Result<Vec<_>>>()?;
8344 let mut merged = row.columns.clone();
8345 for (column_id, value) in new_cells {
8346 merged.insert(column_id, value);
8347 }
8348 to_update.push((table_id, row.row_id, merged));
8349 }
8350 }
8351 for (row_index, (table_id, row_id, merged)) in to_update.into_iter().enumerate()
8352 {
8353 commit_prepare_checkpoint(control, row_index)?;
8354 out.added.push((
8355 table_id,
8356 crate::txn::Staged::Update {
8357 row_id,
8358 new_row: merged.into_iter().collect(),
8359 changed_columns: changed_columns.clone(),
8360 },
8361 ));
8362 out.added_stacks.push(trigger_stack.to_vec());
8363 }
8364 }
8365 TriggerStep::Raise { action, message } => match action {
8366 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
8367 TriggerRaiseAction::Abort
8368 | TriggerRaiseAction::Fail
8369 | TriggerRaiseAction::Rollback => {
8370 let message = eval_trigger_value(message, event, selected)?;
8371 return Err(MongrelError::TriggerValidation(format!(
8372 "trigger {:?} raised: {}; trigger stack: {}",
8373 trigger.name,
8374 trigger_message(message),
8375 Self::format_trigger_stack(trigger_stack)
8376 )));
8377 }
8378 },
8379 }
8380 }
8381 Ok(TriggerProgramOutcome::Continue)
8382 }
8383
8384 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
8385 let Some(stacks) = stacks else {
8386 return Vec::new();
8387 };
8388 let mut out = Vec::new();
8389 for idx in indices {
8390 let Some(stack) = stacks.get(*idx) else {
8391 continue;
8392 };
8393 for name in stack {
8394 if !out.iter().any(|existing| existing == name) {
8395 out.push(name.clone());
8396 }
8397 }
8398 }
8399 out
8400 }
8401
8402 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
8403 let mut out = stack.to_vec();
8404 out.push(trigger_name.to_string());
8405 out
8406 }
8407
8408 fn format_trigger_stack(stack: &[String]) -> String {
8409 if stack.is_empty() {
8410 "<root>".into()
8411 } else {
8412 stack.join(" -> ")
8413 }
8414 }
8415
8416 fn validate_constraints(
8431 &self,
8432 staging: &mut Vec<(u64, crate::txn::Staged)>,
8433 read_epoch: Epoch,
8434 control: Option<&crate::ExecutionControl>,
8435 ) -> Result<()> {
8436 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
8437 use crate::memtable::Row;
8438 use crate::txn::Staged;
8439 use std::collections::HashSet;
8440
8441 commit_prepare_checkpoint(control, 0)?;
8442 let snapshot = Snapshot::at(read_epoch);
8443 let cat = self.catalog.read();
8444
8445 let live: Vec<(u64, &str, &crate::schema::Schema)> = cat
8447 .tables
8448 .iter()
8449 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
8450 .map(|e| (e.table_id, e.name.as_str(), &e.schema))
8451 .collect();
8452
8453 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
8455 if !any_constraints {
8456 return Ok(());
8457 }
8458
8459 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
8461 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
8462 if let Some(r) = rows_cache.get(&table_id) {
8463 return Ok(r.clone());
8464 }
8465 let handle = self.table_by_id(table_id)?;
8466 let rows = match control {
8467 Some(control) => handle.lock().visible_rows_controlled(snapshot, control)?,
8468 None => handle.lock().visible_rows(snapshot)?,
8469 };
8470 rows_cache.insert(table_id, rows.clone());
8471 Ok(rows)
8472 };
8473
8474 let mut processed_updates = HashSet::new();
8479 type PendingUpdate = (usize, u64, crate::rowid::RowId, Vec<(u16, Value)>);
8480 let mut update_pass = 0_usize;
8481 loop {
8482 commit_prepare_checkpoint(control, update_pass)?;
8483 update_pass += 1;
8484 let updates: Vec<PendingUpdate> = staging
8485 .iter()
8486 .enumerate()
8487 .filter_map(|(index, (table_id, op))| match op {
8488 Staged::Update {
8489 row_id,
8490 new_row: cells,
8491 ..
8492 } if !processed_updates.contains(&index) => {
8493 Some((index, *table_id, *row_id, cells.clone()))
8494 }
8495 _ => None,
8496 })
8497 .collect();
8498 if updates.is_empty() {
8499 break;
8500 }
8501 let mut new_ops = Vec::new();
8502 for (update_index, (index, table_id, row_id, new_cells)) in
8503 updates.into_iter().enumerate()
8504 {
8505 commit_prepare_checkpoint(control, update_index)?;
8506 processed_updates.insert(index);
8507 let Some(tname) = live
8508 .iter()
8509 .find(|(id, _, _)| *id == table_id)
8510 .map(|(_, name, _)| *name)
8511 else {
8512 continue;
8513 };
8514 let Some(old_row) = self.table_by_id(table_id)?.lock().get(row_id, snapshot) else {
8515 continue;
8516 };
8517 let new_map: HashMap<u16, Value> = new_cells.iter().cloned().collect();
8518 for (child_id, _child_name, child_schema) in &live {
8519 for fk in &child_schema.constraints.foreign_keys {
8520 if fk.ref_table != tname {
8521 continue;
8522 }
8523 let Some(old_key) = encode_composite_key(&fk.ref_columns, &old_row.columns)
8524 else {
8525 continue;
8526 };
8527 if encode_composite_key(&fk.ref_columns, &new_map).as_deref()
8528 == Some(old_key.as_slice())
8529 {
8530 continue;
8531 }
8532 if fk.on_update == FkAction::Restrict {
8533 continue;
8534 }
8535 let child_rows = load_rows(*child_id)?;
8536 for (child_index, child) in child_rows.into_iter().enumerate() {
8537 commit_prepare_checkpoint(control, child_index)?;
8538 if encode_composite_key(&fk.columns, &child.columns).as_deref()
8539 != Some(old_key.as_slice())
8540 {
8541 continue;
8542 }
8543 if staging.iter().any(|(id, op)| {
8544 *id == *child_id
8545 && matches!(op, Staged::Delete(id) if *id == child.row_id)
8546 }) {
8547 continue;
8548 }
8549 let mut cells: Vec<(u16, Value)> = child
8550 .columns
8551 .iter()
8552 .map(|(column_id, value)| (*column_id, value.clone()))
8553 .collect();
8554 for (child_column, parent_column) in
8555 fk.columns.iter().zip(&fk.ref_columns)
8556 {
8557 cells.retain(|(column_id, _)| column_id != child_column);
8558 let value = match fk.on_update {
8559 FkAction::Cascade => {
8560 new_map.get(parent_column).cloned().unwrap_or(Value::Null)
8561 }
8562 FkAction::SetNull => Value::Null,
8563 FkAction::Restrict => {
8564 return Err(MongrelError::Other(
8565 "restricted foreign-key update reached cascade preparation"
8566 .into(),
8567 ));
8568 }
8569 };
8570 cells.push((*child_column, value));
8571 }
8572 cells.sort_by_key(|(column_id, _)| *column_id);
8573 if let Some(existing_index) = staging.iter().position(|(id, op)| {
8574 *id == *child_id
8575 && matches!(op, Staged::Update { row_id, .. } if *row_id == child.row_id)
8576 }) {
8577 if let Staged::Update {
8578 new_row: existing,
8579 changed_columns,
8580 ..
8581 } = &mut staging[existing_index].1 {
8582 changed_columns.extend(fk.columns.iter().copied());
8583 changed_columns.sort_unstable();
8584 changed_columns.dedup();
8585 if *existing != cells {
8586 *existing = cells;
8587 processed_updates.remove(&existing_index);
8588 }
8589 }
8590 } else {
8591 new_ops.push((
8592 *child_id,
8593 Staged::Update {
8594 row_id: child.row_id,
8595 new_row: cells,
8596 changed_columns: fk.columns.clone(),
8597 },
8598 ));
8599 }
8600 }
8601 }
8602 }
8603 }
8604 staging.extend(new_ops);
8605 }
8606
8607 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
8612 let mut cascade_pass = 0_usize;
8613 loop {
8614 commit_prepare_checkpoint(control, cascade_pass)?;
8615 cascade_pass += 1;
8616 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
8617 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
8618 .iter()
8619 .filter_map(|(t, op)| match op {
8620 Staged::Delete(rid) => Some((*t, *rid)),
8621 _ => None,
8622 })
8623 .collect();
8624 for (delete_index, (table_id, rid)) in deletes.into_iter().enumerate() {
8625 commit_prepare_checkpoint(control, delete_index)?;
8626 if !cascaded.insert((table_id, rid.0)) {
8627 continue;
8628 }
8629 let Some(tname) = live
8630 .iter()
8631 .find(|(t, _, _)| *t == table_id)
8632 .map(|(_, n, _)| *n)
8633 else {
8634 continue;
8635 };
8636 let parent_handle = self.table_by_id(table_id)?;
8637 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
8638 continue;
8639 };
8640 for (child_id, _child_name, child_schema) in &live {
8641 for fk in &child_schema.constraints.foreign_keys {
8642 if fk.ref_table != tname {
8643 continue;
8644 }
8645 let Some(parent_key) =
8646 encode_composite_key(&fk.ref_columns, &parent_row.columns)
8647 else {
8648 continue;
8649 };
8650 let key_preserved = staging.iter().any(|(t, op)| {
8659 if *t != table_id {
8660 return false;
8661 }
8662 let Staged::Put(cells) = op else {
8663 return false;
8664 };
8665 let map: HashMap<u16, crate::memtable::Value> =
8666 cells.iter().cloned().collect();
8667 encode_composite_key(&fk.ref_columns, &map).as_deref()
8668 == Some(parent_key.as_slice())
8669 });
8670 if key_preserved {
8671 continue;
8672 }
8673 match fk.on_delete {
8674 FkAction::Restrict => continue,
8675 FkAction::Cascade => {
8676 let child_rows = load_rows(*child_id)?;
8677 for (child_index, cr) in child_rows.iter().enumerate() {
8678 commit_prepare_checkpoint(control, child_index)?;
8679 if !cascaded.contains(&(*child_id, cr.row_id.0))
8680 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
8681 == Some(parent_key.as_slice())
8682 {
8683 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
8684 }
8685 }
8686 }
8687 FkAction::SetNull => {
8688 let child_rows = load_rows(*child_id)?;
8689 for (child_index, cr) in child_rows.iter().enumerate() {
8690 commit_prepare_checkpoint(control, child_index)?;
8691 if !cascaded.contains(&(*child_id, cr.row_id.0))
8692 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
8693 == Some(parent_key.as_slice())
8694 {
8695 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
8698 .columns
8699 .iter()
8700 .map(|(k, v)| (*k, v.clone()))
8701 .collect();
8702 for cid in &fk.columns {
8703 cells.retain(|(k, _)| k != cid);
8704 cells.push((*cid, crate::memtable::Value::Null));
8705 }
8706 new_ops.push((
8707 *child_id,
8708 Staged::Update {
8709 row_id: cr.row_id,
8710 new_row: cells,
8711 changed_columns: fk.columns.clone(),
8712 },
8713 ));
8714 }
8715 }
8716 }
8717 }
8718 }
8719 }
8720 }
8721 if new_ops.is_empty() {
8722 break;
8723 }
8724 staging.extend(new_ops);
8725 }
8726
8727 let staged_deletes: HashSet<(u64, u64)> = staging
8731 .iter()
8732 .filter_map(|(t, op)| match op {
8733 Staged::Delete(rid) | Staged::Update { row_id: rid, .. } => Some((*t, rid.0)),
8734 _ => None,
8735 })
8736 .collect();
8737
8738 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
8740
8741 for (operation_index, (table_id, op)) in staging.iter().enumerate() {
8743 commit_prepare_checkpoint(control, operation_index)?;
8744 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id).copied()
8745 else {
8746 continue;
8747 };
8748 let cells_map: HashMap<u16, crate::memtable::Value>;
8749 match op {
8750 Staged::Put(cells) | Staged::Update { new_row: cells, .. } => {
8751 cells_map = cells.iter().cloned().collect();
8752
8753 if !schema.constraints.checks.is_empty() {
8755 validate_checks(&schema.constraints.checks, &cells_map)?;
8756 }
8757
8758 for uc in &schema.constraints.uniques {
8760 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
8761 continue; };
8763 let marker = (*table_id, uc.id, key.clone());
8764 if !seen_unique.insert(marker) {
8765 return Err(MongrelError::Conflict(format!(
8766 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
8767 uc.name
8768 )));
8769 }
8770 let rows = load_rows(*table_id)?;
8771 for (row_index, r) in rows.iter().enumerate() {
8772 commit_prepare_checkpoint(control, row_index)?;
8773 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
8776 continue;
8777 }
8778 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
8779 if theirs == key {
8780 return Err(MongrelError::Conflict(format!(
8781 "UNIQUE constraint '{}' on table '{tname}' violated",
8782 uc.name
8783 )));
8784 }
8785 }
8786 }
8787 }
8788
8789 for fk in &schema.constraints.foreign_keys {
8791 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
8792 continue; };
8794 let Some(parent_id) = cat
8795 .tables
8796 .iter()
8797 .find(|t| t.name == fk.ref_table)
8798 .map(|t| t.table_id)
8799 else {
8800 return Err(MongrelError::InvalidArgument(format!(
8801 "FOREIGN KEY '{}' references unknown table '{}'",
8802 fk.name, fk.ref_table
8803 )));
8804 };
8805 let parent_rows = load_rows(parent_id)?;
8806 let mut found = false;
8807 for (row_index, r) in parent_rows.iter().enumerate() {
8808 commit_prepare_checkpoint(control, row_index)?;
8809 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
8810 continue;
8811 }
8812 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
8813 if pkey == child_key {
8814 found = true;
8815 break;
8816 }
8817 }
8818 }
8819 if !found {
8826 for (staged_index, (st_table, st_op)) in staging.iter().enumerate() {
8827 commit_prepare_checkpoint(control, staged_index)?;
8828 if *st_table != parent_id {
8829 continue;
8830 }
8831 if let Staged::Put(pcells)
8832 | Staged::Update {
8833 new_row: pcells, ..
8834 } = st_op
8835 {
8836 let pmap: HashMap<u16, crate::memtable::Value> =
8837 pcells.iter().cloned().collect();
8838 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &pmap)
8839 {
8840 if pkey == child_key {
8841 found = true;
8842 break;
8843 }
8844 }
8845 }
8846 }
8847 }
8848 if !found {
8849 return Err(MongrelError::Conflict(format!(
8850 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
8851 fk.name, fk.ref_table
8852 )));
8853 }
8854 }
8855
8856 if let Staged::Update { row_id, .. } = op {
8861 let parent_handle = self.table_by_id(*table_id)?;
8862 let Some(old_parent) = parent_handle.lock().get(*row_id, snapshot) else {
8863 continue;
8864 };
8865 for (child_id, child_name, child_schema) in &live {
8866 for fk in &child_schema.constraints.foreign_keys {
8867 if fk.ref_table != tname || fk.on_update != FkAction::Restrict {
8868 continue;
8869 }
8870 let Some(old_key) =
8871 encode_composite_key(&fk.ref_columns, &old_parent.columns)
8872 else {
8873 continue;
8874 };
8875 if encode_composite_key(&fk.ref_columns, &cells_map).as_deref()
8876 == Some(old_key.as_slice())
8877 {
8878 continue;
8879 }
8880 for (child_index, child) in
8881 load_rows(*child_id)?.into_iter().enumerate()
8882 {
8883 commit_prepare_checkpoint(control, child_index)?;
8884 if encode_composite_key(&fk.columns, &child.columns).as_deref()
8885 != Some(old_key.as_slice())
8886 {
8887 continue;
8888 }
8889 let replacement = staging.iter().find_map(|(id, op)| {
8890 if *id != *child_id {
8891 return None;
8892 }
8893 match op {
8894 Staged::Delete(id) if *id == child.row_id => Some(None),
8895 Staged::Update {
8896 row_id,
8897 new_row: cells,
8898 ..
8899 } if *row_id == child.row_id => {
8900 let map: HashMap<u16, Value> =
8901 cells.iter().cloned().collect();
8902 Some(encode_composite_key(&fk.columns, &map))
8903 }
8904 _ => None,
8905 }
8906 });
8907 if replacement.is_some_and(|key| {
8908 key.as_deref() != Some(old_key.as_slice())
8909 }) {
8910 continue;
8911 }
8912 return Err(MongrelError::Conflict(format!(
8913 "FOREIGN KEY '{}' on table '{child_name}' restricts update (parent key referenced)",
8914 fk.name
8915 )));
8916 }
8917 }
8918 }
8919 }
8920 }
8921 Staged::Delete(rid) => {
8922 let parent_handle = self.table_by_id(*table_id)?;
8926 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
8927 continue;
8928 };
8929 for (child_id, child_name, child_schema) in &live {
8930 for fk in &child_schema.constraints.foreign_keys {
8931 if fk.ref_table != tname || fk.on_delete != FkAction::Restrict {
8932 continue;
8933 }
8934 let Some(parent_key) =
8935 encode_composite_key(&fk.ref_columns, &parent_row.columns)
8936 else {
8937 continue;
8938 };
8939 let child_rows = load_rows(*child_id)?;
8940 for (row_index, r) in child_rows.iter().enumerate() {
8941 commit_prepare_checkpoint(control, row_index)?;
8942 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
8945 continue;
8946 }
8947 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
8948 if ck == parent_key {
8949 return Err(MongrelError::Conflict(format!(
8950 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
8951 fk.name
8952 )));
8953 }
8954 }
8955 }
8956 }
8957 }
8958 }
8959 Staged::Truncate => {
8960 for (child_id, child_name, child_schema) in &live {
8964 for fk in &child_schema.constraints.foreign_keys {
8965 if fk.ref_table != tname {
8966 continue;
8967 }
8968 let child_rows = load_rows(*child_id)?;
8969 if child_rows
8970 .iter()
8971 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
8972 {
8973 return Err(MongrelError::Conflict(format!(
8974 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
8975 fk.name
8976 )));
8977 }
8978 }
8979 }
8980 }
8981 }
8982 }
8983 Ok(())
8984 }
8985
8986 fn validate_write_permissions(
8987 &self,
8988 staging: &[(u64, crate::txn::Staged)],
8989 principal: Option<&crate::auth::Principal>,
8990 control: Option<&crate::ExecutionControl>,
8991 ) -> Result<()> {
8992 commit_prepare_checkpoint(control, 0)?;
8993 if principal.is_none() && !self.auth_state.require_auth() {
8994 return Ok(());
8995 }
8996 let principal = principal.ok_or(MongrelError::AuthRequired)?;
8997 let needs = summarize_write_permissions(staging);
8998 let catalog = self.catalog.read();
8999
9000 if needs.values().any(|need| need.truncate) {
9001 self.require_for(Some(principal), &crate::auth::Permission::Admin)?;
9002 }
9003 for (need_index, (table_id, need)) in needs.into_iter().enumerate() {
9004 commit_prepare_checkpoint(control, need_index)?;
9005 let entry = catalog
9006 .tables
9007 .iter()
9008 .find(|entry| {
9009 entry.table_id == table_id
9010 && matches!(entry.state, TableState::Live | TableState::Building { .. })
9011 })
9012 .ok_or_else(|| {
9013 MongrelError::NotFound(format!(
9014 "live table {table_id} not found during write validation"
9015 ))
9016 })?;
9017 if matches!(entry.state, TableState::Building { .. }) {
9018 self.require_for(Some(principal), &crate::auth::Permission::Ddl)?;
9019 continue;
9020 }
9021 if need.insert {
9022 Self::require_columns_for_principal(
9023 &entry.name,
9024 &entry.schema,
9025 crate::auth::ColumnOperation::Insert,
9026 &need.insert_columns,
9027 principal,
9028 )?;
9029 }
9030 if need.update {
9031 Self::require_columns_for_principal(
9032 &entry.name,
9033 &entry.schema,
9034 crate::auth::ColumnOperation::Update,
9035 &need.update_columns,
9036 principal,
9037 )?;
9038 }
9039 if need.delete {
9040 self.require_for(
9041 Some(principal),
9042 &crate::auth::Permission::Delete {
9043 table: entry.name.clone(),
9044 },
9045 )?;
9046 }
9047 }
9048 Ok(())
9049 }
9050
9051 fn validate_security_writes(
9052 &self,
9053 staging: &[(u64, crate::txn::Staged)],
9054 read_epoch: Epoch,
9055 explicit_principal: Option<&crate::auth::Principal>,
9056 control: Option<&crate::ExecutionControl>,
9057 ) -> Result<()> {
9058 commit_prepare_checkpoint(control, 0)?;
9059 use crate::security::PolicyCommand;
9060 use crate::txn::Staged;
9061
9062 let catalog = self.catalog.read();
9063 if catalog.security.rls_tables.is_empty() {
9064 return Ok(());
9065 }
9066 let security = catalog.security.clone();
9067 let table_names = catalog
9068 .tables
9069 .iter()
9070 .filter(|entry| matches!(entry.state, TableState::Live))
9071 .map(|entry| (entry.table_id, entry.name.clone()))
9072 .collect::<HashMap<_, _>>();
9073 drop(catalog);
9074 if !staging.iter().any(|(table_id, _)| {
9075 table_names
9076 .get(table_id)
9077 .is_some_and(|table| security.rls_enabled(table))
9078 }) {
9079 return Ok(());
9080 }
9081 let principal = explicit_principal.ok_or(MongrelError::AuthRequired)?;
9082
9083 for (operation_index, (table_id, operation)) in staging.iter().enumerate() {
9084 commit_prepare_checkpoint(control, operation_index)?;
9085 let Some(table) = table_names.get(table_id) else {
9086 continue;
9087 };
9088 if !security.rls_enabled(table) || principal.is_admin {
9089 continue;
9090 }
9091 let denied = |command| MongrelError::PermissionDenied {
9092 required: match command {
9093 PolicyCommand::Insert => crate::auth::Permission::Insert {
9094 table: table.clone(),
9095 },
9096 PolicyCommand::Update => crate::auth::Permission::Update {
9097 table: table.clone(),
9098 },
9099 PolicyCommand::Delete | PolicyCommand::All | PolicyCommand::Select => {
9100 crate::auth::Permission::Delete {
9101 table: table.clone(),
9102 }
9103 }
9104 },
9105 principal: principal.username.clone(),
9106 };
9107 match operation {
9108 Staged::Put(cells) => {
9109 let mut row = crate::memtable::Row::new(RowId(0), Epoch(read_epoch.0));
9110 row.columns.extend(cells.iter().cloned());
9111 if !security.row_allowed(table, PolicyCommand::Insert, &row, principal, true) {
9112 return Err(denied(PolicyCommand::Insert));
9113 }
9114 }
9115 Staged::Update {
9116 row_id,
9117 new_row: cells,
9118 ..
9119 } => {
9120 let old = self
9121 .table_by_id(*table_id)?
9122 .lock()
9123 .get(*row_id, Snapshot::at(read_epoch))
9124 .ok_or_else(|| {
9125 MongrelError::NotFound(format!("row {} not found", row_id.0))
9126 })?;
9127 if !security.row_allowed(table, PolicyCommand::Update, &old, principal, false) {
9128 return Err(denied(PolicyCommand::Update));
9129 }
9130 let mut new = crate::memtable::Row::new(*row_id, Epoch(read_epoch.0));
9131 new.columns.extend(cells.iter().cloned());
9132 if !security.row_allowed(table, PolicyCommand::Update, &new, principal, true) {
9133 return Err(denied(PolicyCommand::Update));
9134 }
9135 }
9136 Staged::Delete(row_id) => {
9137 let old = self
9138 .table_by_id(*table_id)?
9139 .lock()
9140 .get(*row_id, Snapshot::at(read_epoch))
9141 .ok_or_else(|| {
9142 MongrelError::NotFound(format!("row {} not found", row_id.0))
9143 })?;
9144 if !security.row_allowed(table, PolicyCommand::Delete, &old, principal, false) {
9145 return Err(denied(PolicyCommand::Delete));
9146 }
9147 }
9148 Staged::Truncate => return Err(denied(PolicyCommand::Delete)),
9149 }
9150 }
9151 Ok(())
9152 }
9153
9154 #[allow(clippy::too_many_arguments)]
9161 pub(crate) fn commit_transaction_with_external_states(
9162 &self,
9163 txn_id: u64,
9164 read_epoch: Epoch,
9165 staging: Vec<(u64, crate::txn::Staged)>,
9166 external_states: Vec<(String, Vec<u8>)>,
9167 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
9168 security_principal: Option<crate::auth::Principal>,
9169 principal_catalog_bound: bool,
9170 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9171 ) -> Result<(Epoch, Vec<RowId>)> {
9172 self.commit_transaction_with_external_states_inner(
9173 txn_id,
9174 read_epoch,
9175 staging,
9176 external_states,
9177 materialized_view_updates,
9178 security_principal,
9179 principal_catalog_bound,
9180 external_trigger_bridge,
9181 None,
9182 None,
9183 )
9184 }
9185
9186 #[allow(clippy::too_many_arguments)]
9187 pub(crate) fn commit_transaction_with_external_states_controlled(
9188 &self,
9189 txn_id: u64,
9190 read_epoch: Epoch,
9191 staging: Vec<(u64, crate::txn::Staged)>,
9192 external_states: Vec<(String, Vec<u8>)>,
9193 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
9194 security_principal: Option<crate::auth::Principal>,
9195 principal_catalog_bound: bool,
9196 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9197 control: &crate::ExecutionControl,
9198 before_commit: &mut dyn FnMut() -> Result<()>,
9199 ) -> Result<(Epoch, Vec<RowId>)> {
9200 self.commit_transaction_with_external_states_inner(
9201 txn_id,
9202 read_epoch,
9203 staging,
9204 external_states,
9205 materialized_view_updates,
9206 security_principal,
9207 principal_catalog_bound,
9208 external_trigger_bridge,
9209 Some(control),
9210 Some(before_commit),
9211 )
9212 }
9213
9214 #[allow(clippy::too_many_arguments)]
9215 fn commit_transaction_with_external_states_inner(
9216 &self,
9217 txn_id: u64,
9218 read_epoch: Epoch,
9219 mut staging: Vec<(u64, crate::txn::Staged)>,
9220 external_states: Vec<(String, Vec<u8>)>,
9221 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
9222 mut security_principal: Option<crate::auth::Principal>,
9223 principal_catalog_bound: bool,
9224 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9225 control: Option<&crate::ExecutionControl>,
9226 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
9227 ) -> Result<(Epoch, Vec<RowId>)> {
9228 use crate::memtable::Row;
9229 use crate::txn::{Staged, StagedOp, WriteKey};
9230 use crate::wal::Op;
9231 use std::collections::hash_map::DefaultHasher;
9232 use std::hash::{Hash, Hasher};
9233 use std::sync::atomic::Ordering;
9234
9235 if txn_id == crate::wal::SYSTEM_TXN_ID {
9236 return Err(MongrelError::Full(
9237 "per-open transaction id namespace exhausted; reopen the database".into(),
9238 ));
9239 }
9240 if self.read_only {
9241 return Err(MongrelError::ReadOnlyReplica);
9242 }
9243 commit_prepare_checkpoint(control, 0)?;
9244 let observed_security_version = self.security_coordinator.version.load(Ordering::Acquire);
9245 self.refresh_security_catalog_if_stale(observed_security_version)?;
9246 let trigger_binding = trigger_catalog_binding(&self.catalog.read());
9247 if self.auth_state.require_auth() && security_principal.is_none() {
9248 return Err(MongrelError::AuthRequired);
9249 }
9250 {
9251 let catalog = self.catalog.read();
9252 if catalog.require_auth
9253 || principal_catalog_bound
9254 || security_principal
9255 .as_ref()
9256 .is_some_and(|principal| principal.user_id != 0)
9257 {
9258 let principal = security_principal
9259 .as_ref()
9260 .ok_or(MongrelError::AuthRequired)?;
9261 security_principal =
9262 Self::resolve_bound_principal_from_catalog(&catalog, principal);
9263 if security_principal.is_none() {
9264 return Err(MongrelError::AuthRequired);
9265 }
9266 }
9267 }
9268 let _replication_guard = self.replication_barrier.read();
9269 if self.poisoned.load(Ordering::Relaxed) {
9270 return Err(MongrelError::Other(
9271 "database poisoned by fsync error".into(),
9272 ));
9273 }
9274 let mut external_states = dedup_external_states(external_states);
9275 if !external_states.is_empty() {
9276 let cat = self.catalog.read();
9277 for (name, _) in &external_states {
9278 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
9279 return Err(MongrelError::NotFound(format!(
9280 "external table {name:?} not found"
9281 )));
9282 }
9283 }
9284 }
9285 let prepared_materialized_views = {
9286 let mut deduplicated = HashMap::new();
9287 for (definition_index, definition) in materialized_view_updates.into_iter().enumerate()
9288 {
9289 commit_prepare_checkpoint(control, definition_index)?;
9290 if definition.name.is_empty() || definition.query.trim().is_empty() {
9291 return Err(MongrelError::InvalidArgument(
9292 "materialized view name and query must not be empty".into(),
9293 ));
9294 }
9295 deduplicated.insert(definition.name.clone(), definition);
9296 }
9297 let catalog = self.catalog.read();
9298 let mut prepared = Vec::with_capacity(deduplicated.len());
9299 for (definition_index, definition) in deduplicated.into_values().enumerate() {
9300 commit_prepare_checkpoint(control, definition_index)?;
9301 let table_id = catalog
9302 .live(&definition.name)
9303 .ok_or_else(|| {
9304 MongrelError::NotFound(format!(
9305 "materialized view table {:?} not found",
9306 definition.name
9307 ))
9308 })?
9309 .table_id;
9310 prepared.push((table_id, definition));
9311 }
9312 prepared.sort_by(|left, right| left.1.name.cmp(&right.1.name));
9313 prepared
9314 };
9315
9316 self.fill_auto_increment_for_staging(&mut staging, control)?;
9319 self.expand_table_triggers(
9320 &mut staging,
9321 read_epoch,
9322 external_trigger_bridge,
9323 &mut external_states,
9324 control,
9325 )?;
9326 self.fill_auto_increment_for_staging(&mut staging, control)?;
9327 external_states = dedup_external_states(external_states);
9328 let expected_external_generations = {
9329 let catalog = self.catalog.read();
9330 let mut generations = HashMap::with_capacity(external_states.len());
9331 for (name, _) in &external_states {
9332 let entry = catalog
9333 .external_tables
9334 .iter()
9335 .find(|entry| entry.name == *name)
9336 .ok_or_else(|| {
9337 MongrelError::NotFound(format!("external table {name:?} not found"))
9338 })?;
9339 generations.insert(name.clone(), entry.created_epoch);
9340 }
9341 generations
9342 };
9343
9344 self.validate_constraints(&mut staging, read_epoch, control)?;
9349 self.validate_write_permissions(&staging, security_principal.as_ref(), control)?;
9350 self.validate_security_writes(&staging, read_epoch, security_principal.as_ref(), control)?;
9351 let mut normalized = Vec::with_capacity(staging.len() * 2);
9352 for (staged_index, (table_id, op)) in staging.into_iter().enumerate() {
9353 commit_prepare_checkpoint(control, staged_index)?;
9354 match op {
9355 crate::txn::Staged::Update {
9356 row_id,
9357 new_row: cells,
9358 ..
9359 } => {
9360 normalized.push((table_id, crate::txn::Staged::Delete(row_id)));
9361 normalized.push((table_id, crate::txn::Staged::Put(cells)));
9362 }
9363 op => normalized.push((table_id, op)),
9364 }
9365 }
9366 staging = normalized;
9367 let has_changes = !staging.is_empty()
9368 || !external_states.is_empty()
9369 || !prepared_materialized_views.is_empty();
9370 let truncated_tables: HashSet<u64> = staging
9371 .iter()
9372 .filter_map(|(table_id, op)| matches!(op, Staged::Truncate).then_some(*table_id))
9373 .collect();
9374
9375 let write_keys = {
9376 let cat = self.catalog.read();
9377 let mut keys: Vec<WriteKey> = Vec::new();
9378 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
9379 commit_prepare_checkpoint(control, staged_index)?;
9380 match staged {
9381 Staged::Put(cells) => {
9382 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
9383 for col in &entry.schema.columns {
9384 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
9385 if let Some((_, val)) =
9386 cells.iter().find(|(id, _)| *id == col.id)
9387 {
9388 let mut h = DefaultHasher::new();
9389 val.encode_key().hash(&mut h);
9390 keys.push(WriteKey::Unique {
9391 table_id: *table_id,
9392 index_id: 0,
9393 key_hash: h.finish(),
9394 });
9395 }
9396 }
9397 }
9398 for uc in &entry.schema.constraints.uniques {
9405 if let Some(key_bytes) = crate::constraint::encode_composite_key(
9406 &uc.columns,
9407 &cells.iter().cloned().collect(),
9408 ) {
9409 let mut h = DefaultHasher::new();
9410 key_bytes.hash(&mut h);
9411 keys.push(WriteKey::Unique {
9412 table_id: *table_id,
9413 index_id: uc.id | 0x8000,
9414 key_hash: h.finish(),
9415 });
9416 }
9417 }
9418 }
9419 }
9420 Staged::Delete(rid) => keys.push(WriteKey::Row {
9421 table_id: *table_id,
9422 row_id: rid.0,
9423 }),
9424 Staged::Truncate => keys.push(WriteKey::Table {
9425 table_id: *table_id,
9426 }),
9427 Staged::Update { .. } => {
9428 return Err(MongrelError::Other(
9429 "transaction contains an unnormalized update during preparation".into(),
9430 ));
9431 }
9432 }
9433 }
9434 for (external_index, (name, _)) in external_states.iter().enumerate() {
9435 commit_prepare_checkpoint(control, external_index)?;
9436 let mut h = DefaultHasher::new();
9437 name.hash(&mut h);
9438 keys.push(WriteKey::Unique {
9439 table_id: EXTERNAL_TABLE_ID,
9440 index_id: 0,
9441 key_hash: h.finish(),
9442 });
9443 }
9444 keys
9445 };
9446
9447 let min_active = self.active_txns.min_read_epoch();
9449 if min_active < u64::MAX {
9450 self.conflicts.prune_below(Epoch(min_active));
9451 }
9452
9453 if self.conflicts.conflicts(&write_keys, read_epoch) {
9457 return Err(MongrelError::Conflict(
9458 "write-write conflict (pre-validate, first-committer-wins)".into(),
9459 ));
9460 }
9461 let pre_validate_version = self.conflicts.version();
9462
9463 let mut spilled: Vec<SpilledRun> = Vec::new();
9467 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
9468 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
9472 {
9473 let mut table_bytes: HashMap<u64, u64> = HashMap::new();
9474 let mut put_indexes: HashMap<u64, Vec<usize>> = HashMap::new();
9475 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
9476 commit_prepare_checkpoint(control, staged_index)?;
9477 if let Staged::Put(cells) = staged {
9478 let bytes = cells.iter().fold(32_u64, |bytes, (_, value)| {
9479 bytes.saturating_add(value.estimated_bytes())
9480 });
9481 let table_bytes = table_bytes.entry(*table_id).or_default();
9482 *table_bytes = table_bytes.saturating_add(bytes);
9483 put_indexes.entry(*table_id).or_default().push(staged_index);
9484 }
9485 }
9486 let tables = self.tables.read();
9487 for (table_index, (&table_id, &bytes)) in table_bytes.iter().enumerate() {
9488 commit_prepare_checkpoint(control, table_index)?;
9489 if bytes
9490 <= self
9491 .spill_threshold
9492 .load(std::sync::atomic::Ordering::Relaxed)
9493 {
9494 continue;
9495 }
9496 let Some(handle) = tables.get(&table_id) else {
9497 continue;
9498 };
9499 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
9500 let mut t = handle.lock();
9501 let tdir = t.table_dir().to_path_buf();
9502 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
9503 std::fs::create_dir_all(&txn_dir)?;
9504 let run_id = t.alloc_run_id()? as u128;
9505 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
9506 let final_path = t.run_path(run_id as u64);
9507
9508 let mut rows: Vec<Row> = Vec::new();
9509 for (put_index, staged_index) in put_indexes[&table_id].iter().enumerate() {
9510 commit_prepare_checkpoint(control, put_index)?;
9511 let Staged::Put(cells) = &mut staging[*staged_index].1 else {
9512 return Err(MongrelError::Other(
9513 "transaction put index no longer references a put".into(),
9514 ));
9515 };
9516 t.validate_cells_not_null(cells)?;
9517 let row_id = t.alloc_row_id()?;
9518 let mut row = Row::new(row_id, Epoch(0));
9519 row.columns.extend(std::mem::take(cells));
9520 rows.push(row);
9521 }
9522 let schema = t.schema_ref().clone();
9523 let kek = t.kek_ref().cloned();
9524 let specs = t.indexable_column_specs();
9525 drop(t);
9526
9527 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
9528 .uniform_epoch(true);
9529 if let Some(ref kek) = kek {
9530 writer = writer.with_encryption(kek.as_ref(), specs);
9531 }
9532 commit_prepare_checkpoint(control, 0)?;
9533 let header = writer.write(&pending_path, &rows)?;
9534 commit_prepare_checkpoint(control, 0)?;
9535 let row_count = header.row_count;
9536 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
9537 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
9538
9539 spilled.push(SpilledRun {
9540 table_id,
9541 run_id,
9542 pending_path,
9543 final_path,
9544 rows,
9545 row_count,
9546 min_rid,
9547 max_rid,
9548 content_hash: header.content_hash,
9549 });
9550 spilled_tables.insert(table_id);
9551 }
9552 }
9553
9554 if spill_guard.is_some() {
9556 if let Some(hook) = self.spill_hook.lock().as_ref() {
9557 hook();
9558 }
9559 }
9560
9561 let mut prebuilt: Vec<Option<Row>> = std::iter::repeat_with(|| None)
9572 .take(staging.len())
9573 .collect();
9574 let mut delete_images: Vec<Option<Row>> = std::iter::repeat_with(|| None)
9575 .take(staging.len())
9576 .collect();
9577 {
9578 let mut indexes_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
9579 for (index, (table_id, staged)) in staging.iter().enumerate() {
9580 commit_prepare_checkpoint(control, index)?;
9581 if matches!(staged, Staged::Delete(_))
9582 || matches!(staged, Staged::Put(_) if !spilled_tables.contains(table_id))
9583 {
9584 indexes_by_table.entry(*table_id).or_default().push(index);
9585 }
9586 }
9587 let tables = self.tables.read();
9588 for (table_index, (table_id, indexes)) in indexes_by_table.into_iter().enumerate() {
9589 commit_prepare_checkpoint(control, table_index)?;
9590 let handle = tables.get(&table_id).ok_or_else(|| {
9591 MongrelError::NotFound(format!("table {table_id} not mounted"))
9592 })?;
9593 #[cfg(test)]
9594 PREBUILD_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
9595 let mut t = handle.lock();
9596 for (prepare_index, index) in indexes.into_iter().enumerate() {
9597 commit_prepare_checkpoint(control, prepare_index)?;
9598 match &staging[index].1 {
9599 Staged::Put(cells) if !spilled_tables.contains(&table_id) => {
9600 t.validate_cells_not_null(cells)?;
9601 let mut row = Row::new(t.alloc_row_id()?, Epoch(0));
9602 for (column, value) in cells {
9603 row.columns.insert(*column, value.clone());
9604 }
9605 prebuilt[index] = Some(row);
9606 }
9607 Staged::Delete(row_id) => {
9608 delete_images[index] = t.get(*row_id, Snapshot::at(read_epoch));
9609 }
9610 Staged::Put(_) | Staged::Truncate => {}
9611 Staged::Update { .. } => {
9612 return Err(MongrelError::Other(
9613 "transaction contains an unnormalized update during row preparation"
9614 .into(),
9615 ));
9616 }
9617 }
9618 }
9619 }
9620 }
9621
9622 let prepared_table_handles = {
9626 let table_ids: HashSet<u64> = staging.iter().map(|(table_id, _)| *table_id).collect();
9627 let put_table_ids: HashSet<u64> = staging
9628 .iter()
9629 .filter_map(|(table_id, staged)| {
9630 matches!(staged, Staged::Put(_)).then_some(*table_id)
9631 })
9632 .collect();
9633 let tables = self.tables.read();
9634 let mut handles = HashMap::with_capacity(table_ids.len());
9635 for (table_index, table_id) in table_ids.into_iter().enumerate() {
9636 commit_prepare_checkpoint(control, table_index)?;
9637 let handle = tables.get(&table_id).ok_or_else(|| {
9638 MongrelError::NotFound(format!("table {table_id} not mounted"))
9639 })?;
9640 if put_table_ids.contains(&table_id) {
9641 match control {
9642 Some(control) => {
9643 handle.lock().prepare_durable_publish_controlled(control)?
9644 }
9645 None => handle.lock().prepare_durable_publish()?,
9646 }
9647 }
9648 handles.insert(table_id, handle.clone());
9649 }
9650 handles
9651 };
9652
9653 let mut prepared_run_links = PreparedRunLinks::prepare(&spilled)?;
9657
9658 let mut spilled_row_ids: HashMap<u64, VecDeque<RowId>> = spilled
9659 .iter()
9660 .map(|run| {
9661 (
9662 run.table_id,
9663 run.rows.iter().map(|row| row.row_id).collect(),
9664 )
9665 })
9666 .collect();
9667 let committed_row_ids = staging
9668 .iter()
9669 .enumerate()
9670 .filter_map(|(index, (table_id, staged))| {
9671 if !matches!(staged, Staged::Put(_)) {
9672 return None;
9673 }
9674 prebuilt[index].as_ref().map(|row| row.row_id).or_else(|| {
9675 spilled_row_ids
9676 .get_mut(table_id)
9677 .and_then(VecDeque::pop_front)
9678 })
9679 })
9680 .collect();
9681
9682 let mut prepared_external = Vec::with_capacity(external_states.len());
9683 for (external_index, (name, state)) in external_states.iter().enumerate() {
9684 commit_prepare_checkpoint(control, external_index)?;
9685 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
9686 prepared_external.push((name.clone(), state.clone(), pending));
9687 }
9688
9689 let added_runs: Vec<crate::wal::AddedRun> = spilled
9691 .iter()
9692 .map(|s| crate::wal::AddedRun {
9693 table_id: s.table_id,
9694 run_id: s.run_id,
9695 row_count: s.row_count,
9696 level: 0,
9697 min_row_id: s.min_rid,
9698 max_row_id: s.max_rid,
9699 content_hash: s.content_hash,
9700 })
9701 .collect();
9702 if let Some(hook) = self.catalog_commit_hook.lock().as_ref() {
9703 hook();
9704 }
9705 let security_guard = self.security_coordinator.gate.read();
9709 if self.security_coordinator.version.load(Ordering::Acquire) != observed_security_version {
9710 return Err(MongrelError::Conflict(
9711 "security policy changed during write".into(),
9712 ));
9713 }
9714 if spill_guard.is_some() {
9715 if let Some(hook) = self.security_commit_hook.lock().as_ref() {
9716 hook();
9717 }
9718 }
9719 let commit_guard = self.commit_lock.lock();
9720 let catalog_generation_result = (|| {
9721 {
9722 let catalog = self.catalog.read();
9723 for table_id in prepared_table_handles.keys() {
9724 let is_current = catalog.tables.iter().any(|entry| {
9725 entry.table_id == *table_id
9726 && matches!(entry.state, TableState::Live | TableState::Building { .. })
9727 });
9728 if !is_current {
9729 return Err(MongrelError::Conflict(format!(
9730 "table {table_id} changed during transaction preparation"
9731 )));
9732 }
9733 }
9734 for (name, created_epoch) in &expected_external_generations {
9735 let current = catalog
9736 .external_tables
9737 .iter()
9738 .find(|entry| entry.name == *name)
9739 .map(|entry| entry.created_epoch);
9740 if current != Some(*created_epoch) {
9741 return Err(MongrelError::Conflict(format!(
9742 "external table {name:?} changed during transaction preparation"
9743 )));
9744 }
9745 }
9746 for (table_id, definition) in &prepared_materialized_views {
9747 let current = catalog.live(&definition.name).map(|entry| entry.table_id);
9748 if current != Some(*table_id) {
9749 return Err(MongrelError::Conflict(format!(
9750 "materialized view {:?} changed during transaction preparation",
9751 definition.name
9752 )));
9753 }
9754 }
9755 if trigger_catalog_binding(&catalog) != trigger_binding {
9756 return Err(MongrelError::Conflict(
9757 "trigger or referenced table generation changed during transaction preparation"
9758 .into(),
9759 ));
9760 }
9761 }
9762 let tables = self.tables.read();
9763 for (table_id, prepared) in &prepared_table_handles {
9764 if !tables
9765 .get(table_id)
9766 .is_some_and(|current| current.ptr_eq(prepared))
9767 {
9768 return Err(MongrelError::Conflict(format!(
9769 "table {table_id} mount changed during transaction preparation"
9770 )));
9771 }
9772 }
9773 Ok(())
9774 })();
9775 if let Err(error) = catalog_generation_result {
9776 drop(commit_guard);
9777 for (_, _, pending) in &prepared_external {
9778 let _ = std::fs::remove_file(pending);
9779 }
9780 return Err(error);
9781 }
9782 let new_epoch = self.epoch.assigned().next();
9786 let mut spilled_wal_bytes = 0;
9787 let mut spilled_wal_records = Vec::<(u64, Op)>::new();
9788 let spill_prepare = (|| {
9789 for run in &mut spilled {
9790 for row in &mut run.rows {
9791 row.committed_epoch = new_epoch;
9792 }
9793 for rows in encode_spilled_row_chunks(
9794 &run.rows,
9795 &mut spilled_wal_bytes,
9796 SPILLED_WAL_TOTAL_MAX_BYTES,
9797 control,
9798 )? {
9799 spilled_wal_records.push((
9800 run.table_id,
9801 Op::SpilledRows {
9802 table_id: run.table_id,
9803 rows,
9804 },
9805 ));
9806 }
9807 }
9808 Result::<()>::Ok(())
9809 })();
9810 if let Err(error) = spill_prepare {
9811 for (_, _, pending) in &prepared_external {
9812 let _ = std::fs::remove_file(pending);
9813 }
9814 return Err(error);
9815 }
9816 let (new_epoch, mut _epoch_guard, applies, committed_materialized_views, commit_seq) = {
9817 let mut wal = self.shared_wal.lock();
9818
9819 if self.conflicts.version() != pre_validate_version
9824 && self.conflicts.conflicts(&write_keys, read_epoch)
9825 {
9826 drop(wal);
9829 for (_, _, pending) in &prepared_external {
9830 let _ = std::fs::remove_file(pending);
9831 }
9832 return Err(MongrelError::Conflict(
9833 "write-write conflict (sequencer delta re-check)".into(),
9834 ));
9835 }
9836
9837 if let Some(control) = control {
9838 if let Err(error) = control.checkpoint() {
9839 drop(wal);
9840 for (_, _, pending) in &prepared_external {
9841 let _ = std::fs::remove_file(pending);
9842 }
9843 return Err(error);
9844 }
9845 }
9846 let mut applies = Vec::<TableApplyBatch>::new();
9847 let mut apply_indexes = HashMap::<u64, usize>::new();
9848 let mut committed_materialized_views = Vec::new();
9849 let mut wal_records = spilled_wal_records;
9850
9851 let mut index = 0;
9852 while index < staging.len() {
9853 let table_id = staging[index].0;
9854 let handle = prepared_table_handles
9855 .get(&table_id)
9856 .cloned()
9857 .ok_or_else(|| {
9858 MongrelError::NotFound(format!("table {table_id} not prepared"))
9859 })?;
9860 let batch_index = *apply_indexes.entry(table_id).or_insert_with(|| {
9861 let index = applies.len();
9862 applies.push(TableApplyBatch {
9863 table_id,
9864 handle,
9865 ops: Vec::new(),
9866 });
9867 index
9868 });
9869
9870 if spilled_tables.contains(&table_id) && matches!(&staging[index].1, Staged::Put(_))
9873 {
9874 index += 1;
9875 continue;
9876 }
9877
9878 match &staging[index].1 {
9879 Staged::Put(_) => {
9880 let mut rows = Vec::new();
9881 while index < staging.len()
9882 && staging[index].0 == table_id
9883 && matches!(&staging[index].1, Staged::Put(_))
9884 {
9885 let mut row = prebuilt[index].take().ok_or_else(|| {
9886 MongrelError::Other(
9887 "transaction prepare lost a prebuilt put row".into(),
9888 )
9889 })?;
9890 row.committed_epoch = new_epoch;
9891 rows.push(row);
9892 index += 1;
9893 }
9894 let payload = bincode::serialize(&rows)
9895 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
9896 wal_records.push((
9897 table_id,
9898 Op::Put {
9899 table_id,
9900 rows: payload,
9901 },
9902 ));
9903 applies[batch_index].ops.push(StagedOp::Put(rows));
9904 }
9905 Staged::Delete(_) => {
9906 let mut row_ids = Vec::new();
9907 while index < staging.len()
9908 && staging[index].0 == table_id
9909 && matches!(&staging[index].1, Staged::Delete(_))
9910 {
9911 let Staged::Delete(row_id) = &staging[index].1 else {
9912 return Err(MongrelError::Other(
9913 "transaction delete batch changed during WAL preparation"
9914 .into(),
9915 ));
9916 };
9917 if let Some(before) = &delete_images[index] {
9918 wal_records.push((
9919 table_id,
9920 Op::BeforeImage {
9921 table_id,
9922 row_id: *row_id,
9923 row: bincode::serialize(before).map_err(|error| {
9924 MongrelError::Other(format!(
9925 "before-image serialize: {error}"
9926 ))
9927 })?,
9928 },
9929 ));
9930 }
9931 row_ids.push(*row_id);
9932 index += 1;
9933 }
9934 wal_records.push((
9935 table_id,
9936 Op::Delete {
9937 table_id,
9938 row_ids: row_ids.clone(),
9939 },
9940 ));
9941 applies[batch_index].ops.push(StagedOp::Delete(row_ids));
9942 }
9943 Staged::Truncate => {
9944 wal_records.push((table_id, Op::TruncateTable { table_id }));
9945 applies[batch_index].ops.push(StagedOp::Truncate);
9946 index += 1;
9947 }
9948 Staged::Update { .. } => {
9949 return Err(MongrelError::Other(
9950 "transaction contains an unnormalized update at the sequencer".into(),
9951 ));
9952 }
9953 }
9954 }
9955
9956 for (name, state, _) in &prepared_external {
9957 wal_records.push((
9958 EXTERNAL_TABLE_ID,
9959 Op::ExternalTableState {
9960 name: name.clone(),
9961 state: state.clone(),
9962 },
9963 ));
9964 }
9965
9966 for (table_id, definition) in &prepared_materialized_views {
9967 let mut definition = definition.clone();
9968 definition.last_refresh_epoch = new_epoch.0;
9969 wal_records.push((
9970 *table_id,
9971 Op::Ddl(crate::wal::DdlOp::SetMaterializedView {
9972 name: definition.name.clone(),
9973 definition_json: crate::wal::DdlOp::encode_materialized_view(&definition)?,
9974 }),
9975 ));
9976 committed_materialized_views.push(definition);
9977 }
9978 if !committed_materialized_views.is_empty() {
9979 let mut next_catalog = self.catalog.read().clone();
9980 for definition in &committed_materialized_views {
9981 if let Some(existing) = next_catalog
9982 .materialized_views
9983 .iter_mut()
9984 .find(|existing| existing.name == definition.name)
9985 {
9986 *existing = definition.clone();
9987 } else {
9988 next_catalog.materialized_views.push(definition.clone());
9989 }
9990 }
9991 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
9992 wal_records.push((
9993 WAL_TABLE_ID,
9994 Op::Ddl(crate::wal::DdlOp::CatalogSnapshot {
9995 catalog_json: crate::wal::DdlOp::encode_catalog(&next_catalog)?,
9996 }),
9997 ));
9998 }
9999
10000 if let Some(control) = control {
10001 if let Err(error) = control.checkpoint() {
10002 drop(wal);
10003 for (_, _, pending) in &prepared_external {
10004 let _ = std::fs::remove_file(pending);
10005 }
10006 return Err(error);
10007 }
10008 }
10009 if let Some(before_commit) = before_commit.as_mut() {
10010 if let Err(error) = before_commit() {
10011 drop(wal);
10012 for (_, _, pending) in &prepared_external {
10013 let _ = std::fs::remove_file(pending);
10014 }
10015 return Err(error);
10016 }
10017 }
10018
10019 let assigned_epoch = self.epoch.bump_assigned();
10020 let _epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
10021 if assigned_epoch != new_epoch {
10022 for (_, _, pending) in &prepared_external {
10023 let _ = std::fs::remove_file(pending);
10024 }
10025 return Err(MongrelError::Conflict(
10026 "commit epoch changed while sequencer lock was held".into(),
10027 ));
10028 }
10029
10030 prepared_run_links.disarm();
10034
10035 let append: Result<u64> = (|| {
10036 for (table_id, op) in wal_records {
10037 wal.append(txn_id, table_id, op)?;
10038 }
10039 wal.append_commit(txn_id, new_epoch, &added_runs)
10040 })();
10041 let commit_seq =
10042 append.map_err(|error| self.commit_outcome_unknown(new_epoch, error))?;
10043
10044 self.conflicts.record(&write_keys, new_epoch);
10049 (
10050 new_epoch,
10051 _epoch_guard,
10052 applies,
10053 committed_materialized_views,
10054 commit_seq,
10055 )
10056 };
10057 drop(commit_guard);
10058
10059 self.await_durable_commit(commit_seq, new_epoch)?;
10061 drop(security_guard);
10062
10063 let publish_result: Result<()> = {
10065 let mut first_error = None;
10066 let mut spilled_by_table: HashMap<u64, Vec<&SpilledRun>> = HashMap::new();
10067 for run in &spilled {
10068 spilled_by_table.entry(run.table_id).or_default().push(run);
10069 }
10070 let mut modified_tables = Vec::with_capacity(applies.len());
10071 for batch in applies {
10074 #[cfg(test)]
10075 PUBLISH_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
10076 let mut t = batch.handle.lock();
10077 for op in batch.ops {
10078 match op {
10079 StagedOp::Put(rows) => t.apply_put_rows_prepared(rows),
10080 StagedOp::Delete(row_ids) => {
10081 for row_id in row_ids {
10082 t.apply_delete(row_id, new_epoch);
10083 }
10084 }
10085 StagedOp::Truncate => t.apply_truncate(new_epoch),
10086 }
10087 }
10088 if let Some(runs) = spilled_by_table.remove(&batch.table_id) {
10089 for run in runs {
10090 t.link_run(crate::manifest::RunRef {
10091 run_id: run.run_id,
10092 level: 0,
10093 epoch_created: new_epoch.0,
10094 row_count: run.row_count,
10095 });
10096 t.apply_run_metadata_prepared(&run.rows)?;
10097 if truncated_tables.contains(&batch.table_id) {
10098 t.set_flushed_epoch(new_epoch);
10103 }
10104 }
10105 }
10106 t.invalidate_pending_cache();
10107 drop(t);
10108 modified_tables.push(batch.handle);
10109 }
10110
10111 for handle in modified_tables {
10115 #[cfg(test)]
10116 COMMIT_MANIFEST_WRITES.with(|count| count.set(count.get() + 1));
10117 if let Err(error) = handle.lock().persist_manifest(new_epoch) {
10118 first_error.get_or_insert(error);
10119 }
10120 }
10121 for (name, _, pending) in &prepared_external {
10122 if let Err(error) = publish_external_state_file(&self.root, name, pending) {
10123 first_error.get_or_insert(error);
10124 }
10125 }
10126 if !committed_materialized_views.is_empty() {
10127 let mut next_catalog = self.catalog.read().clone();
10128 for definition in committed_materialized_views {
10129 if let Some(existing) = next_catalog
10130 .materialized_views
10131 .iter_mut()
10132 .find(|existing| existing.name == definition.name)
10133 {
10134 *existing = definition;
10135 } else {
10136 next_catalog.materialized_views.push(definition);
10137 }
10138 }
10139 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
10140 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
10141 first_error.get_or_insert(error);
10142 }
10143 }
10144 match first_error {
10145 Some(error) => Err(error),
10146 None => Ok(()),
10147 }
10148 };
10149
10150 if has_changes {
10151 let _ = self.change_wake.send(());
10152 }
10153 self.finish_durable_publish(new_epoch, &mut _epoch_guard, publish_result)?;
10154 Ok((new_epoch, committed_row_ids))
10155 }
10156
10157 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
10160 let e = self.epoch.visible();
10161 let g = self.snapshots.register(e);
10162 (Snapshot::at(e), g)
10163 }
10164
10165 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
10168 let e = self.epoch.visible();
10169 let g = self.snapshots.register_owned(e);
10170 (Snapshot::at(e), g)
10171 }
10172
10173 pub fn set_history_retention_epochs(&self, epochs: u64) -> Result<()> {
10178 let _guard = self.ddl_lock.lock();
10179 let current = self.epoch.visible();
10180 let (old_epochs, old_start) = self.snapshots.history_config();
10181 let earliest_already_guaranteed = if old_epochs == 0 {
10182 current
10183 } else {
10184 Epoch(old_start.0.max(current.0.saturating_sub(old_epochs)))
10185 };
10186 let start = if epochs == 0 {
10187 current
10188 } else {
10189 earliest_already_guaranteed
10190 };
10191 let published = std::cell::Cell::new(false);
10192 let result = write_history_retention(&self.root, epochs, start, || {
10193 self.snapshots.configure_history(epochs, start);
10194 published.set(true);
10195 });
10196 match result {
10197 Err(error) if published.get() => Err(MongrelError::CommitOutcomeUnknown {
10198 epoch: current.0,
10199 message: format!("history-retention publication was not durable: {error}"),
10200 }),
10201 result => result,
10202 }
10203 }
10204
10205 pub fn history_retention_epochs(&self) -> u64 {
10206 self.snapshots.history_config().0
10207 }
10208
10209 pub fn earliest_retained_epoch(&self) -> Epoch {
10210 let current = self.epoch.visible();
10211 self.snapshots.history_floor(current).unwrap_or(current)
10212 }
10213
10214 pub fn snapshot_at_owned(&self, epoch: Epoch) -> Result<(Snapshot, OwnedSnapshotGuard)> {
10217 let current = self.epoch.visible();
10218 if epoch > current {
10219 return Err(MongrelError::InvalidArgument(format!(
10220 "epoch {} is in the future; current epoch is {}",
10221 epoch.0, current.0
10222 )));
10223 }
10224 let earliest = self.earliest_retained_epoch();
10225 if epoch < earliest {
10226 return Err(MongrelError::InvalidArgument(format!(
10227 "epoch {} is no longer retained; earliest available epoch is {}",
10228 epoch.0, earliest.0
10229 )));
10230 }
10231 let guard = self.snapshots.register_owned(epoch);
10232 Ok((Snapshot::at(epoch), guard))
10233 }
10234
10235 pub fn table_names(&self) -> Vec<String> {
10237 self.catalog
10238 .read()
10239 .tables
10240 .iter()
10241 .filter(|t| matches!(t.state, TableState::Live))
10242 .map(|t| t.name.clone())
10243 .collect()
10244 }
10245
10246 pub fn close(&self) -> Result<()> {
10252 for name in self.table_names() {
10253 if let Ok(handle) = self.table(&name) {
10254 if let Err(e) = handle.lock().close() {
10255 eprintln!("[close] flush failed for {name}: {e}");
10256 }
10257 }
10258 }
10259 Ok(())
10260 }
10261
10262 pub fn compact(&self) -> Result<(usize, usize)> {
10269 self.require(&crate::auth::Permission::Ddl)?;
10270 let mut compacted = 0;
10271 let mut skipped = 0;
10272 for name in self.table_names() {
10273 let Ok(handle) = self.table(&name) else {
10274 continue;
10275 };
10276 {
10277 let mut t = handle.lock();
10278 let before = t.run_count();
10279 if before < 2 && !t.should_compact() {
10280 skipped += 1;
10281 continue;
10282 }
10283 match t.compact() {
10284 Ok(()) => {
10285 let after = t.run_count();
10286 compacted += 1;
10287 eprintln!("[compact] {name}: {before} -> {after} runs");
10288 }
10289 Err(e) => {
10290 eprintln!("[compact] {name}: compaction failed: {e}");
10291 skipped += 1;
10292 }
10293 }
10294 }
10295 }
10296 Ok((compacted, skipped))
10297 }
10298
10299 pub fn compact_table(&self, name: &str) -> Result<bool> {
10302 self.require(&crate::auth::Permission::Ddl)?;
10303 let handle = self.table(name)?;
10304 let mut t = handle.lock();
10305 let before = t.run_count();
10306 if before < 2 {
10307 return Ok(false);
10308 }
10309 t.compact()?;
10310 Ok(t.run_count() < before)
10311 }
10312
10313 pub fn table(&self, name: &str) -> Result<TableHandle> {
10315 let cat = self.catalog.read();
10316 let entry = cat
10317 .live(name)
10318 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
10319 let id = entry.table_id;
10320 drop(cat);
10321 self.tables
10322 .read()
10323 .get(&id)
10324 .cloned()
10325 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
10326 }
10327
10328 pub fn has_ttl_tables(&self) -> bool {
10331 self.tables
10332 .read()
10333 .values()
10334 .any(|table| table.lock().ttl().is_some())
10335 }
10336
10337 pub(crate) fn table_by_id(&self, id: u64) -> Result<TableHandle> {
10340 self.tables
10341 .read()
10342 .get(&id)
10343 .cloned()
10344 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
10345 }
10346
10347 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
10353 if name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
10354 return Err(MongrelError::InvalidArgument(format!(
10355 "table names beginning with {CTAS_BUILD_TABLE_PREFIX:?} are reserved"
10356 )));
10357 }
10358 self.create_table_with_state(name, schema, TableState::Live)
10359 }
10360
10361 #[doc(hidden)]
10363 pub fn create_building_table(
10364 &self,
10365 build_name: &str,
10366 intended_name: &str,
10367 query_id: &str,
10368 schema: Schema,
10369 ) -> Result<u64> {
10370 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10371 || intended_name.is_empty()
10372 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10373 || query_id.is_empty()
10374 {
10375 return Err(MongrelError::InvalidArgument(
10376 "invalid CTAS building-table identity".into(),
10377 ));
10378 }
10379 self.create_table_with_state(
10380 build_name,
10381 schema,
10382 TableState::Building {
10383 intended_name: intended_name.to_string(),
10384 query_id: query_id.to_string(),
10385 created_at_unix_nanos: current_unix_nanos(),
10386 replaces_table_id: None,
10387 },
10388 )
10389 }
10390
10391 #[doc(hidden)]
10394 pub fn create_rebuilding_table(
10395 &self,
10396 build_name: &str,
10397 intended_name: &str,
10398 query_id: &str,
10399 schema: Schema,
10400 ) -> Result<u64> {
10401 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10402 || intended_name.is_empty()
10403 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10404 || query_id.is_empty()
10405 {
10406 return Err(MongrelError::InvalidArgument(
10407 "invalid rebuilding-table identity".into(),
10408 ));
10409 }
10410 let replaces_table_id = self
10411 .catalog
10412 .read()
10413 .live(intended_name)
10414 .ok_or_else(|| MongrelError::NotFound(format!("table {intended_name:?} not found")))?
10415 .table_id;
10416 self.create_table_with_state(
10417 build_name,
10418 schema,
10419 TableState::Building {
10420 intended_name: intended_name.to_string(),
10421 query_id: query_id.to_string(),
10422 created_at_unix_nanos: current_unix_nanos(),
10423 replaces_table_id: Some(replaces_table_id),
10424 },
10425 )
10426 }
10427
10428 fn create_table_with_state(
10429 &self,
10430 name: &str,
10431 schema: Schema,
10432 state: TableState,
10433 ) -> Result<u64> {
10434 use crate::wal::DdlOp;
10435 use std::sync::atomic::Ordering;
10436
10437 self.require(&crate::auth::Permission::Ddl)?;
10438 if self.poisoned.load(Ordering::Relaxed) {
10439 return Err(MongrelError::Other(
10440 "database poisoned by fsync error".into(),
10441 ));
10442 }
10443
10444 let _g = self.ddl_lock.lock();
10445 let _security_write = self.security_write()?;
10446 self.require(&crate::auth::Permission::Ddl)?;
10447 {
10448 let cat = self.catalog.read();
10449 match &state {
10450 TableState::Live => {
10451 if cat.live(name).is_some() || cat.building_for(name).is_some() {
10452 return Err(MongrelError::InvalidArgument(format!(
10453 "table {name:?} already exists or is being built"
10454 )));
10455 }
10456 }
10457 TableState::Building {
10458 intended_name,
10459 replaces_table_id,
10460 ..
10461 } => {
10462 let target_matches = match replaces_table_id {
10463 Some(table_id) => cat
10464 .live(intended_name)
10465 .is_some_and(|entry| entry.table_id == *table_id),
10466 None => cat.live(intended_name).is_none(),
10467 };
10468 if !target_matches || cat.building_for(intended_name).is_some() {
10469 return Err(MongrelError::InvalidArgument(format!(
10470 "table {intended_name:?} changed or is already being built"
10471 )));
10472 }
10473 if cat.building(name).is_some() {
10474 return Err(MongrelError::InvalidArgument(format!(
10475 "building table {name:?} already exists"
10476 )));
10477 }
10478 }
10479 TableState::Dropped { .. } => {
10480 return Err(MongrelError::InvalidArgument(
10481 "cannot create a dropped table".into(),
10482 ));
10483 }
10484 }
10485 }
10486
10487 let commit_lock = Arc::clone(&self.commit_lock);
10490 let _c = commit_lock.lock();
10491 let table_id = {
10492 let mut cat = self.catalog.write();
10493 let id = cat.next_table_id;
10494 cat.next_table_id = id
10495 .checked_add(1)
10496 .ok_or_else(|| MongrelError::InvalidArgument("table id space exhausted".into()))?;
10497 Result::<u64>::Ok(id)
10498 }?;
10499 let epoch = self.epoch.bump_assigned();
10500 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
10501 let txn_id = self.alloc_txn_id()?;
10502
10503 let mut schema = schema;
10507 schema.schema_id = table_id;
10508 schema.validate_auto_increment()?;
10515 schema.validate_defaults()?;
10516 schema.validate_ai()?;
10517 for index in &schema.indexes {
10518 index.validate_options()?;
10519 }
10520 for constraint in &schema.constraints.checks {
10521 constraint.expr.validate()?;
10522 }
10523
10524 let table_relative = Path::new(TABLES_DIR).join(table_id.to_string());
10527 let canonical_tdir = self.root.join(&table_relative);
10528 let table_root = Arc::new(
10529 self.durable_root
10530 .create_directory_all_pinned(&table_relative)?,
10531 );
10532 let tdir = table_root.io_path()?;
10533 let mut pending_table_dir = PendingTableDir::new(canonical_tdir);
10534 let ctx = SharedCtx {
10535 root_guard: Some(table_root),
10536 epoch: Arc::clone(&self.epoch),
10537 page_cache: Arc::clone(&self.page_cache),
10538 decoded_cache: Arc::clone(&self.decoded_cache),
10539 snapshots: Arc::clone(&self.snapshots),
10540 kek: self.kek.clone(),
10541 commit_lock: Arc::clone(&self.commit_lock),
10542 shared: Some(crate::engine::SharedWalCtx {
10543 wal: Arc::clone(&self.shared_wal),
10544 group: Arc::clone(&self.group),
10545 poisoned: Arc::clone(&self.poisoned),
10546 txn_ids: Arc::clone(&self.next_txn_id),
10547 change_wake: self.change_wake.clone(),
10548 }),
10549 table_name: Some(name.to_string()),
10550 auth: self.table_auth_checker(),
10551 read_only: self.read_only,
10552 };
10553 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
10554
10555 let schema_json = DdlOp::encode_schema(&schema)?;
10558 let ddl = match &state {
10559 TableState::Live => DdlOp::CreateTable {
10560 table_id,
10561 name: name.to_string(),
10562 schema_json,
10563 },
10564 TableState::Building {
10565 intended_name,
10566 query_id,
10567 created_at_unix_nanos,
10568 replaces_table_id,
10569 } => match replaces_table_id {
10570 Some(replaces_table_id) => DdlOp::CreateRebuildingTable {
10571 table_id,
10572 build_name: name.to_string(),
10573 intended_name: intended_name.clone(),
10574 query_id: query_id.clone(),
10575 created_at_unix_nanos: *created_at_unix_nanos,
10576 replaces_table_id: *replaces_table_id,
10577 schema_json,
10578 },
10579 None => DdlOp::CreateBuildingTable {
10580 table_id,
10581 build_name: name.to_string(),
10582 intended_name: intended_name.clone(),
10583 query_id: query_id.clone(),
10584 created_at_unix_nanos: *created_at_unix_nanos,
10585 schema_json,
10586 },
10587 },
10588 TableState::Dropped { .. } => {
10589 return Err(MongrelError::InvalidArgument(
10590 "cannot create a table in dropped state".into(),
10591 ));
10592 }
10593 };
10594 let mut next_catalog = self.catalog.read().clone();
10595 next_catalog.tables.push(CatalogEntry {
10596 table_id,
10597 name: name.to_string(),
10598 schema: schema.clone(),
10599 state: state.clone(),
10600 created_epoch: epoch.0,
10601 });
10602 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
10603 let commit_seq = {
10604 let mut wal = self.shared_wal.lock();
10605 let append: Result<u64> = (|| {
10606 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
10607 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
10608 wal.append_commit(txn_id, epoch, &[])
10609 })();
10610 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
10611 };
10612 self.await_durable_commit(commit_seq, epoch)?;
10613 pending_table_dir.disarm();
10614
10615 self.tables
10618 .write()
10619 .insert(table_id, TableHandle::new(table));
10620 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
10621 self.finish_durable_publish(epoch, &mut _epoch_guard, checkpoint)?;
10622 Ok(table_id)
10623 }
10624
10625 pub fn drop_table(&self, name: &str) -> Result<()> {
10627 self.drop_table_with_epoch(name).map(|_| ())
10628 }
10629
10630 pub fn drop_table_with_epoch(&self, name: &str) -> Result<Epoch> {
10632 self.drop_table_with_state(name, false, None)
10633 }
10634
10635 pub fn drop_table_with_epoch_controlled<F>(
10636 &self,
10637 name: &str,
10638 mut before_commit: F,
10639 ) -> Result<Epoch>
10640 where
10641 F: FnMut() -> Result<()>,
10642 {
10643 self.drop_table_with_state(name, false, Some(&mut before_commit))
10644 }
10645
10646 #[doc(hidden)]
10648 pub fn discard_building_table(&self, name: &str) -> Result<()> {
10649 if !name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
10650 return Err(MongrelError::InvalidArgument(
10651 "not a CTAS building table".into(),
10652 ));
10653 }
10654 self.drop_table_with_state(name, true, None).map(|_| ())
10655 }
10656
10657 fn drop_table_with_state(
10658 &self,
10659 name: &str,
10660 building: bool,
10661 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10662 ) -> Result<Epoch> {
10663 use crate::wal::DdlOp;
10664 use std::sync::atomic::Ordering;
10665
10666 self.require(&crate::auth::Permission::Ddl)?;
10667 if self.poisoned.load(Ordering::Relaxed) {
10668 return Err(MongrelError::Other(
10669 "database poisoned by fsync error".into(),
10670 ));
10671 }
10672
10673 let _g = self.ddl_lock.lock();
10674 let _security_write = self.security_write()?;
10675 self.require(&crate::auth::Permission::Ddl)?;
10676 let table_id = {
10677 let cat = self.catalog.read();
10678 if building {
10679 cat.building(name)
10680 } else {
10681 cat.live(name)
10682 }
10683 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
10684 .table_id
10685 };
10686
10687 let commit_lock = Arc::clone(&self.commit_lock);
10688 let _c = commit_lock.lock();
10689 let epoch = self.epoch.bump_assigned();
10690 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
10691 let txn_id = self.alloc_txn_id()?;
10692 let mut next_catalog = self.catalog.read().clone();
10693 let entry = next_catalog
10694 .tables
10695 .iter_mut()
10696 .find(|t| t.table_id == table_id)
10697 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
10698 entry.state = TableState::Dropped { at_epoch: epoch.0 };
10699 next_catalog.triggers.retain(|trigger| {
10700 !matches!(
10701 &trigger.trigger.target,
10702 TriggerTarget::Table(target) if target == name
10703 )
10704 });
10705 next_catalog
10706 .materialized_views
10707 .retain(|definition| definition.name != name);
10708 next_catalog
10709 .security
10710 .rls_tables
10711 .retain(|table| table != name);
10712 next_catalog
10713 .security
10714 .policies
10715 .retain(|policy| policy.table != name);
10716 next_catalog
10717 .security
10718 .masks
10719 .retain(|mask| mask.table != name);
10720 for role in &mut next_catalog.roles {
10721 role.permissions
10722 .retain(|permission| permission_table(permission) != Some(name));
10723 }
10724 advance_security_version(&mut next_catalog)?;
10725 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
10726 let commit_seq = {
10727 let mut wal = self.shared_wal.lock();
10728 if let Some(before_commit) = before_commit {
10729 before_commit()?;
10730 }
10731 let append: Result<u64> = (|| {
10732 wal.append(
10733 txn_id,
10734 table_id,
10735 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
10736 )?;
10737 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
10738 wal.append_commit(txn_id, epoch, &[])
10739 })();
10740 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
10741 };
10742 self.await_durable_commit(commit_seq, epoch)?;
10743
10744 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
10745 self.tables.write().remove(&table_id);
10746 self.finish_durable_publish(epoch, &mut _epoch_guard, checkpoint)?;
10747 Ok(epoch)
10748 }
10749
10750 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
10759 self.rename_table_with_epoch(name, new_name).map(|_| ())
10760 }
10761
10762 pub fn rename_table_with_epoch(&self, name: &str, new_name: &str) -> Result<Epoch> {
10764 self.rename_table_with_epoch_inner(name, new_name, None)
10765 }
10766
10767 pub fn rename_table_with_epoch_controlled<F>(
10768 &self,
10769 name: &str,
10770 new_name: &str,
10771 mut before_commit: F,
10772 ) -> Result<Epoch>
10773 where
10774 F: FnMut() -> Result<()>,
10775 {
10776 self.rename_table_with_epoch_inner(name, new_name, Some(&mut before_commit))
10777 }
10778
10779 fn rename_table_with_epoch_inner(
10780 &self,
10781 name: &str,
10782 new_name: &str,
10783 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10784 ) -> Result<Epoch> {
10785 if name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10786 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10787 {
10788 return Err(MongrelError::InvalidArgument(
10789 "the CTAS building-table namespace is reserved".into(),
10790 ));
10791 }
10792 self.rename_table_with_state(name, new_name, false, None, before_commit)
10793 }
10794
10795 #[doc(hidden)]
10797 pub fn publish_building_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
10798 self.publish_building_table_inner(build_name, new_name, None)
10799 }
10800
10801 #[doc(hidden)]
10802 pub fn publish_building_table_controlled<F>(
10803 &self,
10804 build_name: &str,
10805 new_name: &str,
10806 mut before_commit: F,
10807 ) -> Result<Epoch>
10808 where
10809 F: FnMut() -> Result<()>,
10810 {
10811 self.publish_building_table_inner(build_name, new_name, Some(&mut before_commit))
10812 }
10813
10814 fn publish_building_table_inner(
10815 &self,
10816 build_name: &str,
10817 new_name: &str,
10818 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10819 ) -> Result<Epoch> {
10820 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10821 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10822 {
10823 return Err(MongrelError::InvalidArgument(
10824 "invalid CTAS publish identity".into(),
10825 ));
10826 }
10827 self.rename_table_with_state(build_name, new_name, true, None, before_commit)
10828 }
10829
10830 #[doc(hidden)]
10832 pub fn publish_materialized_building_table(
10833 &self,
10834 build_name: &str,
10835 new_name: &str,
10836 definition: crate::catalog::MaterializedViewEntry,
10837 ) -> Result<Epoch> {
10838 self.publish_materialized_building_table_inner(build_name, new_name, definition, None)
10839 }
10840
10841 #[doc(hidden)]
10842 pub fn publish_materialized_building_table_controlled<F>(
10843 &self,
10844 build_name: &str,
10845 new_name: &str,
10846 definition: crate::catalog::MaterializedViewEntry,
10847 mut before_commit: F,
10848 ) -> Result<Epoch>
10849 where
10850 F: FnMut() -> Result<()>,
10851 {
10852 self.publish_materialized_building_table_inner(
10853 build_name,
10854 new_name,
10855 definition,
10856 Some(&mut before_commit),
10857 )
10858 }
10859
10860 fn publish_materialized_building_table_inner(
10861 &self,
10862 build_name: &str,
10863 new_name: &str,
10864 definition: crate::catalog::MaterializedViewEntry,
10865 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10866 ) -> Result<Epoch> {
10867 if definition.name != new_name || definition.query.trim().is_empty() {
10868 return Err(MongrelError::InvalidArgument(
10869 "invalid materialized-view publication".into(),
10870 ));
10871 }
10872 self.rename_table_with_state(build_name, new_name, true, Some(definition), before_commit)
10873 }
10874
10875 #[doc(hidden)]
10877 pub fn publish_rebuilding_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
10878 self.publish_rebuilding_table_inner(build_name, new_name, None, None)
10879 }
10880
10881 #[doc(hidden)]
10882 pub fn publish_rebuilding_table_controlled<F>(
10883 &self,
10884 build_name: &str,
10885 new_name: &str,
10886 mut before_commit: F,
10887 ) -> Result<Epoch>
10888 where
10889 F: FnMut() -> Result<()>,
10890 {
10891 self.publish_rebuilding_table_inner(build_name, new_name, None, Some(&mut before_commit))
10892 }
10893
10894 #[doc(hidden)]
10896 pub fn publish_materialized_rebuilding_table_controlled<F>(
10897 &self,
10898 build_name: &str,
10899 new_name: &str,
10900 definition: crate::catalog::MaterializedViewEntry,
10901 mut before_commit: F,
10902 ) -> Result<Epoch>
10903 where
10904 F: FnMut() -> Result<()>,
10905 {
10906 self.publish_rebuilding_table_inner(
10907 build_name,
10908 new_name,
10909 Some(definition),
10910 Some(&mut before_commit),
10911 )
10912 }
10913
10914 fn publish_rebuilding_table_inner(
10915 &self,
10916 build_name: &str,
10917 new_name: &str,
10918 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
10919 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10920 ) -> Result<Epoch> {
10921 use crate::wal::DdlOp;
10922
10923 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10924 || new_name.is_empty()
10925 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10926 {
10927 return Err(MongrelError::InvalidArgument(
10928 "invalid rebuilding-table publish identity".into(),
10929 ));
10930 }
10931 if materialized_view.as_ref().is_some_and(|definition| {
10932 definition.name != new_name || definition.query.trim().is_empty()
10933 }) {
10934 return Err(MongrelError::InvalidArgument(
10935 "invalid materialized-view replacement".into(),
10936 ));
10937 }
10938 self.require(&crate::auth::Permission::Ddl)?;
10939 if self.poisoned.load(Ordering::Relaxed) {
10940 return Err(MongrelError::Other(
10941 "database poisoned by fsync error".into(),
10942 ));
10943 }
10944
10945 let _ddl = self.ddl_lock.lock();
10946 let _security_write = self.security_write()?;
10947 let (table_id, replaced_table_id) = {
10948 let catalog = self.catalog.read();
10949 let build = catalog.building(build_name).ok_or_else(|| {
10950 MongrelError::NotFound(format!("building table {build_name:?} not found"))
10951 })?;
10952 let replaced_table_id = match &build.state {
10953 TableState::Building {
10954 intended_name,
10955 replaces_table_id: Some(replaced_table_id),
10956 ..
10957 } if intended_name == new_name => *replaced_table_id,
10958 _ => {
10959 return Err(MongrelError::InvalidArgument(format!(
10960 "building table {build_name:?} is not a replacement for {new_name:?}"
10961 )))
10962 }
10963 };
10964 if catalog
10965 .live(new_name)
10966 .is_none_or(|entry| entry.table_id != replaced_table_id)
10967 {
10968 return Err(MongrelError::Conflict(format!(
10969 "table {new_name:?} changed while its replacement was built"
10970 )));
10971 }
10972 (build.table_id, replaced_table_id)
10973 };
10974
10975 let _commit = self.commit_lock.lock();
10976 let epoch = self.epoch.assigned().next();
10977 let txn_id = self.alloc_txn_id()?;
10978 let mut next_catalog = self.catalog.read().clone();
10979 apply_rebuilding_publish(
10980 &mut next_catalog,
10981 table_id,
10982 replaced_table_id,
10983 new_name,
10984 epoch.0,
10985 )?;
10986 if let Some(definition) = materialized_view.as_mut() {
10987 definition.last_refresh_epoch = epoch.0;
10988 }
10989 let materialized_view_json = materialized_view
10990 .as_ref()
10991 .map(DdlOp::encode_materialized_view)
10992 .transpose()?;
10993 if let Some(definition) = materialized_view {
10994 if let Some(existing) = next_catalog
10995 .materialized_views
10996 .iter_mut()
10997 .find(|existing| existing.name == definition.name)
10998 {
10999 *existing = definition;
11000 } else {
11001 next_catalog.materialized_views.push(definition);
11002 }
11003 }
11004 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11005 if let Some(before_commit) = before_commit {
11006 before_commit()?;
11007 }
11008 let assigned_epoch = self.epoch.bump_assigned();
11009 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
11010 if assigned_epoch != epoch {
11011 return Err(MongrelError::Conflict(
11012 "commit epoch changed while sequencer lock was held".into(),
11013 ));
11014 }
11015 let commit_seq = {
11016 let mut wal = self.shared_wal.lock();
11017 let append: Result<u64> = (|| {
11018 wal.append(
11019 txn_id,
11020 table_id,
11021 crate::wal::Op::Ddl(DdlOp::ReplaceBuildingTable {
11022 table_id,
11023 replaced_table_id,
11024 new_name: new_name.to_string(),
11025 }),
11026 )?;
11027 if let Some(definition_json) = materialized_view_json {
11028 wal.append(
11029 txn_id,
11030 table_id,
11031 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
11032 name: new_name.to_string(),
11033 definition_json,
11034 }),
11035 )?;
11036 }
11037 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11038 wal.append_commit(txn_id, epoch, &[])
11039 })();
11040 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11041 };
11042 self.await_durable_commit(commit_seq, epoch)?;
11043
11044 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
11045 self.tables.write().remove(&replaced_table_id);
11046 if let Some(table) = self.tables.read().get(&table_id) {
11047 table.lock().set_catalog_name(new_name.to_string());
11048 }
11049 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
11050 Ok(epoch)
11051 }
11052
11053 fn rename_table_with_state(
11054 &self,
11055 name: &str,
11056 new_name: &str,
11057 building: bool,
11058 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
11059 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11060 ) -> Result<Epoch> {
11061 use crate::wal::DdlOp;
11062 use std::sync::atomic::Ordering;
11063
11064 self.require(&crate::auth::Permission::Ddl)?;
11065 if self.poisoned.load(Ordering::Relaxed) {
11066 return Err(MongrelError::Other(
11067 "database poisoned by fsync error".into(),
11068 ));
11069 }
11070
11071 if name == new_name {
11074 return Ok(self.visible_epoch());
11075 }
11076 if new_name.is_empty() {
11077 return Err(MongrelError::InvalidArgument(
11078 "rename_table: new name must not be empty".into(),
11079 ));
11080 }
11081
11082 let _g = self.ddl_lock.lock();
11083 let _security_write = self.security_write()?;
11084 self.require(&crate::auth::Permission::Ddl)?;
11085 let table_id = {
11086 let cat = self.catalog.read();
11087 let src = if building {
11088 cat.building(name)
11089 } else {
11090 cat.live(name)
11091 }
11092 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
11093 if building
11094 && !matches!(
11095 &src.state,
11096 TableState::Building { intended_name, .. } if intended_name == new_name
11097 )
11098 {
11099 return Err(MongrelError::InvalidArgument(format!(
11100 "building table {name:?} is not reserved for {new_name:?}"
11101 )));
11102 }
11103 if cat.live(new_name).is_some() || (!building && cat.building_for(new_name).is_some()) {
11107 return Err(MongrelError::InvalidArgument(format!(
11108 "rename_table: a table named {new_name:?} already exists"
11109 )));
11110 }
11111 src.table_id
11112 };
11113
11114 let commit_lock = Arc::clone(&self.commit_lock);
11115 let _c = commit_lock.lock();
11116 let epoch = self.epoch.bump_assigned();
11117 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
11118 let txn_id = self.alloc_txn_id()?;
11119 if let Some(definition) = materialized_view.as_mut() {
11120 definition.last_refresh_epoch = epoch.0;
11121 }
11122 let materialized_view_json = materialized_view
11123 .as_ref()
11124 .map(DdlOp::encode_materialized_view)
11125 .transpose()?;
11126 let mut next_catalog = self.catalog.read().clone();
11127 let entry = next_catalog
11128 .tables
11129 .iter_mut()
11130 .find(|t| t.table_id == table_id)
11131 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
11132 entry.name = new_name.to_string();
11133 if building {
11134 entry.state = TableState::Live;
11135 }
11136 for trigger in &mut next_catalog.triggers {
11137 if matches!(
11138 &trigger.trigger.target,
11139 TriggerTarget::Table(target) if target == name
11140 ) {
11141 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
11142 }
11143 }
11144 if let Some(definition) = next_catalog
11145 .materialized_views
11146 .iter_mut()
11147 .find(|definition| definition.name == name)
11148 {
11149 definition.name = new_name.to_string();
11150 }
11151 if let Some(definition) = materialized_view.take() {
11152 next_catalog.materialized_views.push(definition);
11153 }
11154 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11155 for table in &mut next_catalog.security.rls_tables {
11156 if table == name {
11157 *table = new_name.to_string();
11158 }
11159 }
11160 for policy in &mut next_catalog.security.policies {
11161 if policy.table == name {
11162 policy.table = new_name.to_string();
11163 }
11164 }
11165 for mask in &mut next_catalog.security.masks {
11166 if mask.table == name {
11167 mask.table = new_name.to_string();
11168 }
11169 }
11170 for role in &mut next_catalog.roles {
11171 for permission in &mut role.permissions {
11172 rename_permission_table(permission, name, new_name);
11173 }
11174 }
11175 advance_security_version(&mut next_catalog)?;
11176 let ddl = if building {
11177 DdlOp::PublishBuildingTable {
11178 table_id,
11179 new_name: new_name.to_string(),
11180 }
11181 } else {
11182 DdlOp::RenameTable {
11183 table_id,
11184 new_name: new_name.to_string(),
11185 }
11186 };
11187 let commit_seq = {
11188 let mut wal = self.shared_wal.lock();
11189 if let Some(before_commit) = before_commit {
11190 before_commit()?;
11191 }
11192 let append: Result<u64> = (|| {
11193 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
11194 if let Some(definition_json) = materialized_view_json {
11195 wal.append(
11196 txn_id,
11197 table_id,
11198 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
11199 name: new_name.to_string(),
11200 definition_json,
11201 }),
11202 )?;
11203 }
11204 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11205 wal.append_commit(txn_id, epoch, &[])
11206 })();
11207 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11208 };
11209 self.await_durable_commit(commit_seq, epoch)?;
11210
11211 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
11212 if let Some(table) = self.tables.read().get(&table_id) {
11215 table.lock().set_catalog_name(new_name.to_string());
11216 }
11217 self.finish_durable_publish(epoch, &mut _epoch_guard, checkpoint)?;
11218 Ok(epoch)
11219 }
11220
11221 pub fn alter_column(
11222 &self,
11223 table_name: &str,
11224 column_name: &str,
11225 change: AlterColumn,
11226 ) -> Result<ColumnDef> {
11227 self.alter_column_with_epoch(table_name, column_name, change)
11228 .map(|(column, _)| column)
11229 }
11230
11231 pub fn alter_column_with_epoch(
11232 &self,
11233 table_name: &str,
11234 column_name: &str,
11235 change: AlterColumn,
11236 ) -> Result<(ColumnDef, Option<Epoch>)> {
11237 self.alter_column_with_epoch_inner(table_name, column_name, change, None, None, None)
11238 }
11239
11240 pub fn alter_column_with_epoch_controlled<B, A>(
11245 &self,
11246 table_name: &str,
11247 column_name: &str,
11248 change: AlterColumn,
11249 control: &crate::ExecutionControl,
11250 mut before_commit: B,
11251 mut after_commit: A,
11252 ) -> Result<(ColumnDef, Option<Epoch>)>
11253 where
11254 B: FnMut() -> Result<()>,
11255 A: FnMut(Option<Epoch>) -> Result<()>,
11256 {
11257 self.alter_column_with_epoch_inner(
11258 table_name,
11259 column_name,
11260 change,
11261 Some(control),
11262 Some(&mut before_commit),
11263 Some(&mut after_commit),
11264 )
11265 }
11266
11267 #[allow(clippy::too_many_arguments)]
11268 fn alter_column_with_epoch_inner(
11269 &self,
11270 table_name: &str,
11271 column_name: &str,
11272 change: AlterColumn,
11273 control: Option<&crate::ExecutionControl>,
11274 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11275 mut after_commit: Option<&mut dyn FnMut(Option<Epoch>) -> Result<()>>,
11276 ) -> Result<(ColumnDef, Option<Epoch>)> {
11277 use crate::wal::DdlOp;
11278 use std::sync::atomic::Ordering;
11279
11280 self.require(&crate::auth::Permission::Ddl)?;
11281 commit_prepare_checkpoint(control, 0)?;
11282 if self.poisoned.load(Ordering::Relaxed) {
11283 return Err(MongrelError::Other(
11284 "database poisoned by fsync error".into(),
11285 ));
11286 }
11287
11288 let _g = self.ddl_lock.lock();
11289 let table_id = {
11290 let cat = self.catalog.read();
11291 cat.live(table_name)
11292 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
11293 .table_id
11294 };
11295 let handle =
11296 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
11297 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
11298 })?;
11299
11300 let backfill = {
11306 let table = handle.lock();
11307 let old = table
11308 .schema()
11309 .column(column_name)
11310 .cloned()
11311 .ok_or_else(|| MongrelError::Schema(format!("unknown column {column_name}")))?;
11312 let next_flags = change.flags.unwrap_or(old.flags);
11313 if old.flags.contains(crate::schema::ColumnFlags::NULLABLE)
11314 && !next_flags.contains(crate::schema::ColumnFlags::NULLABLE)
11315 && old.default_value.is_some()
11316 {
11317 let snapshot = Snapshot::at(self.epoch.visible());
11318 let mut updates = Vec::new();
11319 let rows = match control {
11320 Some(control) => table.visible_rows_controlled(snapshot, control)?,
11321 None => table.visible_rows(snapshot)?,
11322 };
11323 for (row_index, row) in rows.into_iter().enumerate() {
11324 commit_prepare_checkpoint(control, row_index)?;
11325 if row
11326 .columns
11327 .get(&old.id)
11328 .is_some_and(|value| !matches!(value, Value::Null))
11329 {
11330 continue;
11331 }
11332 let mut cells: Vec<(u16, Value)> = row.columns.into_iter().collect();
11333 table.apply_defaults(&mut cells)?;
11334 updates.push((
11335 table_id,
11336 crate::txn::Staged::Update {
11337 row_id: row.row_id,
11338 new_row: cells,
11339 changed_columns: vec![old.id],
11340 },
11341 ));
11342 }
11343 updates
11344 } else {
11345 Vec::new()
11346 }
11347 };
11348 let durable_epoch = std::cell::Cell::new(None);
11349 let backfill_epoch = if backfill.is_empty() {
11350 None
11351 } else {
11352 let (principal, catalog_bound) = self.transaction_principal_snapshot();
11353 let txn_id = self.alloc_txn_id()?;
11354 let mut entered_fence = false;
11355 let commit_result = match (control, before_commit.as_deref_mut()) {
11356 (Some(control), Some(before_commit)) => self
11357 .commit_transaction_with_external_states_controlled(
11358 txn_id,
11359 self.epoch.visible(),
11360 backfill,
11361 Vec::new(),
11362 Vec::new(),
11363 principal.clone(),
11364 catalog_bound,
11365 None,
11366 control,
11367 &mut || {
11368 before_commit()?;
11369 entered_fence = true;
11370 Ok(())
11371 },
11372 )
11373 .map(|(epoch, _)| epoch),
11374 _ => self
11375 .commit_transaction_with_external_states(
11376 txn_id,
11377 self.epoch.visible(),
11378 backfill,
11379 Vec::new(),
11380 Vec::new(),
11381 principal,
11382 catalog_bound,
11383 None,
11384 )
11385 .map(|(epoch, _)| epoch),
11386 };
11387 let commit_result = if entered_fence {
11388 finish_controlled_commit_attempt(commit_result, &mut after_commit)
11389 } else {
11390 commit_result
11391 };
11392 match &commit_result {
11393 Ok(epoch) => durable_epoch.set(Some(*epoch)),
11394 Err(MongrelError::DurableCommit { epoch, .. }) => {
11395 durable_epoch.set(Some(Epoch(*epoch)));
11396 }
11397 Err(_) => {}
11398 }
11399 Some(commit_result?)
11400 };
11401 let result: Result<(ColumnDef, Option<Epoch>)> = (|| {
11402 let _security_write = self.security_write()?;
11403 self.require(&crate::auth::Permission::Ddl)?;
11404 if self
11405 .catalog
11406 .read()
11407 .live(table_name)
11408 .is_none_or(|entry| entry.table_id != table_id)
11409 {
11410 return Err(MongrelError::Conflict(format!(
11411 "table {table_name:?} changed during ALTER"
11412 )));
11413 }
11414 let mut table = handle.lock();
11415 let (column, prepared_schema) = table.prepare_alter_column(column_name, &change)?;
11416 let renamed_column = (column.name != column_name).then(|| column.name.clone());
11417 let Some(prepared_schema) = prepared_schema else {
11418 return Ok((column, backfill_epoch));
11419 };
11420
11421 let commit_lock = Arc::clone(&self.commit_lock);
11422 let _c = commit_lock.lock();
11423 let epoch = self.epoch.bump_assigned();
11424 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
11425 let txn_id = self.alloc_txn_id()?;
11426 let column_json = DdlOp::encode_column(&column)?;
11427 let mut next_catalog = self.catalog.read().clone();
11428 let catalog_entry_index = next_catalog
11429 .tables
11430 .iter()
11431 .position(|entry| entry.table_id == table_id)
11432 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
11433 if let Some(new_column_name) = &renamed_column {
11434 for (trigger_index, trigger) in next_catalog.triggers.iter_mut().enumerate() {
11435 commit_prepare_checkpoint(control, trigger_index)?;
11436 if matches!(
11437 &trigger.trigger.target,
11438 TriggerTarget::Table(target) if target == table_name
11439 ) {
11440 trigger.trigger = trigger.trigger.renamed_update_column(
11441 column_name,
11442 new_column_name.clone(),
11443 epoch.0,
11444 )?;
11445 }
11446 }
11447 for (role_index, role) in next_catalog.roles.iter_mut().enumerate() {
11448 commit_prepare_checkpoint(control, role_index)?;
11449 for (permission_index, permission) in role.permissions.iter_mut().enumerate() {
11450 commit_prepare_checkpoint(control, permission_index)?;
11451 rename_permission_column(
11452 permission,
11453 table_name,
11454 column_name,
11455 new_column_name,
11456 );
11457 }
11458 }
11459 advance_security_version(&mut next_catalog)?;
11460 }
11461 next_catalog.tables[catalog_entry_index].schema = prepared_schema.clone();
11462 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11463 commit_prepare_checkpoint(control, 0)?;
11464 let mut entered_fence = false;
11465 if let Some(before_commit) = before_commit.as_deref_mut() {
11466 before_commit()?;
11467 entered_fence = true;
11468 }
11469 let commit_result: Result<Epoch> = (|| {
11470 let commit_seq = {
11471 let mut wal = self.shared_wal.lock();
11472 let append: Result<u64> = (|| {
11473 wal.append(
11474 txn_id,
11475 table_id,
11476 crate::wal::Op::Ddl(DdlOp::AlterTable {
11477 table_id,
11478 column_json,
11479 }),
11480 )?;
11481 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11482 wal.append_commit(txn_id, epoch, &[])
11483 })();
11484 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11485 };
11486 self.await_durable_commit(commit_seq, epoch)?;
11487 durable_epoch.set(Some(epoch));
11488
11489 table.apply_altered_schema_prepared(prepared_schema);
11490 let schema = table.schema().clone();
11491 let table_checkpoint = table.checkpoint_altered_schema();
11492 drop(table);
11493 next_catalog.tables[catalog_entry_index].schema = schema;
11494 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11495 let catalog_result =
11496 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref());
11497 let security_version = next_catalog.security_version;
11498 *self.catalog.write() = next_catalog;
11499 if renamed_column.is_some() {
11500 self.security_coordinator
11501 .version
11502 .store(security_version, Ordering::Release);
11503 }
11504 self.epoch.publish_in_order(epoch);
11505 _epoch_guard.disarm();
11506 if let Err(error) = table_checkpoint.and(catalog_result) {
11507 self.poisoned.store(true, Ordering::Relaxed);
11508 return Err(MongrelError::DurableCommit {
11509 epoch: epoch.0,
11510 message: error.to_string(),
11511 });
11512 }
11513 Ok(epoch)
11514 })();
11515 let commit_result = if entered_fence {
11516 finish_controlled_commit_attempt(commit_result, &mut after_commit)
11517 } else {
11518 commit_result
11519 };
11520 let epoch = commit_result?;
11521 Ok((column, Some(epoch)))
11522 })();
11523 result.map_err(|error| match (durable_epoch.get(), error) {
11524 (_, error @ MongrelError::DurableCommit { .. }) => error,
11525 (Some(epoch), error) => MongrelError::DurableCommit {
11526 epoch: epoch.0,
11527 message: error.to_string(),
11528 },
11529 (None, error) => error,
11530 })
11531 }
11532
11533 pub fn set_table_ttl(
11536 &self,
11537 table_name: &str,
11538 column_name: &str,
11539 duration_nanos: u64,
11540 ) -> Result<crate::manifest::TtlPolicy> {
11541 let policy = self.replace_table_ttl(table_name, Some((column_name, duration_nanos)))?;
11542 policy.ok_or_else(|| MongrelError::Other("set TTL produced no policy".into()))
11543 }
11544
11545 #[doc(hidden)]
11547 pub fn set_building_table_ttl(
11548 &self,
11549 table_name: &str,
11550 column_name: &str,
11551 duration_nanos: u64,
11552 ) -> Result<crate::manifest::TtlPolicy> {
11553 let policy = self.replace_table_ttl_with_state(
11554 table_name,
11555 Some((column_name, duration_nanos)),
11556 true,
11557 )?;
11558 policy
11559 .ok_or_else(|| MongrelError::Other("set building-table TTL produced no policy".into()))
11560 }
11561
11562 pub fn clear_table_ttl(&self, table_name: &str) -> Result<()> {
11563 self.replace_table_ttl(table_name, None)?;
11564 Ok(())
11565 }
11566
11567 fn replace_table_ttl(
11568 &self,
11569 table_name: &str,
11570 requested: Option<(&str, u64)>,
11571 ) -> Result<Option<crate::manifest::TtlPolicy>> {
11572 self.replace_table_ttl_with_state(table_name, requested, false)
11573 }
11574
11575 fn replace_table_ttl_with_state(
11576 &self,
11577 table_name: &str,
11578 requested: Option<(&str, u64)>,
11579 building: bool,
11580 ) -> Result<Option<crate::manifest::TtlPolicy>> {
11581 use crate::wal::DdlOp;
11582 use std::sync::atomic::Ordering;
11583
11584 self.require(&crate::auth::Permission::Ddl)?;
11585 if self.poisoned.load(Ordering::Relaxed) {
11586 return Err(MongrelError::Other(
11587 "database poisoned by fsync error".into(),
11588 ));
11589 }
11590
11591 let _g = self.ddl_lock.lock();
11592 let _security_write = self.security_write()?;
11593 self.require(&crate::auth::Permission::Ddl)?;
11594 let table_id = {
11595 let cat = self.catalog.read();
11596 if building {
11597 cat.building(table_name)
11598 } else {
11599 cat.live(table_name)
11600 }
11601 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
11602 .table_id
11603 };
11604 let handle =
11605 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
11606 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
11607 })?;
11608 let mut table = handle.lock();
11609 let policy = match requested {
11610 Some((column, duration)) => Some(table.prepare_ttl_policy(column, duration)?),
11611 None => None,
11612 };
11613 if table.ttl() == policy {
11614 return Ok(policy);
11615 }
11616
11617 let commit_lock = Arc::clone(&self.commit_lock);
11618 let _c = commit_lock.lock();
11619 let epoch = self.epoch.bump_assigned();
11620 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
11621 let txn_id = self.alloc_txn_id()?;
11622 let policy_json = DdlOp::encode_ttl(policy)?;
11623 let mut next_catalog = self.catalog.read().clone();
11624 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11625 let commit_seq = {
11626 let mut wal = self.shared_wal.lock();
11627 let append: Result<u64> = (|| {
11628 wal.append(
11629 txn_id,
11630 table_id,
11631 crate::wal::Op::Ddl(DdlOp::SetTtl {
11632 table_id,
11633 policy_json,
11634 }),
11635 )?;
11636 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11637 wal.append_commit(txn_id, epoch, &[])
11638 })();
11639 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11640 };
11641 self.await_durable_commit(commit_seq, epoch)?;
11642
11643 let mut publish_error = table.apply_ttl_policy_at(policy, epoch).err();
11644 drop(table);
11645 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
11646 publish_error.get_or_insert(error);
11647 }
11648 self.finish_durable_publish(epoch, &mut _epoch_guard, publish_error.map_or(Ok(()), Err))?;
11649 Ok(policy)
11650 }
11651
11652 pub fn gc(&self) -> Result<usize> {
11658 let control = crate::ExecutionControl::new(None);
11659 self.gc_controlled(&control, || true)
11660 }
11661
11662 #[doc(hidden)]
11665 pub fn gc_controlled<F>(
11666 &self,
11667 control: &crate::ExecutionControl,
11668 before_publish: F,
11669 ) -> Result<usize>
11670 where
11671 F: FnOnce() -> bool,
11672 {
11673 self.gc_controlled_with_receipt(control, before_publish)
11674 .map(|(reclaimed, _)| reclaimed)
11675 }
11676
11677 #[doc(hidden)]
11680 pub fn gc_controlled_with_receipt<F>(
11681 &self,
11682 control: &crate::ExecutionControl,
11683 before_publish: F,
11684 ) -> Result<(usize, Option<MaintenanceReceipt>)>
11685 where
11686 F: FnOnce() -> bool,
11687 {
11688 enum Candidate {
11689 Directory(PathBuf),
11690 File(PathBuf),
11691 }
11692
11693 self.require(&crate::auth::Permission::Ddl)?;
11694 let _ddl = self.ddl_lock.lock();
11695 self.require(&crate::auth::Permission::Ddl)?;
11696 control.checkpoint()?;
11697 let maintenance_epoch = self.epoch.visible();
11698 let min_active = self.snapshots.min_active(maintenance_epoch).0;
11699 let mut candidates = Vec::new();
11700
11701 let cat = self.catalog.read();
11703 for (entry_index, entry) in cat.tables.iter().enumerate() {
11704 if entry_index % 256 == 0 {
11705 control.checkpoint()?;
11706 }
11707 if let TableState::Dropped { at_epoch } = entry.state {
11708 if at_epoch <= min_active {
11709 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
11710 if tdir.exists() {
11711 candidates.push(Candidate::Directory(tdir));
11712 }
11713 }
11714 }
11715 }
11716 drop(cat);
11717
11718 let cat = self.catalog.read();
11723 for (entry_index, entry) in cat.tables.iter().enumerate() {
11724 if entry_index % 256 == 0 {
11725 control.checkpoint()?;
11726 }
11727 if !matches!(entry.state, TableState::Live) {
11728 continue;
11729 }
11730 let txn_dir = self
11731 .root
11732 .join(TABLES_DIR)
11733 .join(entry.table_id.to_string())
11734 .join("_txn");
11735 if !txn_dir.exists() {
11736 continue;
11737 }
11738 for (sub_index, sub) in std::fs::read_dir(&txn_dir)?.enumerate() {
11739 if sub_index % 256 == 0 {
11740 control.checkpoint()?;
11741 }
11742 let sub = sub?;
11743 let name = sub.file_name();
11744 let Some(name) = name.to_str() else { continue };
11745 let is_active = name
11747 .parse::<u64>()
11748 .map(|id| self.active_spills.is_active(id))
11749 .unwrap_or(false);
11750 if is_active {
11751 continue;
11752 }
11753 candidates.push(Candidate::Directory(sub.path()));
11754 }
11755 }
11756 drop(cat);
11757
11758 let external_names = {
11759 let cat = self.catalog.read();
11760 cat.external_tables
11761 .iter()
11762 .map(|entry| entry.name.clone())
11763 .collect::<std::collections::HashSet<_>>()
11764 };
11765 let vtab_dir = self.root.join(VTAB_DIR);
11766 if vtab_dir.exists() {
11767 for (entry_index, entry) in std::fs::read_dir(&vtab_dir)?.enumerate() {
11768 if entry_index % 256 == 0 {
11769 control.checkpoint()?;
11770 }
11771 let entry = entry?;
11772 let name = entry.file_name();
11773 let Some(name) = name.to_str() else { continue };
11774 if external_names.contains(name) {
11775 continue;
11776 }
11777 let path = entry.path();
11778 if path.is_dir() {
11779 candidates.push(Candidate::Directory(path));
11780 } else {
11781 candidates.push(Candidate::File(path));
11782 }
11783 }
11784 }
11785
11786 let tables = self
11790 .tables
11791 .read()
11792 .iter()
11793 .map(|(table_id, handle)| (*table_id, handle.clone()))
11794 .collect::<Vec<_>>();
11795 let mut retiring = Vec::new();
11796 for (table_index, (table_id, handle)) in tables.iter().enumerate() {
11797 if table_index % 256 == 0 {
11798 control.checkpoint()?;
11799 }
11800 let backup_pinned: HashSet<u128> = self
11801 .backup_pins
11802 .lock()
11803 .keys()
11804 .filter_map(|(pinned_table, run_id)| {
11805 (*pinned_table == *table_id).then_some(*run_id)
11806 })
11807 .collect();
11808 if handle
11809 .lock()
11810 .has_reapable_retiring(Epoch(min_active), &backup_pinned)
11811 {
11812 retiring.push((handle.clone(), backup_pinned));
11813 }
11814 }
11815
11816 let all_durable = self.active_spills.is_idle()
11824 && tables.iter().all(|(_, handle)| {
11825 let g = handle.lock();
11826 g.memtable_len() == 0 && g.mutable_run_len() == 0
11827 });
11828 let retain = self
11829 .replication_wal_retention_segments
11830 .load(std::sync::atomic::Ordering::Relaxed);
11831 let reap_wal = all_durable
11832 && self
11833 .shared_wal
11834 .lock()
11835 .has_gc_segments_retain_recent(retain)?;
11836
11837 if candidates.is_empty() && retiring.is_empty() && !reap_wal {
11838 return Ok((0, None));
11839 }
11840 control.checkpoint()?;
11841 if !before_publish() {
11842 return Err(MongrelError::Cancelled);
11843 }
11844
11845 let mut reclaimed = 0;
11846 for candidate in candidates {
11847 match candidate {
11848 Candidate::Directory(path) => std::fs::remove_dir_all(path)?,
11849 Candidate::File(path) => std::fs::remove_file(path)?,
11850 }
11851 reclaimed += 1;
11852 }
11853 for (handle, backup_pinned) in retiring {
11854 reclaimed += handle
11855 .lock()
11856 .reap_retiring(Epoch(min_active), &backup_pinned)?;
11857 }
11858 if reap_wal {
11859 reclaimed += self
11860 .shared_wal
11861 .lock()
11862 .gc_segments_retain_recent(u64::MAX, retain)?;
11863 }
11864
11865 Ok((
11866 reclaimed,
11867 Some(MaintenanceReceipt {
11868 epoch: maintenance_epoch,
11869 }),
11870 ))
11871 }
11872
11873 pub fn checkpoint(&self) -> Result<()> {
11891 self.checkpoint_controlled(|| Ok(()))
11892 }
11893
11894 #[doc(hidden)]
11897 pub fn checkpoint_controlled<F>(&self, before_wal_reset: F) -> Result<()>
11898 where
11899 F: FnOnce() -> Result<()>,
11900 {
11901 self.require(&crate::auth::Permission::Ddl)?;
11902 let _replication = self.replication_barrier.write();
11906 let _ddl = self.ddl_lock.lock();
11907 let _security = self.security_coordinator.gate.read();
11908 self.require(&crate::auth::Permission::Ddl)?;
11909
11910 let mut handles = self
11911 .tables
11912 .read()
11913 .iter()
11914 .map(|(table_id, handle)| (*table_id, handle.clone()))
11915 .collect::<Vec<_>>();
11916 handles.sort_by_key(|(table_id, _)| *table_id);
11917 let mut tables = handles
11918 .iter()
11919 .map(|(table_id, handle)| (*table_id, handle.lock()))
11920 .collect::<Vec<_>>();
11921
11922 for (_, table) in &mut tables {
11924 if table.has_pending_writes() || table.memtable_len() > 0 || table.mutable_run_len() > 0
11925 {
11926 table.force_flush()?;
11927 }
11928 }
11929
11930 for (_, table) in &mut tables {
11933 if table.run_count() >= 2 || table.should_compact() {
11934 table.compact()?;
11935 }
11936 }
11937
11938 before_wal_reset()?;
11939
11940 let maintenance_epoch = self.epoch.visible();
11942 let min_active = self.snapshots.min_active(maintenance_epoch);
11943 for (table_id, table) in &mut tables {
11944 let backup_pinned: HashSet<u128> = self
11945 .backup_pins
11946 .lock()
11947 .keys()
11948 .filter_map(|(pinned_table, run_id)| {
11949 (*pinned_table == *table_id).then_some(*run_id)
11950 })
11951 .collect();
11952 table.reap_retiring(min_active, &backup_pinned)?;
11953 }
11954
11955 self.shared_wal.lock().reset_after_checkpoint()?;
11958
11959 let catalog_snapshot = self.catalog.read().clone();
11961 for entry in &catalog_snapshot.tables {
11962 if matches!(entry.state, TableState::Dropped { at_epoch } if at_epoch <= min_active.0) {
11963 crate::durable_file::remove_directory_all(
11964 &self.root.join(TABLES_DIR).join(entry.table_id.to_string()),
11965 )?;
11966 }
11967 if !matches!(entry.state, TableState::Live) {
11968 continue;
11969 }
11970 let transaction_dir = self
11971 .root
11972 .join(TABLES_DIR)
11973 .join(entry.table_id.to_string())
11974 .join("_txn");
11975 if transaction_dir.is_dir() {
11976 for child in std::fs::read_dir(&transaction_dir)? {
11977 let child = child?;
11978 let active = child
11979 .file_name()
11980 .to_str()
11981 .and_then(|name| name.parse::<u64>().ok())
11982 .is_some_and(|txn_id| self.active_spills.is_active(txn_id));
11983 if !active {
11984 crate::durable_file::remove_directory_all(&child.path())?;
11985 }
11986 }
11987 }
11988 }
11989 let external_names = catalog_snapshot
11990 .external_tables
11991 .iter()
11992 .map(|entry| entry.name.as_str())
11993 .collect::<HashSet<_>>();
11994 let external_root = self.root.join(VTAB_DIR);
11995 if external_root.is_dir() {
11996 for entry in std::fs::read_dir(&external_root)? {
11997 let entry = entry?;
11998 let name = entry.file_name();
11999 if name
12000 .to_str()
12001 .is_some_and(|name| external_names.contains(name))
12002 {
12003 continue;
12004 }
12005 if entry.file_type()?.is_dir() {
12006 crate::durable_file::remove_directory_all(&entry.path())?;
12007 } else {
12008 std::fs::remove_file(entry.path())?;
12009 crate::durable_file::sync_directory(&external_root)?;
12010 }
12011 }
12012 }
12013
12014 catalog::write_atomic(&self.root, &catalog_snapshot, self.meta_dek.as_ref())?;
12017 let visible = self.epoch.visible();
12018 for (_, table) in &tables {
12019 table.persist_manifest(visible)?;
12020 }
12021
12022 Ok(())
12023 }
12024 fn alloc_txn_id(&self) -> Result<u64> {
12025 crate::txn::allocate_txn_id(&self.next_txn_id)
12026 }
12027
12028 pub fn set_spill_threshold(&self, bytes: u64) {
12032 self.spill_threshold
12033 .store(bytes, std::sync::atomic::Ordering::Relaxed);
12034 }
12035
12036 #[doc(hidden)]
12040 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12041 *self.spill_hook.lock() = Some(Box::new(f));
12042 }
12043
12044 #[doc(hidden)]
12047 pub fn __set_security_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12048 *self.security_commit_hook.lock() = Some(Box::new(f));
12049 }
12050
12051 #[doc(hidden)]
12054 pub fn __set_catalog_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12055 *self.catalog_commit_hook.lock() = Some(Box::new(f));
12056 }
12057
12058 #[doc(hidden)]
12061 pub fn __set_backup_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12062 *self.backup_hook.lock() = Some(Box::new(f));
12063 }
12064
12065 #[doc(hidden)]
12067 pub fn __set_replication_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12068 *self.replication_hook.lock() = Some(Box::new(f));
12069 }
12070
12071 #[doc(hidden)]
12075 pub fn __wal_group_sync_count(&self) -> u64 {
12076 self.shared_wal.lock().group_sync_count()
12077 }
12078
12079 #[doc(hidden)]
12082 pub fn __poison(&self) {
12083 self.poisoned
12084 .store(true, std::sync::atomic::Ordering::Relaxed);
12085 }
12086
12087 pub fn check(&self) -> Vec<CheckIssue> {
12100 match self.check_inner(None) {
12101 Ok(issues) => issues,
12102 Err(error) => vec![CheckIssue {
12103 table_id: WAL_TABLE_ID,
12104 table_name: "shared WAL".into(),
12105 severity: "error".into(),
12106 description: error.to_string(),
12107 }],
12108 }
12109 }
12110
12111 #[doc(hidden)]
12113 pub fn check_controlled(&self, control: &crate::ExecutionControl) -> Result<Vec<CheckIssue>> {
12114 self.check_inner(Some(control))
12115 }
12116
12117 fn check_inner(&self, control: Option<&crate::ExecutionControl>) -> Result<Vec<CheckIssue>> {
12118 let mut issues = Vec::new();
12119 let cat = self.catalog.read();
12120 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
12121 for (table_index, entry) in cat.tables.iter().enumerate() {
12122 if table_index % 256 == 0 {
12123 if let Some(control) = control {
12124 control.checkpoint()?;
12125 }
12126 }
12127 if !matches!(entry.state, TableState::Live) {
12128 continue;
12129 }
12130 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
12131 let mut err = |sev: &str, desc: String| {
12132 issues.push(CheckIssue {
12133 table_id: entry.table_id,
12134 table_name: entry.name.clone(),
12135 severity: sev.into(),
12136 description: desc,
12137 });
12138 };
12139 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
12140 Ok(m) => m,
12141 Err(e) => {
12142 err("error", format!("manifest read failed: {e}"));
12143 continue;
12144 }
12145 };
12146 if m.flushed_epoch > m.current_epoch {
12147 err(
12148 "error",
12149 format!(
12150 "flushed_epoch {} exceeds current_epoch {} (impossible)",
12151 m.flushed_epoch, m.current_epoch
12152 ),
12153 );
12154 }
12155
12156 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
12157 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
12158 for (run_index, rr) in m.runs.iter().enumerate() {
12159 if run_index % 256 == 0 {
12160 if let Some(control) = control {
12161 control.checkpoint()?;
12162 }
12163 }
12164 referenced.insert(rr.run_id);
12165 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
12166 if !run_path.exists() {
12167 err("error", format!("missing run file: r-{}.sr", rr.run_id));
12168 continue;
12169 }
12170 match crate::sorted_run::RunReader::open(
12171 &run_path,
12172 entry.schema.clone(),
12173 self.kek.clone(),
12174 ) {
12175 Ok(reader) => {
12176 if reader.row_count() as u64 != rr.row_count {
12177 err(
12178 "error",
12179 format!(
12180 "run r-{} row count mismatch: manifest {} vs run {}",
12181 rr.run_id,
12182 rr.row_count,
12183 reader.row_count()
12184 ),
12185 );
12186 }
12187 }
12188 Err(e) => {
12189 err(
12190 "error",
12191 format!("run r-{} integrity check failed: {e}", rr.run_id),
12192 );
12193 }
12194 }
12195 }
12196
12197 for r in &m.retiring {
12201 referenced.insert(r.run_id);
12202 }
12203
12204 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
12206 for (entry_index, ent) in rd.flatten().enumerate() {
12207 if entry_index % 256 == 0 {
12208 if let Some(control) = control {
12209 control.checkpoint()?;
12210 }
12211 }
12212 let p = ent.path();
12213 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
12214 continue;
12215 }
12216 let run_id = p
12217 .file_stem()
12218 .and_then(|s| s.to_str())
12219 .and_then(|s| s.strip_prefix("r-"))
12220 .and_then(|s| s.parse::<u128>().ok());
12221 if let Some(id) = run_id {
12222 if !referenced.contains(&id) {
12223 err(
12224 "warning",
12225 format!("orphan run file r-{id}.sr not referenced by the manifest"),
12226 );
12227 }
12228 }
12229 }
12230 }
12231 }
12232
12233 let external_names = cat
12234 .external_tables
12235 .iter()
12236 .map(|entry| entry.name.clone())
12237 .collect::<std::collections::HashSet<_>>();
12238 let vtab_dir = self.root.join(VTAB_DIR);
12239 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
12240 for (entry_index, entry) in entries.flatten().enumerate() {
12241 if entry_index % 256 == 0 {
12242 if let Some(control) = control {
12243 control.checkpoint()?;
12244 }
12245 }
12246 let name = entry.file_name();
12247 let Some(name) = name.to_str() else { continue };
12248 if !external_names.contains(name) {
12249 issues.push(CheckIssue {
12250 table_id: EXTERNAL_TABLE_ID,
12251 table_name: name.to_string(),
12252 severity: "warning".into(),
12253 description: format!(
12254 "orphan external table state entry {:?} not referenced by the catalog",
12255 entry.path()
12256 ),
12257 });
12258 }
12259 }
12260 }
12261
12262 if let Some(control) = control {
12269 control.checkpoint()?;
12270 }
12271 for (seg, msg) in self.shared_wal.lock().verify_segments() {
12272 issues.push(CheckIssue {
12273 table_id: WAL_TABLE_ID,
12274 table_name: "<wal>".into(),
12275 severity: "error".into(),
12276 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
12277 });
12278 }
12279 Ok(issues)
12280 }
12281
12282 pub fn doctor(&self) -> Result<Vec<u64>> {
12286 let control = crate::ExecutionControl::new(None);
12287 self.doctor_controlled(&control, || true)
12288 }
12289
12290 #[doc(hidden)]
12294 pub fn doctor_controlled<F>(
12295 &self,
12296 control: &crate::ExecutionControl,
12297 before_publish: F,
12298 ) -> Result<Vec<u64>>
12299 where
12300 F: FnOnce() -> bool,
12301 {
12302 self.doctor_controlled_with_receipt(control, before_publish)
12303 .map(|(quarantined, _)| quarantined)
12304 }
12305
12306 #[doc(hidden)]
12309 pub fn doctor_controlled_with_receipt<F>(
12310 &self,
12311 control: &crate::ExecutionControl,
12312 before_publish: F,
12313 ) -> Result<(Vec<u64>, Option<MaintenanceReceipt>)>
12314 where
12315 F: FnOnce() -> bool,
12316 {
12317 let _ddl = self.ddl_lock.lock();
12320 let _security_write = self.security_write()?;
12321 let issues = self.check_inner(Some(control))?;
12322 let bad_tables: std::collections::HashSet<u64> = issues
12327 .iter()
12328 .filter(|i| {
12329 i.severity == "error"
12330 && i.table_id != WAL_TABLE_ID
12331 && i.table_id != EXTERNAL_TABLE_ID
12332 })
12333 .map(|i| i.table_id)
12334 .collect();
12335 if bad_tables.is_empty() {
12336 return Ok((Vec::new(), None));
12337 }
12338 let _commit = self.commit_lock.lock();
12339 control.checkpoint()?;
12340 if !before_publish() {
12341 return Err(MongrelError::Cancelled);
12342 }
12343 let maintenance_epoch = self.epoch.bump_assigned();
12344 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), maintenance_epoch);
12345
12346 let qdir = self.root.join("_quarantine");
12347 crate::durable_file::create_directory(&qdir)?;
12348 let mut bad_tables = bad_tables.into_iter().collect::<Vec<_>>();
12349 bad_tables.sort_unstable();
12350
12351 let mut handles = self
12355 .tables
12356 .read()
12357 .iter()
12358 .filter(|(table_id, _)| bad_tables.binary_search(table_id).is_ok())
12359 .map(|(table_id, handle)| (*table_id, handle.clone()))
12360 .collect::<Vec<_>>();
12361 handles.sort_by_key(|(table_id, _)| *table_id);
12362 let mut table_guards = handles
12363 .iter()
12364 .map(|(table_id, handle)| (*table_id, handle.lock()))
12365 .collect::<Vec<_>>();
12366
12367 let mut next_catalog = self.catalog.read().clone();
12368 for table_id in &bad_tables {
12369 if let Some(entry) = next_catalog
12370 .tables
12371 .iter_mut()
12372 .find(|entry| entry.table_id == *table_id)
12373 {
12374 entry.state = TableState::Dropped {
12375 at_epoch: maintenance_epoch.0,
12376 };
12377 }
12378 }
12379 next_catalog.db_epoch = next_catalog.db_epoch.max(maintenance_epoch.0);
12380
12381 let txn_id = self.alloc_txn_id()?;
12382 let commit_seq = {
12383 let mut wal = self.shared_wal.lock();
12384 let append: Result<u64> = (|| {
12385 for table_id in &bad_tables {
12386 wal.append(
12387 txn_id,
12388 *table_id,
12389 crate::wal::Op::Ddl(crate::wal::DdlOp::DropTable {
12390 table_id: *table_id,
12391 }),
12392 )?;
12393 }
12394 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
12395 wal.append_commit(txn_id, maintenance_epoch, &[])
12396 })();
12397 append.map_err(|error| self.commit_outcome_unknown(maintenance_epoch, error))?
12398 };
12399 self.await_durable_commit(commit_seq, maintenance_epoch)?;
12400 for (_, table) in &mut table_guards {
12401 table.mark_unavailable_after_quarantine();
12402 }
12403 {
12404 let mut live_tables = self.tables.write();
12405 for table_id in &bad_tables {
12406 live_tables.remove(table_id);
12407 }
12408 }
12409 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
12410 self.finish_durable_publish(maintenance_epoch, &mut epoch_guard, checkpoint)?;
12411
12412 for table_id in &bad_tables {
12416 let source = self.root.join(TABLES_DIR).join(table_id.to_string());
12417 if source.exists() {
12418 let destination = qdir.join(table_id.to_string());
12419 if let Err(error) = crate::durable_file::rename(&source, &destination) {
12420 return Err(MongrelError::DurableCommit {
12421 epoch: maintenance_epoch.0,
12422 message: format!(
12423 "DOCTOR dropped table {table_id} but quarantine move failed: {error}"
12424 ),
12425 });
12426 }
12427 }
12428 }
12429 Ok((
12430 bad_tables,
12431 Some(MaintenanceReceipt {
12432 epoch: maintenance_epoch,
12433 }),
12434 ))
12435 }
12436
12437 #[allow(dead_code)]
12439 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
12440 self.kek.as_ref()
12441 }
12442
12443 #[allow(dead_code)]
12445 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
12446 &self.epoch
12447 }
12448
12449 #[allow(dead_code)]
12451 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
12452 &self.snapshots
12453 }
12454}
12455
12456fn external_state_dir(root: &Path, name: &str) -> PathBuf {
12457 root.join(VTAB_DIR).join(name)
12458}
12459
12460fn append_catalog_snapshot(
12461 wal: &mut crate::wal::SharedWal,
12462 txn_id: u64,
12463 catalog: &Catalog,
12464) -> Result<()> {
12465 let catalog_json = crate::wal::DdlOp::encode_catalog(catalog)?;
12466 wal.append(
12467 txn_id,
12468 WAL_TABLE_ID,
12469 crate::wal::Op::Ddl(crate::wal::DdlOp::CatalogSnapshot { catalog_json }),
12470 )?;
12471 Ok(())
12472}
12473
12474fn filter_ignored_staging(
12475 staging: Vec<(u64, crate::txn::Staged)>,
12476 ignored_indices: &std::collections::BTreeSet<usize>,
12477) -> Vec<(u64, crate::txn::Staged)> {
12478 if ignored_indices.is_empty() {
12479 return staging;
12480 }
12481 staging
12482 .into_iter()
12483 .enumerate()
12484 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
12485 .collect()
12486}
12487
12488fn external_state_file(root: &Path, name: &str) -> PathBuf {
12489 external_state_dir(root, name).join("state.json")
12490}
12491
12492fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
12493 let path = external_state_file(root, name);
12494 match std::fs::read(path) {
12495 Ok(bytes) => Ok(bytes),
12496 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
12497 Err(e) => Err(e.into()),
12498 }
12499}
12500
12501fn current_external_state_bytes(
12502 root: &Path,
12503 external_states: &[(String, Vec<u8>)],
12504 name: &str,
12505) -> Result<Vec<u8>> {
12506 for (table, state) in external_states.iter().rev() {
12507 if table == name {
12508 return Ok(state.clone());
12509 }
12510 }
12511 read_external_state_file(root, name)
12512}
12513
12514fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
12515 let mut out = external_states;
12516 dedup_external_states_in_place(&mut out);
12517 out
12518}
12519
12520fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
12521 let mut seen = std::collections::HashSet::new();
12522 let mut out = Vec::with_capacity(external_states.len());
12523 for (name, state) in std::mem::take(external_states).into_iter().rev() {
12524 if seen.insert(name.clone()) {
12525 out.push((name, state));
12526 }
12527 }
12528 out.reverse();
12529 *external_states = out;
12530}
12531
12532fn prepare_external_state_file(
12533 root: &Path,
12534 name: &str,
12535 state: &[u8],
12536 txn_id: u64,
12537) -> Result<PathBuf> {
12538 crate::durable_file::create_directory(&root.join(VTAB_DIR))?;
12539 let dir = external_state_dir(root, name);
12540 crate::durable_file::create_directory(&dir)?;
12541 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
12542 {
12543 let mut file = std::fs::OpenOptions::new()
12544 .create_new(true)
12545 .write(true)
12546 .open(&pending)?;
12547 file.write_all(state)?;
12548 file.sync_all()?;
12549 }
12550 Ok(pending)
12551}
12552
12553fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
12554 let path = external_state_file(root, name);
12555 crate::durable_file::replace(pending, &path)?;
12556 Ok(())
12557}
12558
12559fn write_external_state_file(
12560 durable: &crate::durable_file::DurableRoot,
12561 name: &str,
12562 state: &[u8],
12563) -> Result<()> {
12564 let directory = Path::new(VTAB_DIR).join(name);
12565 durable.create_directory_all(&directory)?;
12566 durable.write_atomic(directory.join("state.json"), state)?;
12567 Ok(())
12568}
12569
12570fn validate_recovered_data_table(
12571 catalog: &Catalog,
12572 tables: &HashMap<u64, TableHandle>,
12573 table_id: u64,
12574 commit_epoch: u64,
12575 offset: u64,
12576) -> Result<bool> {
12577 let entry = catalog
12578 .tables
12579 .iter()
12580 .find(|entry| entry.table_id == table_id)
12581 .ok_or_else(|| MongrelError::CorruptWal {
12582 offset,
12583 reason: format!("committed record references unknown table {table_id}"),
12584 })?;
12585 if commit_epoch < entry.created_epoch {
12586 return Err(MongrelError::CorruptWal {
12587 offset,
12588 reason: format!(
12589 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
12590 entry.created_epoch
12591 ),
12592 });
12593 }
12594 match entry.state {
12595 TableState::Dropped { at_epoch } => {
12596 let abandoned_build_boundary =
12601 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
12602 if commit_epoch >= at_epoch && !abandoned_build_boundary {
12603 Err(MongrelError::CorruptWal {
12604 offset,
12605 reason: format!(
12606 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
12607 ),
12608 })
12609 } else {
12610 Ok(false)
12611 }
12612 }
12613 TableState::Live | TableState::Building { .. } => {
12614 if tables.contains_key(&table_id) {
12615 Ok(true)
12616 } else {
12617 Err(MongrelError::CorruptWal {
12618 offset,
12619 reason: format!("live table {table_id} has no mounted recovery handle"),
12620 })
12621 }
12622 }
12623 }
12624}
12625
12626type RecoveryTableStage = (
12627 Vec<crate::memtable::Row>,
12628 Vec<(crate::rowid::RowId, Epoch)>,
12629 Option<Epoch>,
12630 Epoch,
12631);
12632
12633#[derive(Clone)]
12634struct RecoveryValidationTable {
12635 schema: Schema,
12636 flushed_epoch: u64,
12637}
12638
12639fn validate_shared_wal_recovery_plan(
12640 durable_root: &crate::durable_file::DurableRoot,
12641 catalog: &Catalog,
12642 recovered_table_ids: &HashSet<u64>,
12643 reconciled_table_ids: &HashSet<u64>,
12644 meta_dek: Option<&[u8; META_DEK_LEN]>,
12645 kek: Option<Arc<crate::encryption::Kek>>,
12646 records: &[crate::wal::Record],
12647) -> Result<()> {
12648 use crate::wal::{DdlOp, Op};
12649
12650 let mut tables = HashMap::<u64, RecoveryValidationTable>::new();
12651 for entry in &catalog.tables {
12652 if !matches!(entry.state, TableState::Live) {
12653 continue;
12654 }
12655 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
12656 let manifest = match crate::manifest::read_durable(durable_root, &relative_dir, meta_dek) {
12657 Ok(manifest) => Some(manifest),
12658 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
12659 Err(error) => return Err(error),
12660 };
12661 let flushed_epoch = if let Some(manifest) = manifest {
12662 if manifest.table_id != entry.table_id {
12663 return Err(MongrelError::Conflict(format!(
12664 "catalog table {} storage identity mismatch",
12665 entry.table_id
12666 )));
12667 }
12668 if (manifest.schema_id != entry.schema.schema_id
12669 && !reconciled_table_ids.contains(&entry.table_id))
12670 || manifest.flushed_epoch > manifest.current_epoch
12671 || manifest.global_idx_epoch > manifest.current_epoch
12672 || manifest.next_row_id == u64::MAX
12673 || manifest.auto_inc_next < 0
12674 || manifest.auto_inc_next == i64::MAX
12675 || (entry.schema.auto_increment_column().is_none() && manifest.auto_inc_next != 0)
12676 {
12677 return Err(MongrelError::InvalidArgument(format!(
12678 "table {} manifest counters or schema identity are invalid",
12679 entry.table_id
12680 )));
12681 }
12682 #[cfg(feature = "encryption")]
12683 let idx_dek = kek.as_ref().map(|key| key.derive_idx_key());
12684 #[cfg(not(feature = "encryption"))]
12685 let idx_dek: Option<zeroize::Zeroizing<[u8; 32]>> = None;
12686 crate::global_idx::read_durable_for(
12687 durable_root,
12688 &relative_dir,
12689 entry.table_id,
12690 &entry.schema,
12691 idx_dek.as_deref(),
12692 )?;
12693 let mut run_ids = HashSet::new();
12694 let mut maximum_row_id = None::<u64>;
12695 for run in &manifest.runs {
12696 if run.run_id >= u64::MAX as u128
12697 || run.epoch_created > manifest.current_epoch
12698 || !run_ids.insert(run.run_id)
12699 {
12700 return Err(MongrelError::InvalidArgument(format!(
12701 "table {} manifest contains an invalid or duplicate run id",
12702 entry.table_id
12703 )));
12704 }
12705 let relative = relative_dir
12706 .join(crate::engine::RUNS_DIR)
12707 .join(format!("r-{}.sr", run.run_id as u64));
12708 let file = durable_root.open_regular(&relative)?;
12709 let mut reader = crate::sorted_run::RunReader::open_file(
12710 file,
12711 entry.schema.clone(),
12712 kek.clone(),
12713 )?;
12714 let header = reader.header();
12715 if header.run_id != run.run_id
12716 || header.level != run.level
12717 || header.row_count != run.row_count
12718 || !header.is_uniform_epoch() && header.epoch_created != run.epoch_created
12719 || header.is_uniform_epoch() && header.epoch_created != 0
12720 || header.schema_id > entry.schema.schema_id
12721 {
12722 return Err(MongrelError::InvalidArgument(format!(
12723 "table {} run {} differs from its manifest: header=(id {}, level {}, rows {}, epoch {}, schema {}), manifest=(id {}, level {}, rows {}, epoch {}, schema <= {})",
12724 entry.table_id,
12725 run.run_id,
12726 header.run_id,
12727 header.level,
12728 header.row_count,
12729 header.epoch_created,
12730 header.schema_id,
12731 run.run_id,
12732 run.level,
12733 run.row_count,
12734 run.epoch_created,
12735 entry.schema.schema_id,
12736 )));
12737 }
12738 if header.row_count != 0 {
12739 maximum_row_id = Some(
12740 maximum_row_id
12741 .map_or(header.max_row_id, |value| value.max(header.max_row_id)),
12742 );
12743 }
12744 reader.validate_all_pages()?;
12745 }
12746 if maximum_row_id.is_some_and(|maximum| manifest.next_row_id <= maximum) {
12747 return Err(MongrelError::InvalidArgument(format!(
12748 "table {} next_row_id does not advance beyond persisted rows",
12749 entry.table_id
12750 )));
12751 }
12752 for run in &manifest.retiring {
12753 if run.run_id >= u64::MAX as u128
12754 || run.retire_epoch > manifest.current_epoch
12755 || !run_ids.insert(run.run_id)
12756 {
12757 return Err(MongrelError::InvalidArgument(format!(
12758 "table {} manifest contains an invalid or aliased retired run",
12759 entry.table_id
12760 )));
12761 }
12762 }
12763 manifest.flushed_epoch
12764 } else {
12765 if !recovered_table_ids.contains(&entry.table_id) {
12766 return Err(MongrelError::NotFound(format!(
12767 "live table {} manifest is missing",
12768 entry.table_id
12769 )));
12770 }
12771 0
12772 };
12773 tables.insert(
12774 entry.table_id,
12775 RecoveryValidationTable {
12776 schema: entry.schema.clone(),
12777 flushed_epoch,
12778 },
12779 );
12780 }
12781
12782 let committed = records
12783 .iter()
12784 .filter_map(|record| match record.op {
12785 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
12786 _ => None,
12787 })
12788 .collect::<HashMap<_, _>>();
12789 let mut run_ids = HashSet::new();
12790 let mut recovered_row_ids = HashMap::<u64, HashSet<u64>>::new();
12791 for record in records {
12792 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
12793 continue;
12794 };
12795 match &record.op {
12796 Op::Put { table_id, rows } => {
12797 let table = validate_recovery_data_table_plan(
12798 catalog,
12799 &tables,
12800 *table_id,
12801 commit_epoch,
12802 record.seq.0,
12803 )?;
12804 let decoded: Vec<crate::memtable::Row> =
12805 bincode::deserialize(rows).map_err(|error| MongrelError::CorruptWal {
12806 offset: record.seq.0,
12807 reason: format!(
12808 "committed Put payload for transaction {} could not be decoded: {error}",
12809 record.txn_id
12810 ),
12811 })?;
12812 if let Some(table) = table {
12813 for row in &decoded {
12814 if !recovered_row_ids
12815 .entry(*table_id)
12816 .or_default()
12817 .insert(row.row_id.0)
12818 {
12819 return Err(MongrelError::CorruptWal {
12820 offset: record.seq.0,
12821 reason: format!(
12822 "committed WAL repeats recovered row id {} for table {table_id}",
12823 row.row_id.0
12824 ),
12825 });
12826 }
12827 validate_recovered_row(&table.schema, row)?;
12828 }
12829 }
12830 }
12831 Op::Delete { table_id, .. } | Op::TruncateTable { table_id } => {
12832 validate_recovery_data_table_plan(
12833 catalog,
12834 &tables,
12835 *table_id,
12836 commit_epoch,
12837 record.seq.0,
12838 )?;
12839 }
12840 Op::ExternalTableState { name, .. } => validate_recovered_external_name(name)?,
12841 Op::Ddl(DdlOp::ResetExternalTableState {
12842 name,
12843 generation_epoch,
12844 }) => {
12845 if *generation_epoch != commit_epoch {
12846 return Err(MongrelError::CorruptWal {
12847 offset: record.seq.0,
12848 reason: format!(
12849 "external state reset epoch {generation_epoch} does not match WAL commit epoch {commit_epoch}"
12850 ),
12851 });
12852 }
12853 validate_recovered_external_name(name)?;
12854 }
12855 Op::TxnCommit { added_runs, .. } => {
12856 for added in added_runs {
12857 let Some(table) = validate_recovery_data_table_plan(
12858 catalog,
12859 &tables,
12860 added.table_id,
12861 commit_epoch,
12862 record.seq.0,
12863 )?
12864 else {
12865 continue;
12866 };
12867 if added.run_id >= u64::MAX as u128
12868 || !run_ids.insert((added.table_id, added.run_id))
12869 {
12870 return Err(MongrelError::CorruptWal {
12871 offset: record.seq.0,
12872 reason: format!(
12873 "duplicate or invalid recovered run {} for table {}",
12874 added.run_id, added.table_id
12875 ),
12876 });
12877 }
12878 if commit_epoch <= table.flushed_epoch {
12879 continue;
12880 }
12881 validate_planned_spilled_run(
12882 durable_root,
12883 record.txn_id,
12884 commit_epoch,
12885 added,
12886 &table.schema,
12887 kek.clone(),
12888 )?;
12889 }
12890 }
12891 _ => {}
12892 }
12893 }
12894 Ok(())
12895}
12896
12897fn validate_recovery_data_table_plan<'a>(
12898 catalog: &Catalog,
12899 tables: &'a HashMap<u64, RecoveryValidationTable>,
12900 table_id: u64,
12901 commit_epoch: u64,
12902 offset: u64,
12903) -> Result<Option<&'a RecoveryValidationTable>> {
12904 let entry = catalog
12905 .tables
12906 .iter()
12907 .find(|entry| entry.table_id == table_id)
12908 .ok_or_else(|| MongrelError::CorruptWal {
12909 offset,
12910 reason: format!("committed record references unknown table {table_id}"),
12911 })?;
12912 if commit_epoch < entry.created_epoch {
12913 return Err(MongrelError::CorruptWal {
12914 offset,
12915 reason: format!(
12916 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
12917 entry.created_epoch
12918 ),
12919 });
12920 }
12921 match entry.state {
12922 TableState::Dropped { at_epoch } => {
12923 let abandoned =
12924 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
12925 if commit_epoch >= at_epoch && !abandoned {
12926 return Err(MongrelError::CorruptWal {
12927 offset,
12928 reason: format!(
12929 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
12930 ),
12931 });
12932 }
12933 Ok(None)
12934 }
12935 TableState::Live => {
12936 tables
12937 .get(&table_id)
12938 .map(Some)
12939 .ok_or_else(|| MongrelError::CorruptWal {
12940 offset,
12941 reason: format!("live table {table_id} has no recovery plan"),
12942 })
12943 }
12944 TableState::Building { .. } => Err(MongrelError::CorruptWal {
12945 offset,
12946 reason: format!("building table {table_id} was not normalized before recovery"),
12947 }),
12948 }
12949}
12950
12951fn validate_planned_spilled_run(
12952 root: &crate::durable_file::DurableRoot,
12953 txn_id: u64,
12954 commit_epoch: u64,
12955 added: &crate::wal::AddedRun,
12956 schema: &Schema,
12957 kek: Option<Arc<crate::encryption::Kek>>,
12958) -> Result<()> {
12959 let table = Path::new(TABLES_DIR).join(added.table_id.to_string());
12960 let destination = table
12961 .join(crate::engine::RUNS_DIR)
12962 .join(format!("r-{}.sr", added.run_id as u64));
12963 let pending = table
12964 .join("_txn")
12965 .join(txn_id.to_string())
12966 .join(format!("r-{}.sr", added.run_id as u64));
12967 let file = match root.open_regular(&destination) {
12968 Ok(file) => file,
12969 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
12970 root.open_regular(&pending).map_err(|pending_error| {
12971 if pending_error.kind() == std::io::ErrorKind::NotFound {
12972 MongrelError::CorruptWal {
12973 offset: commit_epoch,
12974 reason: format!(
12975 "committed spilled run {} for transaction {txn_id} is missing",
12976 added.run_id
12977 ),
12978 }
12979 } else {
12980 pending_error.into()
12981 }
12982 })?
12983 }
12984 Err(error) => return Err(error.into()),
12985 };
12986 let mut reader = crate::sorted_run::RunReader::open_file(file, schema.clone(), kek)?;
12987 let header = reader.header();
12988 if header.run_id != added.run_id
12989 || header.content_hash != added.content_hash
12990 || header.row_count != added.row_count
12991 || header.level != added.level
12992 || header.min_row_id != added.min_row_id
12993 || header.max_row_id != added.max_row_id
12994 || header.schema_id != schema.schema_id
12995 || !header.is_uniform_epoch()
12996 || header.epoch_created != 0
12997 {
12998 return Err(MongrelError::CorruptWal {
12999 offset: commit_epoch,
13000 reason: format!(
13001 "committed spilled run {} metadata differs from WAL",
13002 added.run_id
13003 ),
13004 });
13005 }
13006 reader.validate_all_pages()?;
13007 Ok(())
13008}
13009
13010fn recover_shared_wal(
13019 durable_root: &crate::durable_file::DurableRoot,
13020 tables: &HashMap<u64, TableHandle>,
13021 catalog: &Catalog,
13022 epoch: &EpochAuthority,
13023 records: &[crate::wal::Record],
13024) -> Result<()> {
13025 use crate::memtable::Row;
13026 use crate::wal::{DdlOp, Op};
13027
13028 let mut committed: HashMap<u64, u64> = HashMap::new();
13030 let mut spilled_to_link: Vec<(
13031 u64, u64, Vec<crate::wal::AddedRun>,
13034 )> = Vec::new();
13035 for r in records {
13036 if let Op::TxnCommit {
13037 epoch: ce,
13038 ref added_runs,
13039 } = r.op
13040 {
13041 committed.insert(r.txn_id, ce);
13042 if !added_runs.is_empty() {
13043 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
13044 }
13045 }
13046 }
13047 for record in records {
13048 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
13049 continue;
13050 };
13051 match &record.op {
13052 Op::Put { table_id, .. }
13053 | Op::Delete { table_id, .. }
13054 | Op::TruncateTable { table_id } => {
13055 validate_recovered_data_table(
13056 catalog,
13057 tables,
13058 *table_id,
13059 commit_epoch,
13060 record.seq.0,
13061 )?;
13062 }
13063 Op::TxnCommit { added_runs, .. } => {
13064 for run in added_runs {
13065 validate_recovered_data_table(
13066 catalog,
13067 tables,
13068 run.table_id,
13069 commit_epoch,
13070 record.seq.0,
13071 )?;
13072 }
13073 }
13074 _ => {}
13075 }
13076 }
13077 let truncated_transactions: HashSet<(u64, u64)> = records
13078 .iter()
13079 .filter_map(|record| {
13080 committed.get(&record.txn_id)?;
13081 match record.op {
13082 Op::TruncateTable { table_id } => Some((record.txn_id, table_id)),
13083 _ => None,
13084 }
13085 })
13086 .collect();
13087
13088 enum ExternalRecoveryAction {
13090 Write { name: String, state: Vec<u8> },
13091 Reset { name: String },
13092 }
13093 let mut stage: HashMap<u64, RecoveryTableStage> = HashMap::new();
13094 let mut external_actions = Vec::new();
13095 let mut max_epoch = epoch.visible().0;
13096 for r in records.iter().cloned() {
13097 let Some(&ce) = committed.get(&r.txn_id) else {
13098 continue; };
13100 let commit_epoch = Epoch(ce);
13101 max_epoch = max_epoch.max(ce);
13102 match r.op {
13103 Op::Put { table_id, rows } => {
13104 let skip = tables
13106 .get(&table_id)
13107 .map(|h| h.lock().flushed_epoch() >= ce)
13108 .unwrap_or(true);
13109 if skip {
13110 continue;
13111 }
13112 let rows: Vec<Row> = bincode::deserialize(&rows).map_err(|error| {
13113 MongrelError::CorruptWal {
13114 offset: r.seq.0,
13115 reason: format!(
13116 "committed Put payload for transaction {} could not be decoded: {error}",
13117 r.txn_id
13118 ),
13119 }
13120 })?;
13121 let rows: Vec<Row> = rows
13124 .into_iter()
13125 .map(|mut row| {
13126 row.committed_epoch = commit_epoch;
13127 row
13128 })
13129 .collect();
13130 let entry = stage
13131 .entry(table_id)
13132 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
13133 entry.0.extend(rows);
13134 entry.3 = commit_epoch;
13135 }
13136 Op::Delete { table_id, row_ids } => {
13137 let skip = tables
13138 .get(&table_id)
13139 .map(|h| h.lock().flushed_epoch() >= ce)
13140 .unwrap_or(true);
13141 if skip {
13142 continue;
13143 }
13144 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
13145 let entry = stage
13146 .entry(table_id)
13147 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
13148 entry.1.extend(dels);
13149 entry.3 = commit_epoch;
13150 }
13151 Op::TruncateTable { table_id } => {
13152 let skip = tables
13153 .get(&table_id)
13154 .map(|h| h.lock().flushed_epoch() >= ce)
13155 .unwrap_or(true);
13156 if skip {
13157 continue;
13158 }
13159 stage.insert(
13160 table_id,
13161 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
13162 );
13163 }
13164 Op::ExternalTableState { name, state } => {
13165 let current_generation = catalog
13166 .external_tables
13167 .iter()
13168 .find(|entry| entry.name == name)
13169 .map(|entry| entry.created_epoch);
13170 if current_generation.is_some_and(|created_epoch| ce >= created_epoch) {
13171 validate_recovered_external_name(&name)?;
13172 external_actions.push(ExternalRecoveryAction::Write { name, state });
13173 }
13174 }
13175 Op::Ddl(DdlOp::ResetExternalTableState {
13176 name,
13177 generation_epoch,
13178 }) => {
13179 if generation_epoch != ce {
13180 return Err(MongrelError::CorruptWal {
13181 offset: r.seq.0,
13182 reason: format!(
13183 "external state reset epoch {generation_epoch} does not match WAL commit epoch {ce}"
13184 ),
13185 });
13186 }
13187 validate_recovered_external_name(&name)?;
13188 external_actions.push(ExternalRecoveryAction::Reset { name });
13189 }
13190 Op::Flush { .. }
13191 | Op::TxnCommit { .. }
13192 | Op::TxnAbort
13193 | Op::Ddl(_)
13194 | Op::BeforeImage { .. }
13195 | Op::CommitTimestamp { .. }
13196 | Op::SpilledRows { .. } => {}
13197 }
13198 }
13199 for (_, commit_epoch, added_runs) in &mut spilled_to_link {
13200 added_runs.retain(|added| {
13201 tables
13202 .get(&added.table_id)
13203 .is_some_and(|table| table.lock().flushed_epoch() < *commit_epoch)
13204 });
13205 }
13206 spilled_to_link.retain(|(_, _, added_runs)| !added_runs.is_empty());
13207 validate_recovery_table_stages(tables, &stage)?;
13208 validate_recovery_spilled_runs(durable_root, tables, &spilled_to_link)?;
13209
13210 for action in external_actions {
13214 match action {
13215 ExternalRecoveryAction::Write { name, state } => {
13216 write_external_state_file(durable_root, &name, &state)?;
13217 }
13218 ExternalRecoveryAction::Reset { name } => {
13219 durable_root.create_directory_all(VTAB_DIR)?;
13220 durable_root.remove_directory_all(Path::new(VTAB_DIR).join(name))?;
13221 }
13222 }
13223 }
13224 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
13225 let Some(handle) = tables.get(&table_id) else {
13226 continue;
13227 };
13228 let mut t = handle.lock();
13229 if let Some(epoch) = truncate_epoch {
13230 t.apply_truncate(epoch);
13231 }
13232 t.recover_apply(rows, deletes)?;
13233 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
13237 t.live_count = rows.len() as u64;
13238 t.persist_manifest(table_epoch.max(epoch.visible()))?;
13242 }
13243
13244 for (txn_id, ce, added_runs) in &spilled_to_link {
13248 for ar in added_runs {
13249 let Some(handle) = tables.get(&ar.table_id) else {
13250 continue;
13251 };
13252 let mut t = handle.lock();
13253 let table_dir = Path::new(TABLES_DIR).join(ar.table_id.to_string());
13254 let destination = table_dir
13255 .join(crate::engine::RUNS_DIR)
13256 .join(format!("r-{}.sr", ar.run_id));
13257 match durable_root.open_regular(&destination) {
13258 Ok(_) => {}
13259 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
13260 let pending = table_dir
13261 .join("_txn")
13262 .join(txn_id.to_string())
13263 .join(format!("r-{}.sr", ar.run_id));
13264 durable_root.rename_file_new(&pending, &destination)?;
13265 }
13266 Err(error) => return Err(error.into()),
13267 }
13268 let linked = t.recover_spilled_run(crate::manifest::RunRef {
13274 run_id: ar.run_id,
13275 level: ar.level,
13276 epoch_created: *ce,
13277 row_count: ar.row_count,
13278 });
13279 let replaced = truncated_transactions.contains(&(*txn_id, ar.table_id));
13280 if replaced {
13281 t.set_flushed_epoch(Epoch(*ce));
13282 }
13283 if linked || replaced {
13284 t.persist_manifest(Epoch(*ce).max(epoch.visible()))?;
13285 }
13286 }
13287 }
13288
13289 epoch.advance_recovered(Epoch(max_epoch));
13290 Ok(())
13291}
13292
13293fn reconcile_recovered_table_metadata(
13294 tables: &HashMap<u64, TableHandle>,
13295 epoch: Epoch,
13296) -> Result<()> {
13297 let mut table_ids = tables.keys().copied().collect::<Vec<_>>();
13298 table_ids.sort_unstable();
13299 let mut plans = Vec::with_capacity(table_ids.len());
13300 for table_id in &table_ids {
13301 let handle = tables.get(table_id).ok_or_else(|| {
13302 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
13303 })?;
13304 plans.push((*table_id, handle.lock().plan_recovered_metadata()?));
13305 }
13306 for (table_id, plan) in plans {
13309 let handle = tables.get(&table_id).ok_or_else(|| {
13310 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
13311 })?;
13312 handle.lock().apply_recovered_metadata(plan, epoch)?;
13313 }
13314 Ok(())
13315}
13316
13317fn validate_recovered_external_name(name: &str) -> Result<()> {
13318 if name.is_empty()
13319 || !name.chars().all(|character| {
13320 character.is_ascii_alphanumeric() || character == '_' || character == '-'
13321 })
13322 {
13323 return Err(MongrelError::CorruptWal {
13324 offset: 0,
13325 reason: format!("unsafe recovered external-table name {name:?}"),
13326 });
13327 }
13328 Ok(())
13329}
13330
13331fn validate_recovery_table_stages(
13332 tables: &HashMap<u64, TableHandle>,
13333 stages: &HashMap<u64, RecoveryTableStage>,
13334) -> Result<()> {
13335 for (table_id, (rows, _, _, _)) in stages {
13336 let handle = tables
13337 .get(table_id)
13338 .ok_or_else(|| MongrelError::CorruptWal {
13339 offset: *table_id,
13340 reason: format!("recovery stage references unmounted table {table_id}"),
13341 })?;
13342 let table = handle.lock();
13343 table.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
13346 for row in rows {
13347 validate_recovered_row(table.schema(), row)?;
13348 }
13349 }
13350 Ok(())
13351}
13352
13353fn validate_recovered_row(schema: &Schema, row: &crate::memtable::Row) -> Result<()> {
13354 if row.deleted || row.row_id.0 == u64::MAX {
13355 return Err(MongrelError::CorruptWal {
13356 offset: row.row_id.0,
13357 reason: "committed Put payload contains a tombstone or exhausted row id".into(),
13358 });
13359 }
13360 let cells = row
13361 .columns
13362 .iter()
13363 .map(|(column, value)| (*column, value.clone()))
13364 .collect::<Vec<_>>();
13365 schema
13366 .validate_persisted_values(&cells)
13367 .map_err(|error| MongrelError::CorruptWal {
13368 offset: row.row_id.0,
13369 reason: format!("recovered row violates table schema: {error}"),
13370 })?;
13371 if schema.auto_increment_column().is_some_and(|column| {
13372 matches!(row.columns.get(&column.id), Some(Value::Int64(value)) if *value == i64::MAX)
13373 }) {
13374 return Err(MongrelError::CorruptWal {
13375 offset: row.row_id.0,
13376 reason: "recovered AUTO_INCREMENT value exhausts i64".into(),
13377 });
13378 }
13379 Ok(())
13380}
13381
13382fn validate_recovery_spilled_runs(
13383 root: &crate::durable_file::DurableRoot,
13384 tables: &HashMap<u64, TableHandle>,
13385 spilled: &[(u64, u64, Vec<crate::wal::AddedRun>)],
13386) -> Result<()> {
13387 let mut identities = HashSet::new();
13388 for (txn_id, commit_epoch, added_runs) in spilled {
13389 for added in added_runs {
13390 if added.run_id >= u64::MAX as u128 {
13391 return Err(MongrelError::CorruptWal {
13392 offset: *commit_epoch,
13393 reason: format!(
13394 "recovered run id {} exceeds the on-disk namespace",
13395 added.run_id
13396 ),
13397 });
13398 }
13399 let Some(handle) = tables.get(&added.table_id) else {
13400 continue;
13401 };
13402 if !identities.insert((added.table_id, added.run_id)) {
13403 return Err(MongrelError::CorruptWal {
13404 offset: *commit_epoch,
13405 reason: format!(
13406 "duplicate recovered run {} for table {}",
13407 added.run_id, added.table_id
13408 ),
13409 });
13410 }
13411 let table = handle.lock();
13412 validate_planned_spilled_run(
13413 root,
13414 *txn_id,
13415 *commit_epoch,
13416 added,
13417 table.schema(),
13418 table.kek(),
13419 )?;
13420 }
13421 }
13422 Ok(())
13423}
13424
13425fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
13426 match condition {
13427 ProcedureCondition::Pk { .. } => {
13428 if schema.primary_key().is_none() {
13429 return Err(MongrelError::InvalidArgument(
13430 "procedure condition Pk references a table without a primary key".into(),
13431 ));
13432 }
13433 }
13434 ProcedureCondition::BitmapEq { column_id, .. }
13435 | ProcedureCondition::BitmapIn { column_id, .. }
13436 | ProcedureCondition::Range { column_id, .. }
13437 | ProcedureCondition::RangeF64 { column_id, .. }
13438 | ProcedureCondition::IsNull { column_id }
13439 | ProcedureCondition::IsNotNull { column_id }
13440 | ProcedureCondition::FmContains { column_id, .. } => {
13441 validate_column_id(*column_id, schema)?;
13442 }
13443 }
13444 Ok(())
13445}
13446
13447fn bind_procedure_args(
13448 procedure: &StoredProcedure,
13449 mut args: HashMap<String, crate::Value>,
13450) -> Result<HashMap<String, crate::Value>> {
13451 let mut out = HashMap::new();
13452 for param in &procedure.params {
13453 let value = match args.remove(¶m.name) {
13454 Some(value) => value,
13455 None => param.default.clone().ok_or_else(|| {
13456 MongrelError::InvalidArgument(format!(
13457 "missing required procedure parameter {:?}",
13458 param.name
13459 ))
13460 })?,
13461 };
13462 if !param.nullable && matches!(value, crate::Value::Null) {
13463 return Err(MongrelError::InvalidArgument(format!(
13464 "procedure parameter {:?} must not be NULL",
13465 param.name
13466 )));
13467 }
13468 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty.clone()) {
13469 return Err(MongrelError::InvalidArgument(format!(
13470 "procedure parameter {:?} has wrong type",
13471 param.name
13472 )));
13473 }
13474 out.insert(param.name.clone(), value);
13475 }
13476 if let Some(extra) = args.keys().next() {
13477 return Err(MongrelError::InvalidArgument(format!(
13478 "unknown procedure parameter {extra:?}"
13479 )));
13480 }
13481 Ok(out)
13482}
13483
13484fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
13485 matches!(
13486 (value, ty),
13487 (crate::Value::Bool(_), crate::TypeId::Bool)
13488 | (crate::Value::Int64(_), crate::TypeId::Int8)
13489 | (crate::Value::Int64(_), crate::TypeId::Int16)
13490 | (crate::Value::Int64(_), crate::TypeId::Int32)
13491 | (crate::Value::Int64(_), crate::TypeId::Int64)
13492 | (crate::Value::Int64(_), crate::TypeId::UInt8)
13493 | (crate::Value::Int64(_), crate::TypeId::UInt16)
13494 | (crate::Value::Int64(_), crate::TypeId::UInt32)
13495 | (crate::Value::Int64(_), crate::TypeId::UInt64)
13496 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
13497 | (crate::Value::Int64(_), crate::TypeId::Date32)
13498 | (crate::Value::Float64(_), crate::TypeId::Float32)
13499 | (crate::Value::Float64(_), crate::TypeId::Float64)
13500 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
13501 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
13502 )
13503}
13504
13505fn eval_cells(
13506 cells: &[crate::procedure::ProcedureCell],
13507 args: &HashMap<String, crate::Value>,
13508 outputs: &HashMap<String, ProcedureCallOutput>,
13509) -> Result<Vec<(u16, crate::Value)>> {
13510 cells
13511 .iter()
13512 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
13513 .collect()
13514}
13515
13516fn eval_condition(
13517 condition: &ProcedureCondition,
13518 args: &HashMap<String, crate::Value>,
13519 outputs: &HashMap<String, ProcedureCallOutput>,
13520) -> Result<crate::Condition> {
13521 Ok(match condition {
13522 ProcedureCondition::Pk { value } => {
13523 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
13524 }
13525 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
13526 column_id: *column_id,
13527 value: eval_value(value, args, outputs)?.encode_key(),
13528 },
13529 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
13530 column_id: *column_id,
13531 values: values
13532 .iter()
13533 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
13534 .collect::<Result<Vec<_>>>()?,
13535 },
13536 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
13537 column_id: *column_id,
13538 lo: expect_i64(eval_value(lo, args, outputs)?)?,
13539 hi: expect_i64(eval_value(hi, args, outputs)?)?,
13540 },
13541 ProcedureCondition::RangeF64 {
13542 column_id,
13543 lo,
13544 lo_inclusive,
13545 hi,
13546 hi_inclusive,
13547 } => crate::Condition::RangeF64 {
13548 column_id: *column_id,
13549 lo: expect_f64(eval_value(lo, args, outputs)?)?,
13550 lo_inclusive: *lo_inclusive,
13551 hi: expect_f64(eval_value(hi, args, outputs)?)?,
13552 hi_inclusive: *hi_inclusive,
13553 },
13554 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
13555 column_id: *column_id,
13556 },
13557 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
13558 column_id: *column_id,
13559 },
13560 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
13561 column_id: *column_id,
13562 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
13563 },
13564 })
13565}
13566
13567fn eval_value(
13568 value: &ProcedureValue,
13569 args: &HashMap<String, crate::Value>,
13570 outputs: &HashMap<String, ProcedureCallOutput>,
13571) -> Result<crate::Value> {
13572 match value {
13573 ProcedureValue::Literal(value) => Ok(value.clone()),
13574 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
13575 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
13576 }),
13577 ProcedureValue::StepScalar(id) => match outputs.get(id) {
13578 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
13579 _ => Err(MongrelError::InvalidArgument(format!(
13580 "procedure step {id:?} did not return a scalar"
13581 ))),
13582 },
13583 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
13584 Err(MongrelError::InvalidArgument(
13585 "row-valued procedure reference cannot be used as a scalar".into(),
13586 ))
13587 }
13588 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
13589 "structured procedure value cannot be used as a scalar cell".into(),
13590 )),
13591 }
13592}
13593
13594fn eval_return_output(
13595 value: &ProcedureValue,
13596 args: &HashMap<String, crate::Value>,
13597 outputs: &HashMap<String, ProcedureCallOutput>,
13598) -> Result<ProcedureCallOutput> {
13599 match value {
13600 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
13601 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
13602 args.get(name).cloned().ok_or_else(|| {
13603 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
13604 })?,
13605 )),
13606 ProcedureValue::StepRows(id)
13607 | ProcedureValue::StepRow(id)
13608 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
13609 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
13610 }),
13611 ProcedureValue::Object(fields) => {
13612 let mut out = Vec::with_capacity(fields.len());
13613 for (name, value) in fields {
13614 out.push((name.clone(), eval_return_output(value, args, outputs)?));
13615 }
13616 Ok(ProcedureCallOutput::Object(out))
13617 }
13618 ProcedureValue::Array(values) => {
13619 let mut out = Vec::with_capacity(values.len());
13620 for value in values {
13621 out.push(eval_return_output(value, args, outputs)?);
13622 }
13623 Ok(ProcedureCallOutput::Array(out))
13624 }
13625 }
13626}
13627
13628fn expect_i64(value: crate::Value) -> Result<i64> {
13629 match value {
13630 crate::Value::Int64(value) => Ok(value),
13631 _ => Err(MongrelError::InvalidArgument(
13632 "procedure value must be Int64".into(),
13633 )),
13634 }
13635}
13636
13637fn expect_f64(value: crate::Value) -> Result<f64> {
13638 match value {
13639 crate::Value::Float64(value) => Ok(value),
13640 _ => Err(MongrelError::InvalidArgument(
13641 "procedure value must be Float64".into(),
13642 )),
13643 }
13644}
13645
13646fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
13647 match value {
13648 crate::Value::Bytes(value) => Ok(value),
13649 _ => Err(MongrelError::InvalidArgument(
13650 "procedure value must be Bytes".into(),
13651 )),
13652 }
13653}
13654
13655fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
13656 if schema.columns.iter().any(|c| c.id == column_id) {
13657 Ok(())
13658 } else {
13659 Err(MongrelError::InvalidArgument(format!(
13660 "unknown column id {column_id}"
13661 )))
13662 }
13663}
13664
13665fn trigger_matches_event(
13666 trigger: &StoredTrigger,
13667 event: &WriteEvent,
13668 cat: &Catalog,
13669) -> Result<bool> {
13670 if trigger.event != event.kind {
13671 return Ok(false);
13672 }
13673 let TriggerTarget::Table(target) = &trigger.target else {
13674 return Ok(false);
13675 };
13676 if target != &event.table {
13677 return Ok(false);
13678 }
13679 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
13680 let schema = &cat
13681 .live(target)
13682 .ok_or_else(|| {
13683 MongrelError::InvalidArgument(format!(
13684 "trigger {:?} references unknown table {target:?}",
13685 trigger.name
13686 ))
13687 })?
13688 .schema;
13689 let mut watched = Vec::with_capacity(trigger.update_of.len());
13690 for name in &trigger.update_of {
13691 let col = schema.column(name).ok_or_else(|| {
13692 MongrelError::InvalidArgument(format!(
13693 "trigger {:?} references unknown UPDATE OF column {name:?}",
13694 trigger.name
13695 ))
13696 })?;
13697 watched.push(col.id);
13698 }
13699 if !event
13700 .changed_columns
13701 .iter()
13702 .any(|column_id| watched.contains(column_id))
13703 {
13704 return Ok(false);
13705 }
13706 }
13707 Ok(true)
13708}
13709
13710fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
13711 let mut ids = std::collections::BTreeSet::new();
13712 if let Some(old) = old {
13713 ids.extend(old.columns.keys().copied());
13714 }
13715 if let Some(new) = new {
13716 ids.extend(new.columns.keys().copied());
13717 }
13718 ids.into_iter()
13719 .filter(|id| {
13720 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
13721 })
13722 .collect()
13723}
13724
13725fn eval_trigger_cells(
13726 cells: &[crate::trigger::TriggerCell],
13727 event: &WriteEvent,
13728 selected: Option<&TriggerRowImage>,
13729) -> Result<Vec<(u16, Value)>> {
13730 cells
13731 .iter()
13732 .map(|cell| {
13733 Ok((
13734 cell.column_id,
13735 eval_trigger_value(&cell.value, event, selected)?,
13736 ))
13737 })
13738 .collect()
13739}
13740
13741fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
13742 match expr {
13743 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
13744 Value::Bool(value) => Ok(value),
13745 Value::Null => Ok(false),
13746 other => Err(MongrelError::InvalidArgument(format!(
13747 "trigger WHEN value must be boolean, got {other:?}"
13748 ))),
13749 },
13750 TriggerExpr::Eq { left, right } => Ok(values_equal(
13751 &eval_trigger_value(left, event, None)?,
13752 &eval_trigger_value(right, event, None)?,
13753 )),
13754 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
13755 &eval_trigger_value(left, event, None)?,
13756 &eval_trigger_value(right, event, None)?,
13757 )),
13758 TriggerExpr::Lt { left, right } => match value_order(
13759 &eval_trigger_value(left, event, None)?,
13760 &eval_trigger_value(right, event, None)?,
13761 ) {
13762 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
13763 None => Ok(false),
13764 },
13765 TriggerExpr::Lte { left, right } => match value_order(
13766 &eval_trigger_value(left, event, None)?,
13767 &eval_trigger_value(right, event, None)?,
13768 ) {
13769 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
13770 None => Ok(false),
13771 },
13772 TriggerExpr::Gt { left, right } => match value_order(
13773 &eval_trigger_value(left, event, None)?,
13774 &eval_trigger_value(right, event, None)?,
13775 ) {
13776 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
13777 None => Ok(false),
13778 },
13779 TriggerExpr::Gte { left, right } => match value_order(
13780 &eval_trigger_value(left, event, None)?,
13781 &eval_trigger_value(right, event, None)?,
13782 ) {
13783 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
13784 None => Ok(false),
13785 },
13786 TriggerExpr::IsNull(value) => Ok(matches!(
13787 eval_trigger_value(value, event, None)?,
13788 Value::Null
13789 )),
13790 TriggerExpr::IsNotNull(value) => Ok(!matches!(
13791 eval_trigger_value(value, event, None)?,
13792 Value::Null
13793 )),
13794 TriggerExpr::And { left, right } => {
13795 if !eval_trigger_expr(left, event)? {
13796 Ok(false)
13797 } else {
13798 Ok(eval_trigger_expr(right, event)?)
13799 }
13800 }
13801 TriggerExpr::Or { left, right } => {
13802 if eval_trigger_expr(left, event)? {
13803 Ok(true)
13804 } else {
13805 Ok(eval_trigger_expr(right, event)?)
13806 }
13807 }
13808 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
13809 }
13810}
13811
13812fn eval_trigger_condition(
13813 condition: &TriggerCondition,
13814 event: &WriteEvent,
13815 selected: &TriggerRowImage,
13816 schema: &Schema,
13817) -> Result<bool> {
13818 match condition {
13819 TriggerCondition::Pk { value } => {
13820 let pk = schema.primary_key().ok_or_else(|| {
13821 MongrelError::InvalidArgument(
13822 "trigger condition Pk references a table without a primary key".into(),
13823 )
13824 })?;
13825 let lhs = eval_trigger_value(value, event, Some(selected))?;
13826 Ok(values_equal(
13827 &lhs,
13828 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
13829 ))
13830 }
13831 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
13832 selected.columns.get(column_id).unwrap_or(&Value::Null),
13833 &eval_trigger_value(value, event, Some(selected))?,
13834 )),
13835 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
13836 selected.columns.get(column_id).unwrap_or(&Value::Null),
13837 &eval_trigger_value(value, event, Some(selected))?,
13838 )),
13839 TriggerCondition::Lt { column_id, value } => match value_order(
13840 selected.columns.get(column_id).unwrap_or(&Value::Null),
13841 &eval_trigger_value(value, event, Some(selected))?,
13842 ) {
13843 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
13844 None => Ok(false),
13845 },
13846 TriggerCondition::Lte { column_id, value } => match value_order(
13847 selected.columns.get(column_id).unwrap_or(&Value::Null),
13848 &eval_trigger_value(value, event, Some(selected))?,
13849 ) {
13850 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
13851 None => Ok(false),
13852 },
13853 TriggerCondition::Gt { column_id, value } => match value_order(
13854 selected.columns.get(column_id).unwrap_or(&Value::Null),
13855 &eval_trigger_value(value, event, Some(selected))?,
13856 ) {
13857 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
13858 None => Ok(false),
13859 },
13860 TriggerCondition::Gte { column_id, value } => match value_order(
13861 selected.columns.get(column_id).unwrap_or(&Value::Null),
13862 &eval_trigger_value(value, event, Some(selected))?,
13863 ) {
13864 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
13865 None => Ok(false),
13866 },
13867 TriggerCondition::IsNull { column_id } => Ok(matches!(
13868 selected.columns.get(column_id),
13869 None | Some(Value::Null)
13870 )),
13871 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
13872 selected.columns.get(column_id),
13873 None | Some(Value::Null)
13874 )),
13875 TriggerCondition::And { left, right } => {
13876 if !eval_trigger_condition(left, event, selected, schema)? {
13877 Ok(false)
13878 } else {
13879 Ok(eval_trigger_condition(right, event, selected, schema)?)
13880 }
13881 }
13882 TriggerCondition::Or { left, right } => {
13883 if eval_trigger_condition(left, event, selected, schema)? {
13884 Ok(true)
13885 } else {
13886 Ok(eval_trigger_condition(right, event, selected, schema)?)
13887 }
13888 }
13889 TriggerCondition::Not(condition) => {
13890 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
13891 }
13892 }
13893}
13894
13895fn eval_trigger_value(
13896 value: &TriggerValue,
13897 event: &WriteEvent,
13898 selected: Option<&TriggerRowImage>,
13899) -> Result<Value> {
13900 match value {
13901 TriggerValue::Literal(value) => Ok(value.clone()),
13902 TriggerValue::NewColumn(column_id) => event
13903 .new
13904 .as_ref()
13905 .and_then(|row| row.columns.get(column_id))
13906 .cloned()
13907 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
13908 TriggerValue::OldColumn(column_id) => event
13909 .old
13910 .as_ref()
13911 .and_then(|row| row.columns.get(column_id))
13912 .cloned()
13913 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
13914 TriggerValue::SelectedColumn(column_id) => selected
13915 .and_then(|row| row.columns.get(column_id))
13916 .cloned()
13917 .ok_or_else(|| {
13918 MongrelError::InvalidArgument("SELECTED column is not available".into())
13919 }),
13920 }
13921}
13922
13923fn values_equal(left: &Value, right: &Value) -> bool {
13924 match (left, right) {
13925 (Value::Null, Value::Null) => true,
13926 (Value::Bool(a), Value::Bool(b)) => a == b,
13927 (Value::Int64(a), Value::Int64(b)) => a == b,
13928 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
13929 (Value::Bytes(a), Value::Bytes(b)) => a == b,
13930 (Value::Embedding(a), Value::Embedding(b)) => {
13931 a.len() == b.len()
13932 && a.iter()
13933 .zip(b.iter())
13934 .all(|(a, b)| a.to_bits() == b.to_bits())
13935 }
13936 _ => false,
13937 }
13938}
13939
13940fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
13941 match (left, right) {
13942 (Value::Null, _) | (_, Value::Null) => None,
13943 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
13944 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
13945 (Value::Int64(a), Value::Float64(b)) => {
13948 let af = *a as f64;
13949 Some(af.total_cmp(b))
13950 }
13951 (Value::Float64(a), Value::Int64(b)) => {
13954 let bf = *b as f64;
13955 Some(a.total_cmp(&bf))
13956 }
13957 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
13958 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
13959 (Value::Embedding(_), Value::Embedding(_)) => None,
13960 _ => None,
13961 }
13962}
13963
13964fn trigger_message(value: Value) -> String {
13965 match value {
13966 Value::Null => "NULL".into(),
13967 Value::Bool(value) => value.to_string(),
13968 Value::Int64(value) => value.to_string(),
13969 Value::Float64(value) => value.to_string(),
13970 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
13971 Value::Embedding(value) => format!("{value:?}"),
13972 Value::Decimal(value) => value.to_string(),
13973 Value::Interval {
13974 months,
13975 days,
13976 nanos,
13977 } => format!("{months}m {days}d {nanos}ns"),
13978 Value::Uuid(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
13979 Value::Json(b) => String::from_utf8_lossy(&b).into_owned(),
13980 }
13981}
13982
13983fn validate_trigger_step<'a>(
13984 step: &TriggerStep,
13985 cat: &'a Catalog,
13986 target_schema: &Schema,
13987 event: TriggerEvent,
13988 select_schemas: &mut HashMap<String, &'a Schema>,
13989) -> Result<()> {
13990 match step {
13991 TriggerStep::SetNew { cells } => {
13992 if event == TriggerEvent::Delete {
13993 return Err(MongrelError::InvalidArgument(
13994 "SetNew trigger step is not valid for DELETE triggers".into(),
13995 ));
13996 }
13997 for cell in cells {
13998 validate_column_id(cell.column_id, target_schema)?;
13999 validate_trigger_value(&cell.value, target_schema, event)?;
14000 }
14001 }
14002 TriggerStep::Insert { table, cells } => {
14003 let schema = trigger_write_schema(cat, table, "insert")?;
14004 for cell in cells {
14005 validate_column_id(cell.column_id, schema)?;
14006 validate_trigger_value(&cell.value, target_schema, event)?;
14007 }
14008 }
14009 TriggerStep::UpdateByPk { table, pk, cells } => {
14010 let schema = trigger_write_schema(cat, table, "update")?;
14011 if schema.primary_key().is_none() {
14012 return Err(MongrelError::InvalidArgument(format!(
14013 "trigger update_by_pk references table {table:?} without a primary key"
14014 )));
14015 }
14016 validate_trigger_value(pk, target_schema, event)?;
14017 for cell in cells {
14018 validate_column_id(cell.column_id, schema)?;
14019 validate_trigger_value(&cell.value, target_schema, event)?;
14020 }
14021 }
14022 TriggerStep::DeleteByPk { table, pk } => {
14023 let schema = trigger_write_schema(cat, table, "delete")?;
14024 if schema.primary_key().is_none() {
14025 return Err(MongrelError::InvalidArgument(format!(
14026 "trigger delete_by_pk references table {table:?} without a primary key"
14027 )));
14028 }
14029 validate_trigger_value(pk, target_schema, event)?;
14030 }
14031 TriggerStep::Select {
14032 id,
14033 table,
14034 conditions,
14035 } => {
14036 let schema = trigger_read_schema(cat, table)?;
14037 for condition in conditions {
14038 validate_trigger_condition(condition, schema, target_schema, event)?;
14039 }
14040 if select_schemas.contains_key(id) {
14041 return Err(MongrelError::InvalidArgument(format!(
14042 "duplicate select id {id:?} in trigger program"
14043 )));
14044 }
14045 select_schemas.insert(id.clone(), schema);
14046 }
14047 TriggerStep::Foreach { id, steps } => {
14048 if !select_schemas.contains_key(id) {
14049 return Err(MongrelError::InvalidArgument(format!(
14050 "foreach references unknown select id {id:?}"
14051 )));
14052 }
14053 let mut inner_select_schemas = select_schemas.clone();
14054 for step in steps {
14055 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
14056 }
14057 }
14058 TriggerStep::DeleteWhere { table, conditions } => {
14059 let schema = trigger_write_schema(cat, table, "delete")?;
14060 for condition in conditions {
14061 validate_trigger_condition(condition, schema, target_schema, event)?;
14062 }
14063 }
14064 TriggerStep::UpdateWhere {
14065 table,
14066 conditions,
14067 cells,
14068 } => {
14069 let schema = trigger_write_schema(cat, table, "update")?;
14070 for condition in conditions {
14071 validate_trigger_condition(condition, schema, target_schema, event)?;
14072 }
14073 for cell in cells {
14074 validate_column_id(cell.column_id, schema)?;
14075 validate_trigger_value(&cell.value, target_schema, event)?;
14076 }
14077 }
14078 TriggerStep::Raise { message, .. } => {
14079 validate_trigger_value(message, target_schema, event)?
14080 }
14081 }
14082 Ok(())
14083}
14084
14085fn trigger_validation_error(error: MongrelError) -> MongrelError {
14086 match error {
14087 MongrelError::TriggerValidation(_) => error,
14088 MongrelError::InvalidArgument(message)
14089 | MongrelError::Conflict(message)
14090 | MongrelError::NotFound(message) => MongrelError::TriggerValidation(message),
14091 error => error,
14092 }
14093}
14094
14095fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
14096 if let Some(entry) = cat.live(table) {
14097 return Ok(&entry.schema);
14098 }
14099 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
14100 let allowed = match op {
14101 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
14102 "update" | "delete" => entry.capabilities.writable,
14103 _ => false,
14104 };
14105 if !allowed {
14106 return Err(MongrelError::InvalidArgument(format!(
14107 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
14108 entry.module
14109 )));
14110 }
14111 if !entry.capabilities.transaction_safe {
14112 return Err(MongrelError::InvalidArgument(format!(
14113 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
14114 entry.module
14115 )));
14116 }
14117 return Ok(&entry.declared_schema);
14118 }
14119 Err(MongrelError::InvalidArgument(format!(
14120 "trigger references unknown table {table:?}"
14121 )))
14122}
14123
14124fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
14125 if let Some(entry) = cat.live(table) {
14126 return Ok(&entry.schema);
14127 }
14128 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
14129 if entry.capabilities.trigger_safe {
14130 return Ok(&entry.declared_schema);
14131 }
14132 return Err(MongrelError::InvalidArgument(format!(
14133 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
14134 entry.module
14135 )));
14136 }
14137 Err(MongrelError::InvalidArgument(format!(
14138 "trigger references unknown table {table:?}"
14139 )))
14140}
14141
14142fn validate_trigger_condition(
14143 condition: &TriggerCondition,
14144 schema: &Schema,
14145 target_schema: &Schema,
14146 event: TriggerEvent,
14147) -> Result<()> {
14148 match condition {
14149 TriggerCondition::Pk { value } => {
14150 if schema.primary_key().is_none() {
14151 return Err(MongrelError::InvalidArgument(
14152 "trigger condition Pk references a table without a primary key".into(),
14153 ));
14154 }
14155 validate_trigger_value(value, target_schema, event)
14156 }
14157 TriggerCondition::Eq { column_id, value }
14158 | TriggerCondition::NotEq { column_id, value }
14159 | TriggerCondition::Lt { column_id, value }
14160 | TriggerCondition::Lte { column_id, value }
14161 | TriggerCondition::Gt { column_id, value }
14162 | TriggerCondition::Gte { column_id, value } => {
14163 validate_column_id(*column_id, schema)?;
14164 validate_trigger_value(value, target_schema, event)
14165 }
14166 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
14167 validate_column_id(*column_id, schema)
14168 }
14169 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
14170 validate_trigger_condition(left, schema, target_schema, event)?;
14171 validate_trigger_condition(right, schema, target_schema, event)
14172 }
14173 TriggerCondition::Not(condition) => {
14174 validate_trigger_condition(condition, schema, target_schema, event)
14175 }
14176 }
14177}
14178
14179fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
14180 match expr {
14181 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
14182 validate_trigger_value(value, schema, event)
14183 }
14184 TriggerExpr::Eq { left, right }
14185 | TriggerExpr::NotEq { left, right }
14186 | TriggerExpr::Lt { left, right }
14187 | TriggerExpr::Lte { left, right }
14188 | TriggerExpr::Gt { left, right }
14189 | TriggerExpr::Gte { left, right } => {
14190 validate_trigger_value(left, schema, event)?;
14191 validate_trigger_value(right, schema, event)
14192 }
14193 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
14194 validate_trigger_expr(left, schema, event)?;
14195 validate_trigger_expr(right, schema, event)
14196 }
14197 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
14198 }
14199}
14200
14201fn validate_trigger_value(
14202 value: &TriggerValue,
14203 schema: &Schema,
14204 event: TriggerEvent,
14205) -> Result<()> {
14206 match value {
14207 TriggerValue::Literal(_) => Ok(()),
14208 TriggerValue::NewColumn(id) => {
14209 if event == TriggerEvent::Delete {
14210 return Err(MongrelError::InvalidArgument(
14211 "DELETE triggers cannot reference NEW".into(),
14212 ));
14213 }
14214 validate_column_id(*id, schema)
14215 }
14216 TriggerValue::OldColumn(id) => {
14217 if event == TriggerEvent::Insert {
14218 return Err(MongrelError::InvalidArgument(
14219 "INSERT triggers cannot reference OLD".into(),
14220 ));
14221 }
14222 validate_column_id(*id, schema)
14223 }
14224 TriggerValue::SelectedColumn(_) => Ok(()),
14228 }
14229}
14230
14231fn recover_ddl_from_wal(
14237 root: &Path,
14238 durable_root: Option<&crate::durable_file::DurableRoot>,
14239 target_catalog: &mut Catalog,
14240 meta_dek: Option<&[u8; META_DEK_LEN]>,
14241 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
14242 apply: bool,
14243 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
14244) -> Result<()> {
14245 use crate::wal::SharedWal;
14246 let records = match durable_root {
14247 Some(root) => SharedWal::replay_durable_with_dek(root, wal_dek)?,
14248 None => SharedWal::replay_with_dek(root, wal_dek)?,
14249 };
14250 recover_ddl_from_records(
14251 root,
14252 durable_root,
14253 target_catalog,
14254 meta_dek,
14255 apply,
14256 table_roots,
14257 &records,
14258 )
14259}
14260
14261fn recover_ddl_from_records(
14262 root: &Path,
14263 durable_root: Option<&crate::durable_file::DurableRoot>,
14264 target_catalog: &mut Catalog,
14265 meta_dek: Option<&[u8; META_DEK_LEN]>,
14266 apply: bool,
14267 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
14268 records: &[crate::wal::Record],
14269) -> Result<()> {
14270 use crate::wal::{DdlOp, Op};
14271
14272 let original_catalog = target_catalog.clone();
14273 let mut recovered_catalog = original_catalog.clone();
14274 let cat = &mut recovered_catalog;
14275 let mut created_table_ids = HashSet::<u64>::new();
14276 let mut ttl_updates = HashMap::<u64, (Option<crate::manifest::TtlPolicy>, u64)>::new();
14277
14278 let mut committed: HashMap<u64, u64> = HashMap::new();
14279 for r in records {
14280 if let Op::TxnCommit { epoch: ce, .. } = r.op {
14281 committed.insert(r.txn_id, ce);
14282 }
14283 }
14284 let catalog_snapshot_txns = records
14285 .iter()
14286 .filter_map(|record| {
14287 (committed.contains_key(&record.txn_id)
14288 && matches!(&record.op, Op::Ddl(DdlOp::CatalogSnapshot { .. })))
14289 .then_some(record.txn_id)
14290 })
14291 .collect::<HashSet<_>>();
14292
14293 let mut changed = false;
14294 let mut applied_catalog_epoch = cat.db_epoch;
14295 let max_committed_epoch = committed.values().copied().max().unwrap_or(cat.db_epoch);
14296 for r in records.iter().cloned() {
14297 let Some(&ce) = committed.get(&r.txn_id) else {
14298 continue;
14299 };
14300 let txn_id = r.txn_id;
14301 match r.op {
14302 Op::Ddl(DdlOp::CreateTable {
14303 table_id,
14304 ref name,
14305 ref schema_json,
14306 }) => {
14307 if cat.tables.iter().any(|t| t.table_id == table_id) {
14308 continue;
14309 }
14310 let schema = DdlOp::decode_schema(schema_json)?;
14311 validate_recovered_schema(&schema)?;
14312 created_table_ids.insert(table_id);
14313 cat.tables.push(CatalogEntry {
14314 table_id,
14315 name: name.clone(),
14316 schema,
14317 state: TableState::Live,
14318 created_epoch: ce,
14319 });
14320 cat.next_table_id =
14321 cat.next_table_id
14322 .max(table_id.checked_add(1).ok_or_else(|| {
14323 MongrelError::Full("table id namespace exhausted".into())
14324 })?);
14325 changed = true;
14326 }
14327 Op::Ddl(DdlOp::CreateBuildingTable {
14328 table_id,
14329 ref build_name,
14330 ref intended_name,
14331 ref query_id,
14332 created_at_unix_nanos,
14333 ref schema_json,
14334 }) => {
14335 if cat.tables.iter().any(|table| table.table_id == table_id) {
14336 continue;
14337 }
14338 let schema = DdlOp::decode_schema(schema_json)?;
14339 validate_recovered_schema(&schema)?;
14340 created_table_ids.insert(table_id);
14341 cat.tables.push(CatalogEntry {
14342 table_id,
14343 name: build_name.clone(),
14344 schema,
14345 state: TableState::Building {
14346 intended_name: intended_name.clone(),
14347 query_id: query_id.clone(),
14348 created_at_unix_nanos,
14349 replaces_table_id: None,
14350 },
14351 created_epoch: ce,
14352 });
14353 cat.next_table_id =
14354 cat.next_table_id
14355 .max(table_id.checked_add(1).ok_or_else(|| {
14356 MongrelError::Full("table id namespace exhausted".into())
14357 })?);
14358 changed = true;
14359 }
14360 Op::Ddl(DdlOp::CreateRebuildingTable {
14361 table_id,
14362 ref build_name,
14363 ref intended_name,
14364 ref query_id,
14365 created_at_unix_nanos,
14366 replaces_table_id,
14367 ref schema_json,
14368 }) => {
14369 if cat.tables.iter().any(|table| table.table_id == table_id) {
14370 continue;
14371 }
14372 let schema = DdlOp::decode_schema(schema_json)?;
14373 validate_recovered_schema(&schema)?;
14374 created_table_ids.insert(table_id);
14375 cat.tables.push(CatalogEntry {
14376 table_id,
14377 name: build_name.clone(),
14378 schema,
14379 state: TableState::Building {
14380 intended_name: intended_name.clone(),
14381 query_id: query_id.clone(),
14382 created_at_unix_nanos,
14383 replaces_table_id: Some(replaces_table_id),
14384 },
14385 created_epoch: ce,
14386 });
14387 cat.next_table_id =
14388 cat.next_table_id
14389 .max(table_id.checked_add(1).ok_or_else(|| {
14390 MongrelError::Full("table id namespace exhausted".into())
14391 })?);
14392 changed = true;
14393 }
14394 Op::Ddl(DdlOp::DropTable { table_id }) => {
14395 let mut dropped_name = None;
14396 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
14397 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
14398 dropped_name = Some(entry.name.clone());
14399 entry.state = TableState::Dropped { at_epoch: ce };
14400 changed = true;
14401 }
14402 }
14403 if let Some(name) = dropped_name {
14404 let before = cat.materialized_views.len();
14405 cat.materialized_views
14406 .retain(|definition| definition.name != name);
14407 changed |= before != cat.materialized_views.len();
14408 cat.security.rls_tables.retain(|table| table != &name);
14409 cat.security.policies.retain(|policy| policy.table != name);
14410 cat.security.masks.retain(|mask| mask.table != name);
14411 for role in &mut cat.roles {
14412 role.permissions
14413 .retain(|permission| permission_table(permission) != Some(&name));
14414 }
14415 if !catalog_snapshot_txns.contains(&txn_id) {
14416 advance_security_version(cat)?;
14417 }
14418 }
14419 }
14420 Op::Ddl(DdlOp::PublishBuildingTable {
14421 table_id,
14422 ref new_name,
14423 }) => {
14424 if let Some(entry) = cat
14425 .tables
14426 .iter_mut()
14427 .find(|table| table.table_id == table_id)
14428 {
14429 if entry.name != *new_name || !matches!(entry.state, TableState::Live) {
14430 entry.name = new_name.clone();
14431 entry.state = TableState::Live;
14432 changed = true;
14433 }
14434 }
14435 }
14436 Op::Ddl(DdlOp::ReplaceBuildingTable {
14437 table_id,
14438 replaced_table_id,
14439 ref new_name,
14440 }) => {
14441 changed |=
14442 apply_rebuilding_publish(cat, table_id, replaced_table_id, new_name, ce)?;
14443 }
14444 Op::Ddl(DdlOp::RenameTable {
14445 table_id,
14446 ref new_name,
14447 }) => {
14448 let mut old_name = None;
14449 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
14450 if entry.name != *new_name {
14451 old_name = Some(entry.name.clone());
14452 entry.name = new_name.clone();
14453 changed = true;
14454 }
14455 }
14456 if let Some(old_name) = old_name {
14457 if let Some(definition) = cat
14458 .materialized_views
14459 .iter_mut()
14460 .find(|definition| definition.name == old_name)
14461 {
14462 definition.name = new_name.clone();
14463 }
14464 for table in &mut cat.security.rls_tables {
14465 if *table == old_name {
14466 *table = new_name.clone();
14467 }
14468 }
14469 for policy in &mut cat.security.policies {
14470 if policy.table == old_name {
14471 policy.table = new_name.clone();
14472 }
14473 }
14474 for mask in &mut cat.security.masks {
14475 if mask.table == old_name {
14476 mask.table = new_name.clone();
14477 }
14478 }
14479 for role in &mut cat.roles {
14480 for permission in &mut role.permissions {
14481 rename_permission_table(permission, &old_name, new_name);
14482 }
14483 }
14484 if !catalog_snapshot_txns.contains(&txn_id) {
14485 advance_security_version(cat)?;
14486 }
14487 }
14488 }
14492 Op::Ddl(DdlOp::AlterTable {
14493 table_id,
14494 ref column_json,
14495 }) => {
14496 let column = DdlOp::decode_column(column_json)?;
14497 let mut renamed = None;
14498 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
14499 renamed = entry
14500 .schema
14501 .columns
14502 .iter()
14503 .find(|existing| existing.id == column.id && existing.name != column.name)
14504 .map(|existing| {
14505 (
14506 entry.name.clone(),
14507 existing.name.clone(),
14508 column.name.clone(),
14509 )
14510 });
14511 if apply_recovered_column_def(&mut entry.schema, column)? {
14512 validate_recovered_schema(&entry.schema)?;
14513 changed = true;
14514 }
14515 }
14516 if let Some((table, old_name, new_name)) = renamed {
14517 for role in &mut cat.roles {
14518 for permission in &mut role.permissions {
14519 rename_permission_column(permission, &table, &old_name, &new_name);
14520 }
14521 }
14522 if !catalog_snapshot_txns.contains(&txn_id) {
14523 advance_security_version(cat)?;
14524 }
14525 }
14526 }
14527 Op::Ddl(DdlOp::SetTtl {
14528 table_id,
14529 ref policy_json,
14530 }) => {
14531 let policy = DdlOp::decode_ttl(policy_json)?;
14532 let entry = cat
14533 .tables
14534 .iter()
14535 .find(|entry| entry.table_id == table_id)
14536 .ok_or_else(|| {
14537 MongrelError::Schema(format!(
14538 "recovered TTL references unknown table id {table_id}"
14539 ))
14540 })?;
14541 if let Some(policy) = policy {
14542 let valid = entry
14543 .schema
14544 .columns
14545 .iter()
14546 .find(|column| column.id == policy.column_id)
14547 .is_some_and(|column| {
14548 column.ty == TypeId::TimestampNanos
14549 && policy.duration_nanos > 0
14550 && policy.duration_nanos <= i64::MAX as u64
14551 });
14552 if !valid {
14553 return Err(MongrelError::Schema(format!(
14554 "invalid recovered TTL policy for table id {table_id}"
14555 )));
14556 }
14557 }
14558 ttl_updates.insert(table_id, (policy, ce));
14559 }
14560 Op::Ddl(DdlOp::SetMaterializedView {
14561 ref name,
14562 ref definition_json,
14563 }) => {
14564 let definition = DdlOp::decode_materialized_view(definition_json)?;
14565 if definition.name != *name {
14566 return Err(MongrelError::Schema(format!(
14567 "materialized view WAL name mismatch: {name:?}"
14568 )));
14569 }
14570 if cat.live(name).is_some() {
14571 if let Some(existing) = cat
14572 .materialized_views
14573 .iter_mut()
14574 .find(|existing| existing.name == *name)
14575 {
14576 if *existing != definition {
14577 *existing = definition;
14578 changed = true;
14579 }
14580 } else {
14581 cat.materialized_views.push(definition);
14582 changed = true;
14583 }
14584 }
14585 }
14586 Op::Ddl(DdlOp::SetSecurityCatalog { ref security_json }) => {
14587 let security = DdlOp::decode_security(security_json)?;
14588 validate_security_catalog(cat, &security)?;
14589 if cat.security != security {
14590 cat.security = security;
14591 if !catalog_snapshot_txns.contains(&txn_id) {
14592 advance_security_version(cat)?;
14593 }
14594 changed = true;
14595 }
14596 }
14597 Op::Ddl(DdlOp::SetSqlPragma { ref key, value }) => {
14598 let target = match key.as_str() {
14599 "user_version" => &mut cat.user_version,
14600 "application_id" => &mut cat.application_id,
14601 _ => {
14602 return Err(MongrelError::InvalidArgument(format!(
14603 "unsupported recovered SQL pragma {key:?}"
14604 )))
14605 }
14606 };
14607 if *target != Some(value) {
14608 *target = Some(value);
14609 cat.db_epoch = cat.db_epoch.max(ce);
14610 changed = true;
14611 }
14612 }
14613 Op::Ddl(DdlOp::CatalogSnapshot { ref catalog_json }) => {
14614 if ce <= applied_catalog_epoch {
14615 continue;
14616 }
14617 let snapshot = DdlOp::decode_catalog(catalog_json)?;
14618 if snapshot.db_epoch != ce {
14619 return Err(MongrelError::Schema(format!(
14620 "catalog snapshot epoch {} does not match WAL commit epoch {ce}",
14621 snapshot.db_epoch
14622 )));
14623 }
14624 validate_recovered_catalog(&snapshot)?;
14625 validate_catalog_transition(cat, &snapshot)?;
14626 *cat = snapshot;
14627 applied_catalog_epoch = ce;
14628 changed = true;
14629 }
14630 _ => {}
14631 }
14632 }
14633
14634 if cat.db_epoch < max_committed_epoch {
14635 cat.db_epoch = max_committed_epoch;
14636 changed = true;
14637 }
14638 changed |= repair_catalog_allocator_counters(cat)?;
14639
14640 validate_recovered_catalog(cat)?;
14641 let storage_reconciliation = validate_recovered_storage_plan(
14642 root,
14643 durable_root,
14644 cat,
14645 &created_table_ids,
14646 &ttl_updates,
14647 meta_dek,
14648 )?;
14649
14650 let needs_storage_apply = !storage_reconciliation.is_empty() || !ttl_updates.is_empty();
14651 if apply && (changed || needs_storage_apply) {
14652 for table_id in storage_reconciliation {
14653 let entry = cat
14654 .tables
14655 .iter()
14656 .find(|entry| entry.table_id == table_id)
14657 .ok_or_else(|| MongrelError::CorruptWal {
14658 offset: table_id,
14659 reason: "recovery storage plan lost its catalog table".into(),
14660 })?;
14661 ensure_recovered_table_storage(
14662 table_roots
14663 .and_then(|roots| roots.get(&table_id))
14664 .map(Arc::as_ref),
14665 durable_root,
14666 &root.join(TABLES_DIR).join(table_id.to_string()),
14667 table_id,
14668 &entry.schema,
14669 meta_dek,
14670 )?;
14671 }
14672 for (table_id, (policy, ttl_epoch)) in ttl_updates {
14673 let Some(entry) = cat.tables.iter().find(|entry| {
14674 entry.table_id == table_id
14675 && matches!(entry.state, TableState::Live | TableState::Building { .. })
14676 }) else {
14677 continue;
14678 };
14679 let table_root = if let Some(root) = table_roots.and_then(|roots| roots.get(&table_id))
14680 {
14681 root.try_clone()?
14682 } else if let Some(root) = durable_root {
14683 root.open_directory(Path::new(TABLES_DIR).join(table_id.to_string()))?
14684 } else {
14685 crate::durable_file::DurableRoot::open(
14686 root.join(TABLES_DIR).join(table_id.to_string()),
14687 )?
14688 };
14689 let table_dir = table_root.io_path()?;
14690 let mut manifest = crate::manifest::read_durable(&table_root, "", meta_dek)?;
14691 if manifest.ttl != policy || manifest.current_epoch < ttl_epoch {
14692 manifest.ttl = policy;
14693 manifest.current_epoch = manifest.current_epoch.max(ttl_epoch);
14694 manifest.schema_id = entry.schema.schema_id;
14695 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
14696 }
14697 }
14698 if changed {
14699 match durable_root {
14700 Some(root) => catalog::write_durable(root, cat, meta_dek)?,
14701 None => catalog::write_atomic(root, cat, meta_dek)?,
14702 }
14703 }
14704 }
14705 *target_catalog = recovered_catalog;
14706 Ok(())
14707}
14708
14709fn ensure_recovered_table_storage(
14710 pinned_table: Option<&crate::durable_file::DurableRoot>,
14711 durable_root: Option<&crate::durable_file::DurableRoot>,
14712 fallback_table_dir: &Path,
14713 table_id: u64,
14714 schema: &Schema,
14715 meta_dek: Option<&[u8; META_DEK_LEN]>,
14716) -> Result<()> {
14717 let table_root = if let Some(root) = pinned_table {
14718 root.try_clone()?
14719 } else if let Some(root) = durable_root {
14720 let relative = Path::new(TABLES_DIR).join(table_id.to_string());
14721 match root.open_directory(&relative) {
14722 Ok(table) => table,
14723 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
14724 root.create_directory_all_pinned(relative)?
14725 }
14726 Err(error) => return Err(error.into()),
14727 }
14728 } else {
14729 crate::durable_file::create_directory_all(fallback_table_dir)?;
14730 crate::durable_file::DurableRoot::open(fallback_table_dir)?
14731 };
14732 let table_dir = table_root.io_path()?;
14733 let mut existing_manifest = match crate::manifest::read_durable(&table_root, "", meta_dek) {
14734 Ok(manifest) => {
14735 if manifest.table_id != table_id {
14736 return Err(MongrelError::Conflict(format!(
14737 "recovered table directory id mismatch: expected {table_id}, found {}",
14738 manifest.table_id
14739 )));
14740 }
14741 Some(manifest)
14742 }
14743 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
14744 Err(error) => return Err(error),
14745 };
14746
14747 table_root.create_directory_all(crate::engine::WAL_DIR)?;
14748 table_root.create_directory_all(crate::engine::RUNS_DIR)?;
14749 crate::engine::write_schema(&table_dir, schema)?;
14750
14751 if let Some(mut manifest) = existing_manifest.take() {
14752 if manifest.schema_id != schema.schema_id {
14753 manifest.schema_id = schema.schema_id;
14754 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
14755 }
14756 } else {
14757 let mut manifest = crate::manifest::Manifest::new(table_id, schema.schema_id);
14759 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
14760 }
14761 Ok(())
14762}
14763
14764fn validate_recovered_schema(schema: &Schema) -> Result<()> {
14765 schema.validate_auto_increment()?;
14766 schema.validate_defaults()?;
14767 schema.validate_ai()?;
14768 let mut column_ids = HashSet::new();
14769 let mut column_names = HashSet::new();
14770 for column in &schema.columns {
14771 if !column_ids.insert(column.id) || !column_names.insert(column.name.as_str()) {
14772 return Err(MongrelError::Schema(
14773 "recovered schema contains duplicate columns".into(),
14774 ));
14775 }
14776 match &column.ty {
14777 TypeId::Decimal128 { precision, scale }
14778 if *precision == 0 || *precision > 38 || scale.unsigned_abs() > *precision =>
14779 {
14780 return Err(MongrelError::Schema(format!(
14781 "column {:?} has invalid decimal precision or scale",
14782 column.name
14783 )));
14784 }
14785 TypeId::Enum { variants }
14786 if variants.is_empty()
14787 || variants.iter().any(String::is_empty)
14788 || variants.iter().collect::<HashSet<_>>().len() != variants.len() =>
14789 {
14790 return Err(MongrelError::Schema(format!(
14791 "column {:?} has invalid enum variants",
14792 column.name
14793 )));
14794 }
14795 _ => {}
14796 }
14797 }
14798 let mut index_names = HashSet::new();
14799 for index in &schema.indexes {
14800 index.validate_options()?;
14801 if index.name.is_empty()
14802 || !index_names.insert(index.name.as_str())
14803 || schema
14804 .columns
14805 .iter()
14806 .all(|column| column.id != index.column_id)
14807 {
14808 return Err(MongrelError::Schema(format!(
14809 "recovered index {:?} references missing column {}",
14810 index.name, index.column_id
14811 )));
14812 }
14813 }
14814 let mut colocated = HashSet::new();
14815 for group in &schema.colocation {
14816 if group.is_empty()
14817 || group.iter().any(|id| !column_ids.contains(id))
14818 || group.iter().any(|id| !colocated.insert(*id))
14819 {
14820 return Err(MongrelError::Schema(
14821 "recovered schema contains invalid column co-location groups".into(),
14822 ));
14823 }
14824 }
14825
14826 let mut constraint_ids = HashSet::new();
14827 let mut constraint_names = HashSet::<String>::new();
14828 let mut validate_constraint_identity = |id: u16, name: &str| -> Result<()> {
14829 if name.is_empty()
14830 || !constraint_ids.insert(id)
14831 || !constraint_names.insert(name.to_owned())
14832 {
14833 return Err(MongrelError::Schema(
14834 "recovered schema contains duplicate or empty constraint identities".into(),
14835 ));
14836 }
14837 Ok(())
14838 };
14839 for unique in &schema.constraints.uniques {
14840 validate_constraint_identity(unique.id, &unique.name)?;
14841 if unique.columns.is_empty()
14842 || unique.columns.iter().any(|id| !column_ids.contains(id))
14843 || unique.columns.iter().collect::<HashSet<_>>().len() != unique.columns.len()
14844 {
14845 return Err(MongrelError::Schema(format!(
14846 "unique constraint {:?} has invalid columns",
14847 unique.name
14848 )));
14849 }
14850 }
14851 for foreign_key in &schema.constraints.foreign_keys {
14852 validate_constraint_identity(foreign_key.id, &foreign_key.name)?;
14853 if foreign_key.ref_table.is_empty()
14854 || foreign_key.columns.is_empty()
14855 || foreign_key.columns.len() != foreign_key.ref_columns.len()
14856 || foreign_key
14857 .columns
14858 .iter()
14859 .any(|id| !column_ids.contains(id))
14860 || foreign_key.columns.iter().collect::<HashSet<_>>().len() != foreign_key.columns.len()
14861 || foreign_key.ref_columns.iter().collect::<HashSet<_>>().len()
14862 != foreign_key.ref_columns.len()
14863 {
14864 return Err(MongrelError::Schema(format!(
14865 "foreign key {:?} has invalid columns",
14866 foreign_key.name
14867 )));
14868 }
14869 if (matches!(foreign_key.on_delete, crate::constraint::FkAction::SetNull)
14870 || matches!(foreign_key.on_update, crate::constraint::FkAction::SetNull))
14871 && foreign_key.columns.iter().any(|id| {
14872 schema
14873 .columns
14874 .iter()
14875 .find(|column| column.id == *id)
14876 .is_none_or(|column| {
14877 !column.flags.contains(crate::schema::ColumnFlags::NULLABLE)
14878 })
14879 })
14880 {
14881 return Err(MongrelError::Schema(format!(
14882 "foreign key {:?} uses SET NULL on a non-nullable column",
14883 foreign_key.name
14884 )));
14885 }
14886 }
14887 for check in &schema.constraints.checks {
14888 validate_constraint_identity(check.id, &check.name)?;
14889 check.expr.validate()?;
14890 validate_check_columns(&check.expr, &column_ids)?;
14891 }
14892 Ok(())
14893}
14894
14895fn validate_check_columns(
14896 expression: &crate::constraint::CheckExpr,
14897 column_ids: &HashSet<u16>,
14898) -> Result<()> {
14899 use crate::constraint::CheckExpr;
14900 match expression {
14901 CheckExpr::Col(id) | CheckExpr::IsNull(id) | CheckExpr::IsNotNull(id) => {
14902 if column_ids.contains(id) {
14903 Ok(())
14904 } else {
14905 Err(MongrelError::Schema(format!(
14906 "check constraint references unknown column {id}"
14907 )))
14908 }
14909 }
14910 CheckExpr::Regex { col, .. } => {
14911 if column_ids.contains(col) {
14912 Ok(())
14913 } else {
14914 Err(MongrelError::Schema(format!(
14915 "check constraint references unknown column {col}"
14916 )))
14917 }
14918 }
14919 CheckExpr::Add(left, right)
14920 | CheckExpr::Sub(left, right)
14921 | CheckExpr::Mul(left, right)
14922 | CheckExpr::Div(left, right)
14923 | CheckExpr::Mod(left, right)
14924 | CheckExpr::Eq(left, right)
14925 | CheckExpr::Ne(left, right)
14926 | CheckExpr::Lt(left, right)
14927 | CheckExpr::Le(left, right)
14928 | CheckExpr::Gt(left, right)
14929 | CheckExpr::Ge(left, right)
14930 | CheckExpr::And(left, right)
14931 | CheckExpr::Or(left, right) => {
14932 validate_check_columns(left, column_ids)?;
14933 validate_check_columns(right, column_ids)
14934 }
14935 CheckExpr::Not(inner) => validate_check_columns(inner, column_ids),
14936 CheckExpr::True | CheckExpr::Lit(_) => Ok(()),
14937 }
14938}
14939
14940fn validate_catalog_transition(current: &Catalog, next: &Catalog) -> Result<()> {
14941 for (name, prior, candidate) in [
14942 ("db_epoch", current.db_epoch, next.db_epoch),
14943 ("next_table_id", current.next_table_id, next.next_table_id),
14944 (
14945 "next_segment_no",
14946 current.next_segment_no,
14947 next.next_segment_no,
14948 ),
14949 ("next_user_id", current.next_user_id, next.next_user_id),
14950 (
14951 "security_version",
14952 current.security_version,
14953 next.security_version,
14954 ),
14955 ] {
14956 if candidate < prior {
14957 return Err(MongrelError::Schema(format!(
14958 "catalog snapshot rolls back {name} from {prior} to {candidate}"
14959 )));
14960 }
14961 }
14962 for prior in ¤t.tables {
14963 let Some(candidate) = next
14964 .tables
14965 .iter()
14966 .find(|entry| entry.table_id == prior.table_id)
14967 else {
14968 return Err(MongrelError::Schema(format!(
14969 "catalog snapshot removes table identity {}",
14970 prior.table_id
14971 )));
14972 };
14973 if candidate.created_epoch != prior.created_epoch
14974 || candidate.schema.schema_id < prior.schema.schema_id
14975 || matches!(prior.state, TableState::Dropped { .. })
14976 && !matches!(candidate.state, TableState::Dropped { .. })
14977 {
14978 return Err(MongrelError::Schema(format!(
14979 "catalog snapshot rolls back table identity {}",
14980 prior.table_id
14981 )));
14982 }
14983 }
14984 for prior in ¤t.users {
14985 if let Some(candidate) = next.users.iter().find(|user| user.id == prior.id) {
14986 if candidate.username != prior.username
14987 || candidate.created_epoch != prior.created_epoch
14988 {
14989 return Err(MongrelError::Schema(format!(
14990 "catalog snapshot reuses user identity {}",
14991 prior.id
14992 )));
14993 }
14994 }
14995 }
14996 Ok(())
14997}
14998
14999fn validate_recovered_catalog(catalog: &Catalog) -> Result<()> {
15000 let mut table_ids = HashSet::new();
15001 let mut active_names = HashSet::new();
15002 let mut max_table_id = None::<u64>;
15003 for entry in &catalog.tables {
15004 if !table_ids.insert(entry.table_id) {
15005 return Err(MongrelError::Schema(format!(
15006 "catalog contains duplicate table id {}",
15007 entry.table_id
15008 )));
15009 }
15010 max_table_id = Some(max_table_id.map_or(entry.table_id, |value| value.max(entry.table_id)));
15011 if entry.name.is_empty() || entry.created_epoch > catalog.db_epoch {
15012 return Err(MongrelError::Schema(format!(
15013 "catalog table {} has invalid name or creation epoch",
15014 entry.table_id
15015 )));
15016 }
15017 validate_recovered_schema(&entry.schema)?;
15018 match &entry.state {
15019 TableState::Live => {
15020 if !active_names.insert(entry.name.as_str()) {
15021 return Err(MongrelError::Schema(format!(
15022 "catalog contains duplicate active table name {:?}",
15023 entry.name
15024 )));
15025 }
15026 }
15027 TableState::Dropped { at_epoch } => {
15028 if *at_epoch < entry.created_epoch || *at_epoch > catalog.db_epoch {
15029 return Err(MongrelError::Schema(format!(
15030 "catalog table {} has invalid drop epoch {at_epoch}",
15031 entry.table_id
15032 )));
15033 }
15034 }
15035 TableState::Building {
15036 intended_name,
15037 query_id,
15038 replaces_table_id,
15039 ..
15040 } => {
15041 if intended_name.is_empty() || query_id.is_empty() {
15042 return Err(MongrelError::Schema(format!(
15043 "building table {} has empty identity fields",
15044 entry.table_id
15045 )));
15046 }
15047 if !active_names.insert(entry.name.as_str()) {
15048 return Err(MongrelError::Schema(format!(
15049 "catalog contains duplicate active/building table name {:?}",
15050 entry.name
15051 )));
15052 }
15053 if replaces_table_id.is_some_and(|id| id == entry.table_id) {
15054 return Err(MongrelError::Schema(
15055 "building table cannot replace itself".into(),
15056 ));
15057 }
15058 }
15059 }
15060 }
15061 if let Some(maximum) = max_table_id {
15062 let required = maximum
15063 .checked_add(1)
15064 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
15065 if catalog.next_table_id < required {
15066 return Err(MongrelError::Schema(format!(
15067 "catalog next_table_id {} precedes required {required}",
15068 catalog.next_table_id
15069 )));
15070 }
15071 }
15072 for entry in &catalog.tables {
15073 if let TableState::Building {
15074 replaces_table_id: Some(replaced),
15075 ..
15076 } = entry.state
15077 {
15078 if !table_ids.contains(&replaced) {
15079 return Err(MongrelError::Schema(format!(
15080 "building table {} replaces unknown table {replaced}",
15081 entry.table_id
15082 )));
15083 }
15084 }
15085 }
15086 for entry in &catalog.tables {
15087 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
15088 validate_foreign_key_targets(catalog, &entry.schema)?;
15089 }
15090 }
15091
15092 let mut external_names = HashSet::new();
15093 for entry in &catalog.external_tables {
15094 entry.validate()?;
15095 validate_recovered_schema(&entry.declared_schema)?;
15096 if !entry.declared_schema.constraints.is_empty() {
15097 return Err(MongrelError::Schema(format!(
15098 "external table {:?} cannot carry engine-enforced constraints",
15099 entry.name
15100 )));
15101 }
15102 if entry.created_epoch > catalog.db_epoch
15103 || !external_names.insert(entry.name.as_str())
15104 || active_names.contains(entry.name.as_str())
15105 {
15106 return Err(MongrelError::Schema(format!(
15107 "invalid or duplicate external table {:?}",
15108 entry.name
15109 )));
15110 }
15111 }
15112
15113 let mut procedure_names = HashSet::new();
15114 for entry in &catalog.procedures {
15115 entry.procedure.validate()?;
15116 if entry.procedure.created_epoch > entry.procedure.updated_epoch
15117 || entry.procedure.updated_epoch > catalog.db_epoch
15118 || !procedure_names.insert(entry.procedure.name.as_str())
15119 {
15120 return Err(MongrelError::Schema(format!(
15121 "invalid or duplicate procedure {:?}",
15122 entry.procedure.name
15123 )));
15124 }
15125 validate_recovered_procedure_references(catalog, &entry.procedure)?;
15126 }
15127
15128 let mut trigger_names = HashSet::new();
15129 for entry in &catalog.triggers {
15130 entry.trigger.validate()?;
15131 if entry.trigger.created_epoch > entry.trigger.updated_epoch
15132 || entry.trigger.updated_epoch > catalog.db_epoch
15133 || !trigger_names.insert(entry.trigger.name.as_str())
15134 {
15135 return Err(MongrelError::Schema(format!(
15136 "invalid or duplicate trigger {:?}",
15137 entry.trigger.name
15138 )));
15139 }
15140 validate_recovered_trigger_references(catalog, &entry.trigger)?;
15141 }
15142
15143 let mut views = HashSet::new();
15144 for view in &catalog.materialized_views {
15145 let target = catalog.live(&view.name).ok_or_else(|| {
15146 MongrelError::Schema(format!(
15147 "materialized view {:?} has no live table",
15148 view.name
15149 ))
15150 })?;
15151 if view.name.is_empty()
15152 || view.query.trim().is_empty()
15153 || view.last_refresh_epoch > catalog.db_epoch
15154 || !views.insert(view.name.as_str())
15155 {
15156 return Err(MongrelError::Schema(format!(
15157 "materialized view {:?} has no unique live table",
15158 view.name
15159 )));
15160 }
15161 if let Some(incremental) = &view.incremental {
15162 let source = catalog.live(&incremental.source_table).ok_or_else(|| {
15163 MongrelError::Schema(format!(
15164 "materialized view {:?} references missing source {:?}",
15165 view.name, incremental.source_table
15166 ))
15167 })?;
15168 if source.table_id != incremental.source_table_id
15169 || source
15170 .schema
15171 .columns
15172 .iter()
15173 .all(|column| column.id != incremental.group_column)
15174 {
15175 return Err(MongrelError::Schema(format!(
15176 "materialized view {:?} has invalid incremental source",
15177 view.name
15178 )));
15179 }
15180 let target_ids = target
15181 .schema
15182 .columns
15183 .iter()
15184 .map(|column| column.id)
15185 .collect::<HashSet<_>>();
15186 let mut output_ids = HashSet::new();
15187 let count_outputs = incremental
15188 .outputs
15189 .iter()
15190 .filter(|output| {
15191 matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
15192 })
15193 .count();
15194 if incremental.checkpoint_event_id.is_empty()
15195 || !target_ids.contains(&incremental.group_output_column)
15196 || !target_ids.contains(&incremental.count_output_column)
15197 || incremental.outputs.is_empty()
15198 || count_outputs != 1
15199 || incremental.outputs.iter().any(|output| {
15200 !target_ids.contains(&output.output_column)
15201 || output.output_column == incremental.group_output_column
15202 || !output_ids.insert(output.output_column)
15203 || matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
15204 && output.output_column != incremental.count_output_column
15205 || match output.kind {
15206 crate::catalog::IncrementalAggregateKind::Sum { source_column } => {
15207 source
15208 .schema
15209 .columns
15210 .iter()
15211 .all(|column| column.id != source_column)
15212 }
15213 crate::catalog::IncrementalAggregateKind::Count => false,
15214 }
15215 })
15216 {
15217 return Err(MongrelError::Schema(format!(
15218 "materialized view {:?} has invalid incremental outputs",
15219 view.name
15220 )));
15221 }
15222 }
15223 }
15224
15225 validate_security_catalog(catalog, &catalog.security)?;
15226 validate_recovered_auth_catalog(catalog)?;
15227 Ok(())
15228}
15229
15230fn repair_catalog_allocator_counters(catalog: &mut Catalog) -> Result<bool> {
15231 let mut changed = false;
15232 if let Some(maximum) = catalog.tables.iter().map(|entry| entry.table_id).max() {
15233 let required = maximum
15234 .checked_add(1)
15235 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
15236 if catalog.next_table_id < required {
15237 catalog.next_table_id = required;
15238 changed = true;
15239 }
15240 }
15241 if let Some(maximum) = catalog.users.iter().map(|user| user.id).max() {
15242 let required = maximum
15243 .checked_add(1)
15244 .ok_or_else(|| MongrelError::Full("user id namespace exhausted".into()))?;
15245 if catalog.next_user_id < required {
15246 catalog.next_user_id = required;
15247 changed = true;
15248 }
15249 }
15250 Ok(changed)
15251}
15252
15253fn validate_foreign_key_targets(catalog: &Catalog, schema: &Schema) -> Result<()> {
15254 for foreign_key in &schema.constraints.foreign_keys {
15255 let parent = catalog.live(&foreign_key.ref_table).ok_or_else(|| {
15256 MongrelError::Schema(format!(
15257 "foreign key {:?} references unknown live table {:?}",
15258 foreign_key.name, foreign_key.ref_table
15259 ))
15260 })?;
15261 let referenced_unique = parent
15262 .schema
15263 .constraints
15264 .uniques
15265 .iter()
15266 .any(|unique| unique.columns == foreign_key.ref_columns)
15267 || foreign_key.ref_columns.len() == 1
15268 && parent
15269 .schema
15270 .primary_key()
15271 .is_some_and(|column| column.id == foreign_key.ref_columns[0]);
15272 if !referenced_unique {
15273 return Err(MongrelError::Schema(format!(
15274 "foreign key {:?} does not reference a unique key",
15275 foreign_key.name
15276 )));
15277 }
15278 for (local_id, parent_id) in foreign_key.columns.iter().zip(&foreign_key.ref_columns) {
15279 let local = schema.columns.iter().find(|column| column.id == *local_id);
15280 let referenced = parent
15281 .schema
15282 .columns
15283 .iter()
15284 .find(|column| column.id == *parent_id);
15285 if local
15286 .zip(referenced)
15287 .is_none_or(|(local, referenced)| local.ty != referenced.ty)
15288 {
15289 return Err(MongrelError::Schema(format!(
15290 "foreign key {:?} has missing or incompatible columns",
15291 foreign_key.name
15292 )));
15293 }
15294 }
15295 }
15296 Ok(())
15297}
15298
15299fn validate_recovered_procedure_references(
15300 catalog: &Catalog,
15301 procedure: &StoredProcedure,
15302) -> Result<()> {
15303 for step in &procedure.body.steps {
15304 let Some(table_name) = step.table() else {
15305 continue;
15306 };
15307 let schema = &catalog
15308 .live(table_name)
15309 .ok_or_else(|| {
15310 MongrelError::Schema(format!(
15311 "procedure {:?} references unknown table {table_name:?}",
15312 procedure.name
15313 ))
15314 })?
15315 .schema;
15316 match step {
15317 ProcedureStep::NativeQuery {
15318 conditions,
15319 projection,
15320 ..
15321 } => {
15322 for condition in conditions {
15323 validate_condition_columns(condition, schema)?;
15324 }
15325 for id in projection.iter().flatten() {
15326 validate_column_id(*id, schema)?;
15327 }
15328 }
15329 ProcedureStep::Put { cells, .. } => {
15330 for cell in cells {
15331 validate_column_id(cell.column_id, schema)?;
15332 }
15333 }
15334 ProcedureStep::Upsert {
15335 cells,
15336 update_cells,
15337 ..
15338 } => {
15339 for cell in cells.iter().chain(update_cells.iter().flatten()) {
15340 validate_column_id(cell.column_id, schema)?;
15341 }
15342 }
15343 ProcedureStep::DeleteByPk { .. } if schema.primary_key().is_none() => {
15344 return Err(MongrelError::Schema(format!(
15345 "procedure {:?} deletes by primary key on table without one",
15346 procedure.name
15347 )));
15348 }
15349 ProcedureStep::DeleteByPk { .. }
15350 | ProcedureStep::DeleteRows { .. }
15351 | ProcedureStep::SqlQuery { .. } => {}
15352 }
15353 }
15354 Ok(())
15355}
15356
15357fn validate_recovered_trigger_references(catalog: &Catalog, trigger: &StoredTrigger) -> Result<()> {
15358 let target_schema = match &trigger.target {
15359 TriggerTarget::Table(name) => catalog
15360 .live(name)
15361 .ok_or_else(|| {
15362 MongrelError::Schema(format!(
15363 "trigger {:?} references unknown table {name:?}",
15364 trigger.name
15365 ))
15366 })?
15367 .schema
15368 .clone(),
15369 TriggerTarget::View(_) => Schema {
15370 columns: trigger.target_columns.clone(),
15371 ..Schema::default()
15372 },
15373 };
15374 for column in &trigger.update_of {
15375 if target_schema.column(column).is_none() {
15376 return Err(MongrelError::Schema(format!(
15377 "trigger {:?} references unknown UPDATE OF column {column:?}",
15378 trigger.name
15379 )));
15380 }
15381 }
15382 if let Some(expr) = &trigger.when {
15383 validate_trigger_expr(expr, &target_schema, trigger.event)?;
15384 }
15385 let mut selects = HashMap::new();
15386 for step in &trigger.program.steps {
15387 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before {
15388 return Err(MongrelError::Schema(
15389 "SetNew is only valid in BEFORE triggers".into(),
15390 ));
15391 }
15392 validate_trigger_step(step, catalog, &target_schema, trigger.event, &mut selects)?;
15393 }
15394 Ok(())
15395}
15396
15397fn validate_recovered_auth_catalog(catalog: &Catalog) -> Result<()> {
15398 let mut role_names = HashSet::new();
15399 for role in &catalog.roles {
15400 if role.name.is_empty()
15401 || role.created_epoch > catalog.db_epoch
15402 || !role_names.insert(role.name.as_str())
15403 {
15404 return Err(MongrelError::Schema(format!(
15405 "invalid or duplicate role {:?}",
15406 role.name
15407 )));
15408 }
15409 for permission in &role.permissions {
15410 if let Some(table) = permission_table(permission) {
15411 let schema = catalog
15412 .live(table)
15413 .map(|entry| &entry.schema)
15414 .or_else(|| {
15415 catalog
15416 .external_tables
15417 .iter()
15418 .find(|entry| entry.name == table)
15419 .map(|entry| &entry.declared_schema)
15420 })
15421 .ok_or_else(|| {
15422 MongrelError::Schema(format!(
15423 "role {:?} references unknown table {table:?}",
15424 role.name
15425 ))
15426 })?;
15427 let columns = match permission {
15428 crate::auth::Permission::SelectColumns { columns, .. }
15429 | crate::auth::Permission::InsertColumns { columns, .. }
15430 | crate::auth::Permission::UpdateColumns { columns, .. } => Some(columns),
15431 _ => None,
15432 };
15433 if columns.is_some_and(|columns| {
15434 columns.is_empty()
15435 || columns.iter().any(|column| schema.column(column).is_none())
15436 }) {
15437 return Err(MongrelError::Schema(format!(
15438 "role {:?} contains invalid column permissions",
15439 role.name
15440 )));
15441 }
15442 }
15443 }
15444 }
15445 let mut user_ids = HashSet::new();
15446 let mut usernames = HashSet::new();
15447 let mut maximum_user_id = 0;
15448 for user in &catalog.users {
15449 maximum_user_id = maximum_user_id.max(user.id);
15450 if user.id == 0
15451 || user.username.is_empty()
15452 || user.password_hash.is_empty()
15453 || user.created_epoch > catalog.db_epoch
15454 || !user_ids.insert(user.id)
15455 || !usernames.insert(user.username.as_str())
15456 || user
15457 .roles
15458 .iter()
15459 .any(|role| !role_names.contains(role.as_str()))
15460 {
15461 return Err(MongrelError::Schema(format!(
15462 "invalid or duplicate user {:?}",
15463 user.username
15464 )));
15465 }
15466 }
15467 if !catalog.users.is_empty() && catalog.next_user_id <= maximum_user_id {
15468 return Err(MongrelError::Schema(
15469 "catalog next_user_id does not advance beyond existing user ids".into(),
15470 ));
15471 }
15472 if catalog.require_auth && !catalog.users.iter().any(|user| user.is_admin) {
15473 return Err(MongrelError::Schema(
15474 "authenticated catalog has no administrator".into(),
15475 ));
15476 }
15477 Ok(())
15478}
15479
15480fn validate_recovered_storage_plan(
15481 root: &Path,
15482 durable_root: Option<&crate::durable_file::DurableRoot>,
15483 catalog: &Catalog,
15484 created_table_ids: &HashSet<u64>,
15485 ttl_updates: &HashMap<u64, (Option<crate::manifest::TtlPolicy>, u64)>,
15486 meta_dek: Option<&[u8; META_DEK_LEN]>,
15487) -> Result<Vec<u64>> {
15488 const MAX_SCHEMA_BYTES: u64 = 16 * 1024 * 1024;
15489 let mut reconcile = Vec::new();
15490 for entry in &catalog.tables {
15491 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
15492 continue;
15493 }
15494 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
15495 let table_dir = root.join(TABLES_DIR).join(entry.table_id.to_string());
15496 let table_exists = match durable_root {
15497 Some(root) => match root.open_directory(&relative_dir) {
15498 Ok(_) => true,
15499 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
15500 Err(error) => return Err(error.into()),
15501 },
15502 None => table_dir.is_dir(),
15503 };
15504 if !table_exists {
15505 if created_table_ids.contains(&entry.table_id) {
15506 reconcile.push(entry.table_id);
15507 continue;
15508 }
15509 return Err(MongrelError::NotFound(format!(
15510 "catalog table {} storage is missing",
15511 entry.table_id
15512 )));
15513 }
15514 let manifest_result = match durable_root {
15515 Some(root) => crate::manifest::read_durable(root, &relative_dir, meta_dek),
15516 None => crate::manifest::read(&table_dir, meta_dek),
15517 };
15518 let manifest = match manifest_result {
15519 Ok(manifest) => manifest,
15520 Err(MongrelError::Io(error))
15521 if created_table_ids.contains(&entry.table_id)
15522 && error.kind() == std::io::ErrorKind::NotFound =>
15523 {
15524 reconcile.push(entry.table_id);
15525 continue;
15526 }
15527 Err(error) => return Err(error),
15528 };
15529 if manifest.table_id != entry.table_id {
15530 return Err(MongrelError::Conflict(format!(
15531 "catalog table {} storage identity mismatch",
15532 entry.table_id
15533 )));
15534 }
15535 let schema_result = match durable_root {
15536 Some(root) => root
15537 .open_regular(relative_dir.join(crate::engine::SCHEMA_FILENAME))
15538 .map_err(MongrelError::from),
15539 None => crate::durable_file::open_regular_nofollow(
15540 &table_dir.join(crate::engine::SCHEMA_FILENAME),
15541 ),
15542 };
15543 let file = match schema_result {
15544 Ok(file) => file,
15545 Err(MongrelError::Io(error))
15546 if created_table_ids.contains(&entry.table_id)
15547 && error.kind() == std::io::ErrorKind::NotFound =>
15548 {
15549 reconcile.push(entry.table_id);
15550 continue;
15551 }
15552 Err(error) => return Err(error),
15553 };
15554 let length = file.metadata()?.len();
15555 if length > MAX_SCHEMA_BYTES {
15556 return Err(MongrelError::ResourceLimitExceeded {
15557 resource: "recovered schema bytes",
15558 requested: usize::try_from(length).unwrap_or(usize::MAX),
15559 limit: MAX_SCHEMA_BYTES as usize,
15560 });
15561 }
15562 let disk_schema: Schema = serde_json::from_reader(file.take(MAX_SCHEMA_BYTES + 1))
15563 .map_err(|error| MongrelError::Schema(format!("decode recovered schema: {error}")))?;
15564 if manifest.schema_id != entry.schema.schema_id
15565 || crate::wal::DdlOp::encode_schema(&disk_schema)?
15566 != crate::wal::DdlOp::encode_schema(&entry.schema)?
15567 {
15568 reconcile.push(entry.table_id);
15569 }
15570 }
15571 for table_id in ttl_updates.keys() {
15572 if !catalog.tables.iter().any(|entry| {
15573 entry.table_id == *table_id
15574 && matches!(entry.state, TableState::Live | TableState::Building { .. })
15575 }) {
15576 continue;
15577 }
15578 let relative_dir = Path::new(TABLES_DIR).join(table_id.to_string());
15579 let table_exists = match durable_root {
15580 Some(root) => match root.open_directory(&relative_dir) {
15581 Ok(_) => true,
15582 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
15583 Err(error) => return Err(error.into()),
15584 },
15585 None => root.join(&relative_dir).is_dir(),
15586 };
15587 if !table_exists && !created_table_ids.contains(table_id) {
15588 return Err(MongrelError::NotFound(format!(
15589 "TTL recovery table {table_id} storage is missing"
15590 )));
15591 }
15592 }
15593 reconcile.sort_unstable();
15594 reconcile.dedup();
15595 Ok(reconcile)
15596}
15597
15598fn validate_catalog_table_storage(
15599 root: &crate::durable_file::DurableRoot,
15600 catalog: &Catalog,
15601 meta_dek: Option<&[u8; META_DEK_LEN]>,
15602) -> Result<()> {
15603 for entry in &catalog.tables {
15604 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
15605 continue;
15606 }
15607 let table_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
15608 let manifest = crate::manifest::read_durable(root, &table_dir, meta_dek)?;
15609 if manifest.table_id != entry.table_id || manifest.schema_id != entry.schema.schema_id {
15610 return Err(MongrelError::Conflict(format!(
15611 "catalog table {} storage identity mismatch",
15612 entry.table_id
15613 )));
15614 }
15615 root.open_regular(table_dir.join(crate::engine::SCHEMA_FILENAME))?;
15616 }
15617 Ok(())
15618}
15619
15620fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> Result<bool> {
15621 match schema.columns.iter_mut().find(|c| c.id == column.id) {
15622 Some(existing) if *existing == column => Ok(false),
15623 Some(existing) => {
15624 *existing = column;
15625 schema.schema_id = schema
15626 .schema_id
15627 .checked_add(1)
15628 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
15629 Ok(true)
15630 }
15631 None => {
15632 schema.columns.push(column);
15633 schema.schema_id = schema
15634 .schema_id
15635 .checked_add(1)
15636 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
15637 Ok(true)
15638 }
15639 }
15640}
15641
15642fn permission_table(permission: &crate::auth::Permission) -> Option<&str> {
15643 use crate::auth::Permission;
15644 match permission {
15645 Permission::Select { table }
15646 | Permission::Insert { table }
15647 | Permission::Update { table }
15648 | Permission::Delete { table }
15649 | Permission::SelectColumns { table, .. }
15650 | Permission::InsertColumns { table, .. }
15651 | Permission::UpdateColumns { table, .. } => Some(table),
15652 Permission::All | Permission::Ddl | Permission::Admin => None,
15653 }
15654}
15655
15656fn apply_rebuilding_publish(
15657 catalog: &mut Catalog,
15658 table_id: u64,
15659 replaced_table_id: u64,
15660 new_name: &str,
15661 epoch: u64,
15662) -> Result<bool> {
15663 let already_published = catalog.tables.iter().any(|entry| {
15664 entry.table_id == table_id
15665 && entry.name == new_name
15666 && matches!(entry.state, TableState::Live)
15667 }) && catalog.tables.iter().any(|entry| {
15668 entry.table_id == replaced_table_id && matches!(entry.state, TableState::Dropped { .. })
15669 });
15670 if already_published {
15671 return Ok(false);
15672 }
15673 let schema = catalog
15674 .tables
15675 .iter()
15676 .find(|entry| entry.table_id == table_id)
15677 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?
15678 .schema
15679 .clone();
15680 let replaced = catalog
15681 .tables
15682 .iter_mut()
15683 .find(|entry| entry.table_id == replaced_table_id)
15684 .ok_or_else(|| MongrelError::NotFound(format!("table id {replaced_table_id} not found")))?;
15685 replaced.state = TableState::Dropped { at_epoch: epoch };
15686 let replacement = catalog
15687 .tables
15688 .iter_mut()
15689 .find(|entry| entry.table_id == table_id)
15690 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?;
15691 replacement.name = new_name.to_string();
15692 replacement.state = TableState::Live;
15693
15694 for role in &mut catalog.roles {
15695 role.permissions.retain_mut(|permission| {
15696 retain_rebuilt_permission_columns(permission, new_name, &schema)
15697 });
15698 }
15699 for definition in &mut catalog.materialized_views {
15700 if let Some(incremental) = definition.incremental.as_mut() {
15701 if incremental.source_table == new_name
15702 && incremental.source_table_id == replaced_table_id
15703 {
15704 incremental.source_table_id = table_id;
15705 }
15706 }
15707 }
15708 advance_security_version(catalog)?;
15709 Ok(true)
15710}
15711
15712fn retain_rebuilt_permission_columns(
15713 permission: &mut crate::auth::Permission,
15714 target_table: &str,
15715 schema: &Schema,
15716) -> bool {
15717 use crate::auth::Permission;
15718 let columns = match permission {
15719 Permission::SelectColumns { table, columns }
15720 | Permission::InsertColumns { table, columns }
15721 | Permission::UpdateColumns { table, columns }
15722 if table == target_table =>
15723 {
15724 Some(columns)
15725 }
15726 _ => None,
15727 };
15728 if let Some(columns) = columns {
15729 columns.retain(|column| schema.column(column).is_some());
15730 !columns.is_empty()
15731 } else {
15732 true
15733 }
15734}
15735
15736fn rename_permission_table(permission: &mut crate::auth::Permission, old: &str, new: &str) {
15737 use crate::auth::Permission;
15738 let table = match permission {
15739 Permission::Select { table }
15740 | Permission::Insert { table }
15741 | Permission::Update { table }
15742 | Permission::Delete { table }
15743 | Permission::SelectColumns { table, .. }
15744 | Permission::InsertColumns { table, .. }
15745 | Permission::UpdateColumns { table, .. } => Some(table),
15746 Permission::All | Permission::Ddl | Permission::Admin => None,
15747 };
15748 if let Some(table) = table.filter(|table| table.as_str() == old) {
15749 *table = new.to_string();
15750 }
15751}
15752
15753fn rename_permission_column(
15754 permission: &mut crate::auth::Permission,
15755 target_table: &str,
15756 old: &str,
15757 new: &str,
15758) {
15759 use crate::auth::Permission;
15760 let columns = match permission {
15761 Permission::SelectColumns { table, columns }
15762 | Permission::InsertColumns { table, columns }
15763 | Permission::UpdateColumns { table, columns }
15764 if table == target_table =>
15765 {
15766 Some(columns)
15767 }
15768 _ => None,
15769 };
15770 if let Some(column) = columns
15771 .into_iter()
15772 .flatten()
15773 .find(|column| column.as_str() == old)
15774 {
15775 *column = new.to_string();
15776 }
15777}
15778
15779fn merge_permission(
15780 permissions: &mut Vec<crate::auth::Permission>,
15781 permission: crate::auth::Permission,
15782) {
15783 use crate::auth::Permission;
15784 let (kind, table, mut columns) = match permission {
15785 Permission::SelectColumns { table, columns } => (0, table, columns),
15786 Permission::InsertColumns { table, columns } => (1, table, columns),
15787 Permission::UpdateColumns { table, columns } => (2, table, columns),
15788 permission if !permissions.contains(&permission) => {
15789 permissions.push(permission);
15790 return;
15791 }
15792 _ => return,
15793 };
15794 for permission in permissions.iter_mut() {
15795 let existing = match permission {
15796 Permission::SelectColumns {
15797 table: existing_table,
15798 columns,
15799 } if kind == 0 && existing_table == &table => Some(columns),
15800 Permission::InsertColumns {
15801 table: existing_table,
15802 columns,
15803 } if kind == 1 && existing_table == &table => Some(columns),
15804 Permission::UpdateColumns {
15805 table: existing_table,
15806 columns,
15807 } if kind == 2 && existing_table == &table => Some(columns),
15808 _ => None,
15809 };
15810 if let Some(existing) = existing {
15811 existing.append(&mut columns);
15812 existing.sort();
15813 existing.dedup();
15814 return;
15815 }
15816 }
15817 columns.sort();
15818 columns.dedup();
15819 let permission = if kind == 0 {
15820 Permission::SelectColumns { table, columns }
15821 } else if kind == 1 {
15822 Permission::InsertColumns { table, columns }
15823 } else {
15824 Permission::UpdateColumns { table, columns }
15825 };
15826 permissions.push(permission);
15827}
15828
15829fn revoke_permission_from(
15830 permissions: &mut Vec<crate::auth::Permission>,
15831 revoked: &crate::auth::Permission,
15832) {
15833 use crate::auth::Permission;
15834 let revoked_columns = match revoked {
15835 Permission::SelectColumns { table, columns } => Some((0, table, columns)),
15836 Permission::InsertColumns { table, columns } => Some((1, table, columns)),
15837 Permission::UpdateColumns { table, columns } => Some((2, table, columns)),
15838 _ => None,
15839 };
15840 let Some((kind, table, columns)) = revoked_columns else {
15841 permissions.retain(|permission| permission != revoked);
15842 return;
15843 };
15844 for permission in permissions.iter_mut() {
15845 let current = match permission {
15846 Permission::SelectColumns {
15847 table: current_table,
15848 columns,
15849 } if kind == 0 && current_table == table => Some(columns),
15850 Permission::InsertColumns {
15851 table: current_table,
15852 columns,
15853 } if kind == 1 && current_table == table => Some(columns),
15854 Permission::UpdateColumns {
15855 table: current_table,
15856 columns,
15857 } if kind == 2 && current_table == table => Some(columns),
15858 _ => None,
15859 };
15860 if let Some(current) = current {
15861 current.retain(|column| !columns.contains(column));
15862 }
15863 }
15864 permissions.retain(|permission| match permission {
15865 Permission::SelectColumns { columns, .. }
15866 | Permission::InsertColumns { columns, .. }
15867 | Permission::UpdateColumns { columns, .. } => !columns.is_empty(),
15868 _ => true,
15869 });
15870}
15871
15872fn validate_security_catalog(
15873 catalog: &Catalog,
15874 security: &crate::security::SecurityCatalog,
15875) -> Result<()> {
15876 let mut policy_names = HashSet::new();
15877 for table in &security.rls_tables {
15878 if catalog.live(table).is_none() {
15879 return Err(MongrelError::NotFound(format!(
15880 "RLS table {table:?} not found"
15881 )));
15882 }
15883 }
15884 for policy in &security.policies {
15885 if !policy_names.insert((policy.table.clone(), policy.name.clone())) {
15886 return Err(MongrelError::InvalidArgument(format!(
15887 "duplicate policy {:?} on {:?}",
15888 policy.name, policy.table
15889 )));
15890 }
15891 let schema = &catalog
15892 .live(&policy.table)
15893 .ok_or_else(|| {
15894 MongrelError::NotFound(format!("policy table {:?} not found", policy.table))
15895 })?
15896 .schema;
15897 if let Some(expression) = &policy.using {
15898 validate_security_expression(expression, schema)?;
15899 }
15900 if let Some(expression) = &policy.with_check {
15901 validate_security_expression(expression, schema)?;
15902 }
15903 }
15904 let mut mask_names = HashSet::new();
15905 for mask in &security.masks {
15906 if !mask_names.insert((mask.table.clone(), mask.name.clone())) {
15907 return Err(MongrelError::InvalidArgument(format!(
15908 "duplicate mask {:?} on {:?}",
15909 mask.name, mask.table
15910 )));
15911 }
15912 let column = catalog
15913 .live(&mask.table)
15914 .and_then(|entry| {
15915 entry
15916 .schema
15917 .columns
15918 .iter()
15919 .find(|column| column.id == mask.column)
15920 })
15921 .ok_or_else(|| {
15922 MongrelError::NotFound(format!(
15923 "mask column {} on {:?} not found",
15924 mask.column, mask.table
15925 ))
15926 })?;
15927 if matches!(
15928 mask.strategy,
15929 crate::security::MaskStrategy::Redact { .. } | crate::security::MaskStrategy::Sha256
15930 ) && !matches!(column.ty, TypeId::Bytes | TypeId::Enum { .. })
15931 {
15932 return Err(MongrelError::InvalidArgument(format!(
15933 "mask {:?} requires a string/bytes column",
15934 mask.name
15935 )));
15936 }
15937 }
15938 Ok(())
15939}
15940
15941fn validate_security_expression(
15942 expression: &crate::security::SecurityExpr,
15943 schema: &Schema,
15944) -> Result<()> {
15945 use crate::security::SecurityExpr;
15946 match expression {
15947 SecurityExpr::True => Ok(()),
15948 SecurityExpr::ColumnEqCurrentUser { column }
15949 | SecurityExpr::ColumnEqValue { column, .. } => {
15950 if schema
15951 .columns
15952 .iter()
15953 .any(|candidate| candidate.id == *column)
15954 {
15955 Ok(())
15956 } else {
15957 Err(MongrelError::InvalidArgument(format!(
15958 "security expression references unknown column id {column}"
15959 )))
15960 }
15961 }
15962 SecurityExpr::And { left, right } | SecurityExpr::Or { left, right } => {
15963 validate_security_expression(left, schema)?;
15964 validate_security_expression(right, schema)
15965 }
15966 SecurityExpr::Not { expression } => validate_security_expression(expression, schema),
15967 }
15968}
15969
15970fn sweep_unreferenced_table_dirs(root: &Path, cat: &Catalog) -> Result<()> {
15972 let referenced = cat
15973 .tables
15974 .iter()
15975 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
15976 .map(|entry| entry.table_id)
15977 .collect::<HashSet<_>>();
15978 let tables_dir = root.join(TABLES_DIR);
15979 let entries = match std::fs::read_dir(&tables_dir) {
15980 Ok(entries) => entries,
15981 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
15982 Err(error) => return Err(error.into()),
15983 };
15984 for entry in entries {
15985 let entry = entry?;
15986 if !entry.file_type()?.is_dir() {
15987 continue;
15988 }
15989 let file_name = entry.file_name();
15990 let Some(name) = file_name.to_str() else {
15991 continue;
15992 };
15993 let Ok(table_id) = name.parse::<u64>() else {
15994 continue;
15995 };
15996 if name != table_id.to_string() {
15997 continue;
15998 }
15999 if !referenced.contains(&table_id) {
16000 crate::durable_file::remove_directory_all(&entry.path())?;
16001 }
16002 }
16003 Ok(())
16004}
16005
16006fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
16011 for entry in &cat.tables {
16012 let txn_dir = root
16013 .join(TABLES_DIR)
16014 .join(entry.table_id.to_string())
16015 .join("_txn");
16016 if txn_dir.exists() {
16017 let _ = std::fs::remove_dir_all(&txn_dir);
16018 }
16019 }
16020}
16021
16022#[cfg(test)]
16023mod write_permission_tests {
16024 use super::*;
16025 use crate::txn::Staged;
16026
16027 struct NoopExternalBridge;
16028
16029 impl ExternalTriggerBridge for NoopExternalBridge {
16030 fn apply_trigger_external_write(
16031 &self,
16032 _entry: &ExternalTableEntry,
16033 base_state: Vec<u8>,
16034 _op: ExternalTriggerWrite,
16035 ) -> Result<ExternalTriggerWriteResult> {
16036 Ok(ExternalTriggerWriteResult::new(base_state))
16037 }
16038 }
16039
16040 fn assert_txn_namespace_full<T>(result: Result<T>) {
16041 assert!(matches!(result, Err(MongrelError::Full(_))));
16042 }
16043
16044 #[test]
16045 fn every_begin_api_preserves_transaction_id_exhaustion_without_wal_mutation() {
16046 let directory = tempfile::tempdir().unwrap();
16047 let database = Database::create(directory.path()).unwrap();
16048 let generation = (*database.next_txn_id.lock() >> 32).saturating_add(1);
16049 *database.next_txn_id.lock() = generation << 32;
16050 let before = crate::wal::SharedWal::replay(directory.path())
16051 .unwrap()
16052 .len();
16053 let bridge = NoopExternalBridge;
16054
16055 assert_txn_namespace_full(database.begin().commit());
16056 assert_txn_namespace_full(database.begin_as(None).commit_with_row_ids());
16057 assert_txn_namespace_full(
16058 database
16059 .begin_with_isolation(crate::txn::IsolationLevel::Serializable)
16060 .commit(),
16061 );
16062 assert_txn_namespace_full(
16063 database
16064 .begin_with_external_trigger_bridge(&bridge)
16065 .commit(),
16066 );
16067 assert_txn_namespace_full(
16068 database
16069 .begin_with_external_trigger_bridge_as(&bridge, None)
16070 .commit_controlled(&crate::ExecutionControl::new(None), || Ok(())),
16071 );
16072
16073 assert_eq!(
16074 crate::wal::SharedWal::replay(directory.path())
16075 .unwrap()
16076 .len(),
16077 before
16078 );
16079 drop(database);
16080 Database::open(directory.path()).unwrap();
16081 }
16082
16083 #[test]
16084 fn recovered_storage_identity_mismatch_does_not_mutate_directory() {
16085 let directory = tempfile::tempdir().unwrap();
16086 let table_dir = directory.path().join("7");
16087 crate::durable_file::create_directory_all(&table_dir).unwrap();
16088 let original_schema = test_schema();
16089 crate::engine::write_schema(&table_dir, &original_schema).unwrap();
16090 let mut manifest = crate::manifest::Manifest::new(8, original_schema.schema_id);
16091 crate::manifest::write_atomic(&table_dir, &mut manifest, None).unwrap();
16092 let schema_path = table_dir.join(crate::engine::SCHEMA_FILENAME);
16093 let original_bytes = std::fs::read(&schema_path).unwrap();
16094
16095 let mut replacement_schema = original_schema;
16096 replacement_schema.schema_id += 1;
16097 assert!(matches!(
16098 ensure_recovered_table_storage(None, None, &table_dir, 7, &replacement_schema, None,),
16099 Err(MongrelError::Conflict(_))
16100 ));
16101
16102 assert_eq!(std::fs::read(schema_path).unwrap(), original_bytes);
16103 assert!(!table_dir.join(crate::engine::WAL_DIR).exists());
16104 assert!(!table_dir.join(crate::engine::RUNS_DIR).exists());
16105 assert_eq!(crate::manifest::read(&table_dir, None).unwrap().table_id, 8);
16106 }
16107
16108 #[test]
16109 fn catalog_table_missing_storage_fails_without_recreating_it() {
16110 let directory = tempfile::tempdir().unwrap();
16111 let table_dir = {
16112 let database = Database::create(directory.path()).unwrap();
16113 database.create_table("docs", test_schema()).unwrap();
16114 directory
16115 .path()
16116 .join(TABLES_DIR)
16117 .join(database.table_id("docs").unwrap().to_string())
16118 };
16119 std::fs::remove_dir_all(&table_dir).unwrap();
16120
16121 assert!(matches!(
16122 Database::open(directory.path()),
16123 Err(MongrelError::NotFound(_))
16124 ));
16125 assert!(!table_dir.exists());
16126 }
16127
16128 #[test]
16129 fn authentication_and_principal_resolution_share_one_catalog_snapshot() {
16130 let directory = tempfile::tempdir().unwrap();
16131 let database = std::sync::Arc::new(
16132 Database::create_with_credentials(directory.path(), "admin", "admin-password").unwrap(),
16133 );
16134 database.create_user("alice", "old-password").unwrap();
16135 let old_identity = database.user_identity("alice").unwrap();
16136 let (verified_tx, verified_rx) = std::sync::mpsc::channel();
16137 let (resume_tx, resume_rx) = std::sync::mpsc::channel();
16138 let (mutation_started_tx, mutation_started_rx) = std::sync::mpsc::channel();
16139 let (mutation_done_tx, mutation_done_rx) = std::sync::mpsc::channel();
16140
16141 std::thread::scope(|scope| {
16142 let authenticate = {
16143 let database = std::sync::Arc::clone(&database);
16144 scope.spawn(move || {
16145 database.authenticate_principal_inner("alice", "old-password", || {
16146 verified_tx.send(()).unwrap();
16147 resume_rx.recv().unwrap();
16148 })
16149 })
16150 };
16151 verified_rx.recv().unwrap();
16152 let mutate = {
16153 let database = std::sync::Arc::clone(&database);
16154 scope.spawn(move || {
16155 mutation_started_tx.send(()).unwrap();
16156 database.drop_user("alice").unwrap();
16157 database.create_user("alice", "new-password").unwrap();
16158 mutation_done_tx.send(()).unwrap();
16159 })
16160 };
16161 mutation_started_rx.recv().unwrap();
16162 assert!(mutation_done_rx
16163 .recv_timeout(std::time::Duration::from_millis(50))
16164 .is_err());
16165 resume_tx.send(()).unwrap();
16166 let principal = authenticate.join().unwrap().unwrap().unwrap();
16167 assert_eq!((principal.user_id, principal.created_epoch), old_identity);
16168 mutate.join().unwrap();
16169 });
16170
16171 assert_ne!(database.user_identity("alice").unwrap(), old_identity);
16172 assert!(database
16173 .authenticate_principal("alice", "old-password")
16174 .unwrap()
16175 .is_none());
16176 assert!(database
16177 .authenticate_principal("alice", "new-password")
16178 .unwrap()
16179 .is_some());
16180 }
16181
16182 #[test]
16183 fn homogeneous_batch_summarizes_to_one_permission_decision() {
16184 let staging = (0..10_050)
16185 .map(|_| {
16186 (
16187 7,
16188 Staged::Put(vec![(2, Value::Int64(2)), (1, Value::Int64(1))]),
16189 )
16190 })
16191 .collect::<Vec<_>>();
16192
16193 let needs = summarize_write_permissions(&staging);
16194 let table = needs.get(&7).unwrap();
16195 assert_eq!(needs.len(), 1);
16196 assert!(table.insert);
16197 assert_eq!(table.insert_columns, [1, 2]);
16198 assert!(!table.update);
16199 assert!(!table.delete);
16200 assert!(!table.truncate);
16201 }
16202
16203 #[test]
16204 fn mixed_writes_union_columns_and_preserve_empty_operations() {
16205 let staging = vec![
16206 (7, Staged::Put(vec![(2, Value::Int64(2))])),
16207 (7, Staged::Put(vec![(1, Value::Int64(1))])),
16208 (
16209 7,
16210 Staged::Update {
16211 row_id: RowId(1),
16212 new_row: vec![(1, Value::Int64(1)), (2, Value::Int64(2))],
16213 changed_columns: vec![2],
16214 },
16215 ),
16216 (7, Staged::Delete(RowId(2))),
16217 (8, Staged::Truncate),
16218 ];
16219
16220 let needs = summarize_write_permissions(&staging);
16221 let table = needs.get(&7).unwrap();
16222 assert_eq!(table.insert_columns, [1, 2]);
16223 assert!(table.update);
16224 assert_eq!(table.update_columns, [2]);
16225 assert!(table.delete);
16226 assert!(needs.get(&8).unwrap().truncate);
16227 }
16228
16229 #[test]
16230 fn final_permission_decisions_do_not_scale_with_rows() {
16231 let credentialless_dir = tempfile::tempdir().unwrap();
16232 let credentialless = Database::create(credentialless_dir.path()).unwrap();
16233 credentialless.create_table("docs", test_schema()).unwrap();
16234 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16235 credentialless
16236 .validate_write_permissions(&puts(credentialless.table_id("docs").unwrap()), None, None)
16237 .unwrap();
16238 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 0));
16239
16240 let authenticated_dir = tempfile::tempdir().unwrap();
16241 let authenticated =
16242 Database::create_with_credentials(authenticated_dir.path(), "admin", "admin-password")
16243 .unwrap();
16244 authenticated.create_table("docs", test_schema()).unwrap();
16245 let admin = authenticated.resolve_principal("admin").unwrap();
16246 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16247 authenticated
16248 .validate_write_permissions(
16249 &puts(authenticated.table_id("docs").unwrap()),
16250 Some(&admin),
16251 None,
16252 )
16253 .unwrap();
16254 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
16255 }
16256
16257 #[test]
16258 fn delete_batch_checks_permission_once_when_staged_and_once_when_committed() {
16259 let dir = tempfile::tempdir().unwrap();
16260 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
16261 db.create_table("docs", test_schema()).unwrap();
16262 let admin = db.resolve_principal("admin").unwrap();
16263 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16264
16265 let mut transaction = db.begin_as(Some(admin));
16266 transaction
16267 .delete_batch("docs", (0..100).map(RowId).collect())
16268 .unwrap();
16269 transaction.commit().unwrap();
16270
16271 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 2));
16272 }
16273
16274 #[test]
16275 fn truncate_validation_checks_admin_once_for_all_tables() {
16276 let dir = tempfile::tempdir().unwrap();
16277 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
16278 db.create_table("first", test_schema()).unwrap();
16279 db.create_table("second", test_schema()).unwrap();
16280 let admin = db.resolve_principal("admin").unwrap();
16281 let staging = vec![
16282 (db.table_id("first").unwrap(), Staged::Truncate),
16283 (db.table_id("second").unwrap(), Staged::Truncate),
16284 ];
16285
16286 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16287 db.validate_write_permissions(&staging, Some(&admin), None)
16288 .unwrap();
16289 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
16290 }
16291
16292 #[test]
16293 fn one_table_commit_batches_structural_work() {
16294 let dir = tempfile::tempdir().unwrap();
16295 let db = Database::create(dir.path()).unwrap();
16296 db.create_table("docs", test_schema()).unwrap();
16297 let table_id = db.table_id("docs").unwrap();
16298
16299 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(0));
16300 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
16301 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
16302 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
16303 db.transaction(|transaction| {
16304 for id in 0..100 {
16305 transaction.put("docs", vec![(1, Value::Int64(id))])?;
16306 }
16307 Ok(())
16308 })
16309 .unwrap();
16310
16311 AUTO_INCREMENT_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 2));
16312 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16313 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16314 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
16315
16316 let puts = crate::wal::SharedWal::replay(dir.path())
16317 .unwrap()
16318 .into_iter()
16319 .filter_map(|record| match record.op {
16320 crate::wal::Op::Put { table_id: id, rows } if id == table_id => Some(
16321 bincode::deserialize::<Vec<crate::memtable::Row>>(&rows)
16322 .unwrap()
16323 .len(),
16324 ),
16325 _ => None,
16326 })
16327 .collect::<Vec<_>>();
16328 assert_eq!(puts, [100]);
16329
16330 let row_ids = db
16331 .table("docs")
16332 .unwrap()
16333 .lock()
16334 .visible_rows(db.snapshot().0)
16335 .unwrap()
16336 .into_iter()
16337 .take(2)
16338 .map(|row| row.row_id)
16339 .collect::<Vec<_>>();
16340 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
16341 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
16342 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
16343 db.transaction(|transaction| {
16344 for row_id in row_ids {
16345 transaction.delete("docs", row_id)?;
16346 }
16347 Ok(())
16348 })
16349 .unwrap();
16350 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16351 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16352 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
16353
16354 let deletes = crate::wal::SharedWal::replay(dir.path())
16355 .unwrap()
16356 .into_iter()
16357 .filter_map(|record| match record.op {
16358 crate::wal::Op::Delete {
16359 table_id: id,
16360 row_ids,
16361 } if id == table_id => Some(row_ids.len()),
16362 _ => None,
16363 })
16364 .collect::<Vec<_>>();
16365 assert_eq!(deletes, [2]);
16366 }
16367
16368 fn puts(table_id: u64) -> Vec<(u64, Staged)> {
16369 (0..10_050)
16370 .map(|id| (table_id, Staged::Put(vec![(1, Value::Int64(id))])))
16371 .collect()
16372 }
16373
16374 fn test_schema() -> Schema {
16375 Schema {
16376 columns: vec![ColumnDef {
16377 id: 1,
16378 name: "id".into(),
16379 ty: TypeId::Int64,
16380 flags: crate::schema::ColumnFlags::empty()
16381 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
16382 default_value: None,
16383 }],
16384 ..Schema::default()
16385 }
16386 }
16387}
16388
16389#[cfg(test)]
16390mod cdc_bounds_tests {
16391 use super::*;
16392
16393 #[test]
16394 fn retained_byte_limit_rejects_without_allocating_payload() {
16395 let mut retained = 0;
16396 let error = charge_cdc_bytes(
16397 &mut retained,
16398 CDC_MAX_RETAINED_BYTES.saturating_add(1),
16399 "CDC retained bytes",
16400 )
16401 .unwrap_err();
16402 assert!(matches!(
16403 error,
16404 MongrelError::ResourceLimitExceeded {
16405 resource: "CDC retained bytes",
16406 ..
16407 }
16408 ));
16409 }
16410
16411 #[test]
16412 fn row_json_estimate_accounts_for_byte_array_expansion() {
16413 let row = crate::memtable::Row::new(RowId(1), Epoch(1))
16414 .with_column(1, Value::Bytes(vec![0; 1024]));
16415 assert!(cdc_row_json_bytes(&row) >= 1024 * std::mem::size_of::<serde_json::Value>());
16416 }
16417}
16418
16419#[cfg(test)]
16420mod generation_metrics_tests {
16421 use super::*;
16422 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
16423
16424 #[test]
16425 fn legacy_cow_fallback_is_measured() {
16426 let dir = tempfile::tempdir().unwrap();
16427 let table = Table::create(
16428 dir.path(),
16429 Schema {
16430 columns: vec![ColumnDef {
16431 id: 1,
16432 name: "id".into(),
16433 ty: TypeId::Int64,
16434 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
16435 default_value: None,
16436 }],
16437 ..Schema::default()
16438 },
16439 1,
16440 )
16441 .unwrap();
16442 let handle = TableHandle::from_table(table);
16443 let held = match &handle.inner {
16444 TableHandleInner::CopyOnWrite(slot) => Arc::clone(&slot.read()),
16445 TableHandleInner::Direct(_) => unreachable!(),
16446 };
16447
16448 handle.lock().set_sync_byte_threshold(1);
16449
16450 let stats = handle.generation_stats();
16451 assert_eq!(stats.cow_clone_count, 1);
16452 assert!(stats.estimated_cow_clone_bytes > 0);
16453 drop(held);
16454 }
16455}
16456
16457#[cfg(test)]
16458mod trigger_engine_tests {
16459 use super::*;
16460
16461 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
16462 WriteEvent {
16463 table: "test".into(),
16464 kind: TriggerEvent::Insert,
16465 new: Some(TriggerRowImage {
16466 columns: new_cells.iter().cloned().collect(),
16467 }),
16468 old: Some(TriggerRowImage {
16469 columns: old_cells.iter().cloned().collect(),
16470 }),
16471 changed_columns: Vec::new(),
16472 op_indices: Vec::new(),
16473 put_idx: None,
16474 trigger_stack: Vec::new(),
16475 }
16476 }
16477
16478 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
16479 WriteEvent {
16480 table: "test".into(),
16481 kind: TriggerEvent::Insert,
16482 new: Some(TriggerRowImage {
16483 columns: new_cells.iter().cloned().collect(),
16484 }),
16485 old: None,
16486 changed_columns: Vec::new(),
16487 op_indices: Vec::new(),
16488 put_idx: None,
16489 trigger_stack: Vec::new(),
16490 }
16491 }
16492
16493 #[test]
16494 fn value_order_int64_vs_float64() {
16495 assert_eq!(
16496 value_order(&Value::Int64(5), &Value::Float64(5.0)),
16497 Some(std::cmp::Ordering::Equal)
16498 );
16499 assert_eq!(
16500 value_order(&Value::Int64(5), &Value::Float64(3.0)),
16501 Some(std::cmp::Ordering::Greater)
16502 );
16503 assert_eq!(
16504 value_order(&Value::Int64(2), &Value::Float64(3.0)),
16505 Some(std::cmp::Ordering::Less)
16506 );
16507 }
16508
16509 #[test]
16510 fn value_order_null_returns_none() {
16511 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
16512 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
16513 assert_eq!(value_order(&Value::Null, &Value::Null), None);
16514 }
16515
16516 #[test]
16517 fn value_order_cross_group_returns_none() {
16518 assert_eq!(
16519 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
16520 None
16521 );
16522 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
16523 assert_eq!(
16524 value_order(
16525 &Value::Embedding(vec![1.0, 2.0]),
16526 &Value::Embedding(vec![1.0, 2.0])
16527 ),
16528 None
16529 );
16530 }
16531
16532 #[test]
16533 fn eval_trigger_expr_ranges_and_booleans() {
16534 let expr = TriggerExpr::And {
16535 left: Box::new(TriggerExpr::Gt {
16536 left: TriggerValue::NewColumn(1),
16537 right: TriggerValue::Literal(Value::Int64(0)),
16538 }),
16539 right: Box::new(TriggerExpr::Lte {
16540 left: TriggerValue::NewColumn(1),
16541 right: TriggerValue::Literal(Value::Int64(100)),
16542 }),
16543 };
16544 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
16545 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
16546 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
16547
16548 let or_expr = TriggerExpr::Or {
16549 left: Box::new(TriggerExpr::Lt {
16550 left: TriggerValue::NewColumn(1),
16551 right: TriggerValue::Literal(Value::Int64(0)),
16552 }),
16553 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
16554 TriggerValue::OldColumn(2),
16555 )))),
16556 };
16557 assert!(eval_trigger_expr(
16558 &or_expr,
16559 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
16560 )
16561 .unwrap());
16562 assert!(!eval_trigger_expr(
16563 &or_expr,
16564 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
16565 )
16566 .unwrap());
16567
16568 assert!(eval_trigger_expr(
16569 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
16570 &event_insert(&[])
16571 )
16572 .unwrap());
16573 assert!(!eval_trigger_expr(
16574 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
16575 &event_insert(&[])
16576 )
16577 .unwrap());
16578 assert!(!eval_trigger_expr(
16579 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
16580 &event_insert(&[])
16581 )
16582 .unwrap());
16583 }
16584}