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 durable_root = crate::durable_file::DurableRoot::open(root.as_ref())?;
1597 Self::begin_open_durable(durable_root, lock_timeout_ms)
1598 }
1599
1600 fn begin_open_durable(
1601 durable_root: crate::durable_file::DurableRoot,
1602 lock_timeout_ms: u32,
1603 ) -> Result<(PathBuf, DatabaseFileLock)> {
1604 let io_root = durable_root.io_path()?;
1605 let current_root = io_root.canonicalize()?;
1606 let mut lock = Self::acquire_database_lock(¤t_root, lock_timeout_ms)?;
1607 lock.durable_root = Some(Arc::new(durable_root));
1608 let io_root = lock
1609 .durable_root
1610 .as_ref()
1611 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
1612 .io_path()?;
1613 if lock
1614 .durable_root
1615 .as_ref()
1616 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
1617 .open_directory(META_DIR)
1618 .is_err()
1619 {
1620 return Err(MongrelError::NotFound(format!(
1621 "no database metadata found at {:?}",
1622 current_root
1623 )));
1624 }
1625 Self::acquire_legacy_database_lock(&mut lock, &io_root, lock_timeout_ms)?;
1626 Ok((io_root, lock))
1627 }
1628
1629 pub fn create(root: impl AsRef<Path>) -> Result<Self> {
1631 let (root, lock) = Self::begin_create(root)?;
1632 Self::create_inner(root, None, lock)
1633 }
1634
1635 #[cfg(feature = "encryption")]
1638 pub fn create_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
1639 let (root, lock) = Self::begin_create(root)?;
1640 let salt = crate::encryption::random_salt()?;
1641 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
1642 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1643 Self::create_inner(root, Some(kek), lock)
1644 }
1645
1646 #[cfg(feature = "encryption")]
1649 pub fn create_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
1650 let (root, lock) = Self::begin_create(root)?;
1651 let salt = crate::encryption::random_salt()?;
1652 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
1653 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
1654 Self::create_inner(root, Some(kek), lock)
1655 }
1656
1657 fn create_inner(
1658 root: PathBuf,
1659 kek: Option<Arc<crate::encryption::Kek>>,
1660 lock: DatabaseFileLock,
1661 ) -> Result<Self> {
1662 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
1663 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
1664 let cat = Catalog::empty();
1665 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
1666 Self::finish_open(root, cat, kek, meta_dek, false, None, None, None, lock)
1667 }
1668
1669 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
1671 Self::open_inner(root, None, None)
1672 }
1673
1674 #[cfg(feature = "encryption")]
1676 pub fn open_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
1677 let (root, lock) = Self::begin_open(root, 0)?;
1678 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
1679 MongrelError::Other("database root descriptor was not pinned".into())
1680 })?)?;
1681 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1682 Self::open_inner_locked(root, Some(kek), lock)
1683 }
1684
1685 #[cfg(feature = "encryption")]
1688 pub fn open_encrypted_with_options(
1689 root: impl AsRef<Path>,
1690 passphrase: &str,
1691 options: OpenOptions,
1692 ) -> Result<Self> {
1693 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
1694 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
1695 MongrelError::Other("database root descriptor was not pinned".into())
1696 })?)?;
1697 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1698 Self::open_inner_locked(root, Some(kek), lock)
1699 }
1700
1701 #[cfg(feature = "encryption")]
1703 pub fn open_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
1704 let (root, lock) = Self::begin_open(root, 0)?;
1705 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
1706 MongrelError::Other("database root descriptor was not pinned".into())
1707 })?)?;
1708 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
1709 Self::open_inner_locked(root, Some(kek), lock)
1710 }
1711
1712 pub fn open_with_credentials(
1724 root: impl AsRef<Path>,
1725 username: &str,
1726 password: &str,
1727 ) -> Result<Self> {
1728 Self::open_inner_with_credentials(root, None, username, password)
1729 }
1730
1731 pub fn open_with_credentials_and_options(
1735 root: impl AsRef<Path>,
1736 username: &str,
1737 password: &str,
1738 options: OpenOptions,
1739 ) -> Result<Self> {
1740 Self::open_inner_with_credentials_and_lock_timeout(
1741 root,
1742 None,
1743 username,
1744 password,
1745 options.lock_timeout_ms,
1746 )
1747 }
1748
1749 #[cfg(feature = "encryption")]
1752 pub fn open_encrypted_with_credentials(
1753 root: impl AsRef<Path>,
1754 passphrase: &str,
1755 username: &str,
1756 password: &str,
1757 ) -> Result<Self> {
1758 let (root, lock) = Self::begin_open(root, 0)?;
1759 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
1760 MongrelError::Other("database root descriptor was not pinned".into())
1761 })?)?;
1762 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1763 Self::open_inner_with_credentials_locked(root, Some(kek), username, password, lock)
1764 }
1765
1766 #[cfg(feature = "encryption")]
1770 pub fn open_encrypted_with_credentials_and_options(
1771 root: impl AsRef<Path>,
1772 passphrase: &str,
1773 username: &str,
1774 password: &str,
1775 options: OpenOptions,
1776 ) -> Result<Self> {
1777 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
1778 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
1779 MongrelError::Other("database root descriptor was not pinned".into())
1780 })?)?;
1781 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1782 Self::open_inner_with_credentials_locked(root, Some(kek), username, password, lock)
1783 }
1784
1785 pub fn open_with_options(root: impl AsRef<Path>, options: OpenOptions) -> Result<Self> {
1792 Self::open_inner_with_lock_timeout(root, None, None, options.lock_timeout_ms)
1795 }
1796
1797 fn open_inner_with_lock_timeout(
1798 root: impl AsRef<Path>,
1799 kek: Option<Arc<crate::encryption::Kek>>,
1800 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
1801 lock_timeout_ms: u32,
1802 ) -> Result<Self> {
1803 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
1804 Self::open_inner_locked(root, kek, lock)
1805 }
1806
1807 fn open_inner_locked(
1808 root: PathBuf,
1809 kek: Option<Arc<crate::encryption::Kek>>,
1810 lock: DatabaseFileLock,
1811 ) -> Result<Self> {
1812 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
1813 let mut cat = catalog::read_durable(
1814 lock.durable_root.as_deref().ok_or_else(|| {
1815 MongrelError::Other("database root descriptor was not pinned".into())
1816 })?,
1817 meta_dek.as_ref(),
1818 )?
1819 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
1820 let recovery_checkpoint = cat.clone();
1821
1822 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
1825 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
1826 lock.durable_root.as_deref().ok_or_else(|| {
1827 MongrelError::Other("database root descriptor was not pinned".into())
1828 })?,
1829 wal_dek.as_ref(),
1830 )?;
1831 recover_ddl_from_records(
1832 &root,
1833 Some(lock.durable_root.as_deref().ok_or_else(|| {
1834 MongrelError::Other("database root descriptor was not pinned".into())
1835 })?),
1836 &mut cat,
1837 meta_dek.as_ref(),
1838 false,
1839 None,
1840 &recovery_records,
1841 )?;
1842 Self::finish_open(
1843 root,
1844 cat,
1845 kek,
1846 meta_dek,
1847 true,
1848 Some(recovery_checkpoint),
1849 Some(recovery_records),
1850 None,
1851 lock,
1852 )
1853 }
1854
1855 fn open_inner_with_credentials(
1862 root: impl AsRef<Path>,
1863 kek: Option<Arc<crate::encryption::Kek>>,
1864 username: &str,
1865 password: &str,
1866 ) -> Result<Self> {
1867 Self::open_inner_with_credentials_and_lock_timeout(root, kek, username, password, 0)
1868 }
1869
1870 fn open_inner_with_credentials_and_lock_timeout(
1874 root: impl AsRef<Path>,
1875 kek: Option<Arc<crate::encryption::Kek>>,
1876 username: &str,
1877 password: &str,
1878 lock_timeout_ms: u32,
1879 ) -> Result<Self> {
1880 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
1881 Self::open_inner_with_credentials_locked(root, kek, username, password, lock)
1882 }
1883
1884 fn open_inner_with_credentials_locked(
1885 root: PathBuf,
1886 kek: Option<Arc<crate::encryption::Kek>>,
1887 username: &str,
1888 password: &str,
1889 lock: DatabaseFileLock,
1890 ) -> Result<Self> {
1891 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
1892 let mut cat = catalog::read_durable(
1893 lock.durable_root.as_deref().ok_or_else(|| {
1894 MongrelError::Other("database root descriptor was not pinned".into())
1895 })?,
1896 meta_dek.as_ref(),
1897 )?
1898 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
1899 let recovery_checkpoint = cat.clone();
1900
1901 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
1904 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
1905 lock.durable_root.as_deref().ok_or_else(|| {
1906 MongrelError::Other("database root descriptor was not pinned".into())
1907 })?,
1908 wal_dek.as_ref(),
1909 )?;
1910 recover_ddl_from_records(
1911 &root,
1912 Some(lock.durable_root.as_deref().ok_or_else(|| {
1913 MongrelError::Other("database root descriptor was not pinned".into())
1914 })?),
1915 &mut cat,
1916 meta_dek.as_ref(),
1917 false,
1918 None,
1919 &recovery_records,
1920 )?;
1921
1922 if !cat.require_auth {
1925 return Err(MongrelError::AuthNotRequired);
1926 }
1927
1928 let user = cat
1933 .users
1934 .iter()
1935 .find(|u| u.username == username)
1936 .filter(|u| !u.password_hash.is_empty())
1937 .ok_or_else(|| MongrelError::InvalidCredentials {
1938 username: username.to_string(),
1939 })?;
1940 let password_ok = crate::auth::verify_password(password, &user.password_hash)
1941 .map_err(MongrelError::Other)?;
1942 if !password_ok {
1943 return Err(MongrelError::InvalidCredentials {
1944 username: username.to_string(),
1945 });
1946 }
1947
1948 let principal =
1950 Self::resolve_principal_from_catalog(&cat, &user.username).ok_or_else(|| {
1951 MongrelError::InvalidCredentials {
1952 username: username.to_string(),
1953 }
1954 })?;
1955
1956 Self::finish_open(
1957 root,
1958 cat,
1959 kek,
1960 meta_dek,
1961 true,
1962 Some(recovery_checkpoint),
1963 Some(recovery_records),
1964 Some(principal),
1965 lock,
1966 )
1967 }
1968
1969 pub fn create_with_credentials(
1979 root: impl AsRef<Path>,
1980 admin_username: &str,
1981 admin_password: &str,
1982 ) -> Result<Self> {
1983 let (root, lock) = Self::begin_create(root)?;
1984 Self::create_inner_with_credentials(root, None, admin_username, admin_password, lock)
1985 }
1986
1987 #[cfg(feature = "encryption")]
1991 pub fn create_encrypted_with_credentials(
1992 root: impl AsRef<Path>,
1993 passphrase: &str,
1994 admin_username: &str,
1995 admin_password: &str,
1996 ) -> Result<Self> {
1997 let (root, lock) = Self::begin_create(root)?;
1998 let salt = crate::encryption::random_salt()?;
1999 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
2000 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2001 Self::create_inner_with_credentials(root, Some(kek), admin_username, admin_password, lock)
2002 }
2003
2004 fn create_inner_with_credentials(
2005 root: PathBuf,
2006 kek: Option<Arc<crate::encryption::Kek>>,
2007 admin_username: &str,
2008 admin_password: &str,
2009 lock: DatabaseFileLock,
2010 ) -> Result<Self> {
2011 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
2012 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2013
2014 let password_hash =
2016 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
2017 let mut cat = Catalog::empty();
2018 cat.require_auth = true;
2019 cat.next_user_id = 2;
2020 cat.users.push(crate::auth::UserEntry {
2021 id: 1,
2022 username: admin_username.to_string(),
2023 password_hash,
2024 roles: Vec::new(),
2025 is_admin: true,
2026 created_epoch: 0,
2027 });
2028 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
2029
2030 let admin_principal = crate::auth::Principal {
2033 user_id: 1,
2034 created_epoch: 0,
2035 username: admin_username.to_string(),
2036 is_admin: true,
2037 roles: Vec::new(),
2038 permissions: Vec::new(),
2039 };
2040 Self::finish_open(
2041 root,
2042 cat,
2043 kek,
2044 meta_dek,
2045 false,
2046 None,
2047 None,
2048 Some(admin_principal),
2049 lock,
2050 )
2051 }
2052
2053 fn reject_existing_database(root: &Path) -> Result<()> {
2054 if root.join(catalog::CATALOG_FILENAME).exists() {
2057 return Err(MongrelError::InvalidArgument(format!(
2058 "database already exists at {}; use Database::open() to open it, \
2059 or remove the directory first",
2060 root.display()
2061 )));
2062 }
2063 Ok(())
2064 }
2065
2066 fn open_inner(
2067 root: impl AsRef<Path>,
2068 kek: Option<Arc<crate::encryption::Kek>>,
2069 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
2070 ) -> Result<Self> {
2071 Self::open_inner_with_lock_timeout(root, kek, None, 0)
2072 }
2073
2074 pub(crate) fn open_replica_recovery_durable(
2078 root: &crate::durable_file::DurableRoot,
2079 ) -> Result<Self> {
2080 let (root, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
2081 Self::open_replica_recovery_inner(root, None, lock)
2082 }
2083
2084 #[cfg(feature = "encryption")]
2085 pub(crate) fn open_encrypted_replica_recovery_durable(
2086 root: &crate::durable_file::DurableRoot,
2087 passphrase: &str,
2088 ) -> Result<Self> {
2089 let (root_path, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
2090 let salt = read_encryption_salt(root)?;
2091 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2092 Self::open_replica_recovery_inner(root_path, Some(kek), lock)
2093 }
2094
2095 fn open_replica_recovery_inner(
2096 root: PathBuf,
2097 kek: Option<Arc<crate::encryption::Kek>>,
2098 lock: DatabaseFileLock,
2099 ) -> Result<Self> {
2100 if !root.join(META_DIR).join("replica").is_file() {
2101 return Err(MongrelError::InvalidArgument(
2102 "recovery auth bypass requires a marked replica staging directory".into(),
2103 ));
2104 }
2105 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2106 let mut cat = catalog::read_durable(
2107 lock.durable_root.as_deref().ok_or_else(|| {
2108 MongrelError::Other("database root descriptor was not pinned".into())
2109 })?,
2110 meta_dek.as_ref(),
2111 )?
2112 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
2113 let recovery_checkpoint = cat.clone();
2114 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
2115 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
2116 lock.durable_root.as_deref().ok_or_else(|| {
2117 MongrelError::Other("database root descriptor was not pinned".into())
2118 })?,
2119 wal_dek.as_ref(),
2120 )?;
2121 recover_ddl_from_records(
2122 &root,
2123 Some(lock.durable_root.as_deref().ok_or_else(|| {
2124 MongrelError::Other("database root descriptor was not pinned".into())
2125 })?),
2126 &mut cat,
2127 meta_dek.as_ref(),
2128 false,
2129 None,
2130 &recovery_records,
2131 )?;
2132 let principal = if cat.require_auth {
2133 cat.users
2134 .iter()
2135 .find(|user| user.is_admin)
2136 .and_then(|user| Self::resolve_principal_from_catalog(&cat, &user.username))
2137 .ok_or_else(|| {
2138 MongrelError::Schema(
2139 "authenticated replica catalog has no recoverable admin".into(),
2140 )
2141 })?
2142 .into()
2143 } else {
2144 None
2145 };
2146 Self::finish_open(
2147 root,
2148 cat,
2149 kek,
2150 meta_dek,
2151 true,
2152 Some(recovery_checkpoint),
2153 Some(recovery_records),
2154 principal,
2155 lock,
2156 )
2157 }
2158
2159 fn fs_lock_exclusive(f: &std::fs::File, timeout_ms: u32) -> std::io::Result<()> {
2171 use fs2::FileExt;
2172 if timeout_ms == 0 {
2173 return f.try_lock_exclusive();
2174 }
2175 let deadline =
2177 std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms as u64);
2178 let mut next_sleep = std::time::Duration::from_millis(1);
2179 loop {
2180 match f.try_lock_exclusive() {
2181 Ok(()) => return Ok(()),
2182 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
2183 let now = std::time::Instant::now();
2184 if now >= deadline {
2185 return Err(std::io::Error::new(
2186 std::io::ErrorKind::WouldBlock,
2187 format!("could not acquire database lock within {timeout_ms}ms"),
2188 ));
2189 }
2190 let remaining = deadline - now;
2191 let sleep = next_sleep.min(remaining);
2192 std::thread::sleep(sleep);
2193 next_sleep = next_sleep
2196 .saturating_mul(10)
2197 .min(std::time::Duration::from_millis(50));
2198 }
2199 Err(e) => return Err(e),
2200 }
2201 }
2202 }
2203
2204 #[allow(clippy::too_many_arguments)]
2205 fn finish_open(
2206 root: PathBuf,
2207 cat: Catalog,
2208 kek: Option<Arc<crate::encryption::Kek>>,
2209 meta_dek: Option<[u8; META_DEK_LEN]>,
2210 existing: bool,
2211 recovery_checkpoint: Option<Catalog>,
2212 recovery_records: Option<Vec<crate::wal::Record>>,
2213 principal: Option<crate::auth::Principal>,
2214 lock: DatabaseFileLock,
2215 ) -> Result<Self> {
2216 let durable_root = Arc::clone(lock.durable_root.as_ref().ok_or_else(|| {
2217 MongrelError::Other("database root descriptor was not pinned".into())
2218 })?);
2219 let read_only = if existing {
2220 match durable_root.open_regular(Path::new(META_DIR).join("replica")) {
2221 Ok(_) => true,
2222 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
2223 Err(error) => return Err(error.into()),
2224 }
2225 } else {
2226 false
2227 };
2228 let recovered_catalog = cat;
2229 let mut cat = recovered_catalog.clone();
2230 let abandoned = if existing && !read_only {
2231 let abandoned = cat
2232 .tables
2233 .iter()
2234 .filter(|entry| matches!(entry.state, TableState::Building { .. }))
2235 .map(|entry| entry.table_id)
2236 .collect::<Vec<_>>();
2237 for entry in &mut cat.tables {
2238 if abandoned.contains(&entry.table_id) {
2239 entry.state = TableState::Dropped {
2240 at_epoch: cat.db_epoch,
2241 };
2242 }
2243 }
2244 abandoned
2245 } else {
2246 Vec::new()
2247 };
2248 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
2249 let recovery_records = match (existing, recovery_records) {
2250 (true, Some(records)) => records,
2251 (true, None) => {
2252 return Err(MongrelError::Other(
2253 "existing open has no validated WAL recovery plan".into(),
2254 ))
2255 }
2256 (false, _) => Vec::new(),
2257 };
2258 let (history_epochs, history_start) =
2259 read_history_retention(&durable_root, Epoch(cat.db_epoch))?;
2260 let open_generation = if existing {
2261 let checkpoint = recovery_checkpoint.as_ref().ok_or_else(|| {
2262 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
2263 })?;
2264 let recovered_table_ids = cat
2265 .tables
2266 .iter()
2267 .filter(|entry| {
2268 checkpoint
2269 .tables
2270 .iter()
2271 .all(|checkpoint| checkpoint.table_id != entry.table_id)
2272 })
2273 .map(|entry| entry.table_id)
2274 .collect::<HashSet<_>>();
2275 let reconciled_table_ids = cat
2276 .tables
2277 .iter()
2278 .filter(|entry| {
2279 checkpoint
2280 .tables
2281 .iter()
2282 .find(|checkpoint| checkpoint.table_id == entry.table_id)
2283 .is_some_and(|checkpoint| {
2284 crate::wal::DdlOp::encode_schema(&checkpoint.schema).ok()
2285 != crate::wal::DdlOp::encode_schema(&entry.schema).ok()
2286 })
2287 })
2288 .map(|entry| entry.table_id)
2289 .collect::<HashSet<_>>();
2290 validate_shared_wal_recovery_plan(
2291 &durable_root,
2292 &cat,
2293 &recovered_table_ids,
2294 &reconciled_table_ids,
2295 meta_dek.as_ref(),
2296 kek.clone(),
2297 &recovery_records,
2298 )?;
2299 let retained_generation = recovery_records
2300 .iter()
2301 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
2302 .map(|record| record.txn_id >> 32)
2303 .max()
2304 .unwrap_or(0);
2305 let head_generation =
2306 crate::wal::SharedWal::durable_open_generation(&durable_root, wal_dek.as_ref())?;
2307 let durable_floor = match head_generation {
2308 Some(head) if retained_generation > head => {
2309 return Err(MongrelError::CorruptWal {
2310 offset: retained_generation,
2311 reason: format!(
2312 "retained transaction generation {retained_generation} exceeds WAL head generation {head}"
2313 ),
2314 })
2315 }
2316 Some(head) => head,
2317 None => retained_generation,
2318 };
2319 let stored = catalog::read_generation(&durable_root)?;
2320 if stored.is_some_and(|generation| generation < durable_floor) {
2321 return Err(MongrelError::Other(format!(
2322 "open-generation {stored:?} precedes durable WAL generation {durable_floor}"
2323 )));
2324 }
2325 let bumped = stored
2326 .unwrap_or(durable_floor)
2327 .max(durable_floor)
2328 .checked_add(1)
2329 .ok_or_else(|| MongrelError::Full("open-generation namespace exhausted".into()))?;
2330 if bumped > u32::MAX as u64 {
2331 return Err(MongrelError::Full(
2332 "open-generation namespace exhausted".into(),
2333 ));
2334 }
2335 bumped
2336 } else {
2337 0
2338 };
2339 let principal = if cat.require_auth {
2340 let supplied = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
2341 Some(
2342 Self::resolve_bound_principal_from_catalog(&cat, supplied)
2343 .ok_or(MongrelError::AuthRequired)?,
2344 )
2345 } else {
2346 principal
2347 };
2348 let mut table_roots = HashMap::<u64, Arc<crate::durable_file::DurableRoot>>::new();
2349 if existing {
2350 for entry in &cat.tables {
2351 if !matches!(entry.state, TableState::Live) {
2352 continue;
2353 }
2354 match durable_root
2355 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))
2356 {
2357 Ok(root) => {
2358 table_roots.insert(entry.table_id, Arc::new(root));
2359 }
2360 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
2361 Err(error) => return Err(error.into()),
2362 }
2363 }
2364 }
2365
2366 if existing {
2370 let mut applied = recovery_checkpoint.ok_or_else(|| {
2371 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
2372 })?;
2373 recover_ddl_from_records(
2374 &root,
2375 Some(&durable_root),
2376 &mut applied,
2377 meta_dek.as_ref(),
2378 true,
2379 Some(&table_roots),
2380 &recovery_records,
2381 )?;
2382 let catalog_value = |catalog: &Catalog| {
2383 serde_json::to_value(catalog)
2384 .map_err(|error| MongrelError::Other(format!("catalog compare: {error}")))
2385 };
2386 if catalog_value(&applied)? != catalog_value(&recovered_catalog)? {
2387 return Err(MongrelError::CorruptWal {
2388 offset: 0,
2389 reason: "validated and applied DDL recovery plans differ".into(),
2390 });
2391 }
2392 if catalog_value(&cat)? != catalog_value(&applied)? {
2393 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
2394 }
2395 validate_catalog_table_storage(&durable_root, &cat, meta_dek.as_ref())?;
2396 if !read_only {
2397 sweep_unreferenced_table_dirs(&root, &cat)?;
2398 }
2399 match durable_root.remove_directory_all(Path::new(META_DIR).join("backup-pins")) {
2400 Ok(()) => {}
2401 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
2402 Err(error) => return Err(error.into()),
2403 }
2404 }
2405
2406 let epoch = Arc::new(EpochAuthority::new(cat.db_epoch));
2407 let snapshots = Arc::new(SnapshotRegistry::new());
2408 snapshots.configure_history(history_epochs, history_start);
2409 let page_cache = Arc::new(crate::cache::Sharded::new(
2410 crate::cache::CACHE_SHARDS,
2411 || {
2412 crate::cache::PageCache::new(
2413 crate::engine::PAGE_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
2414 )
2415 },
2416 ));
2417 let decoded_cache = Arc::new(crate::cache::Sharded::new(
2418 crate::cache::CACHE_SHARDS,
2419 || {
2420 crate::cache::DecodedPageCache::new(
2421 crate::engine::DECODED_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
2422 )
2423 },
2424 ));
2425 let commit_lock = Arc::new(Mutex::new(()));
2426 let shared_wal = Arc::new(Mutex::new(if existing {
2427 crate::wal::SharedWal::open_durable_root_validated(
2428 Arc::clone(&durable_root),
2429 Epoch(cat.db_epoch),
2430 wal_dek.clone(),
2431 Some(&recovery_records),
2432 )?
2433 } else {
2434 crate::wal::SharedWal::create_with_durable_root(
2435 Arc::clone(&durable_root),
2436 Epoch(cat.db_epoch),
2437 wal_dek.clone(),
2438 )?
2439 }));
2440 let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));
2444 let group = Arc::new(crate::txn::GroupCommit::new(
2445 shared_wal.lock().durable_seq(),
2446 ));
2447 let (change_wake, _change_rx) = tokio::sync::broadcast::channel(256);
2448 let txn_ids = Arc::new(Mutex::new(1u64));
2452 let _ = abandoned;
2453
2454 let auth_state = crate::auth_state::AuthState::new(cat.require_auth, principal.clone());
2459 let security_coordinator = security_coordinator(&root, cat.security_version);
2460 let auth_checker: Option<Arc<dyn crate::auth_state::TableAuthChecker>> = Some(Arc::new(
2461 crate::auth_state::DefaultTableAuthChecker::new(auth_state.clone()),
2462 ));
2463
2464 let mut tables: HashMap<u64, TableHandle> = HashMap::new();
2470 for entry in &cat.tables {
2471 if !matches!(entry.state, TableState::Live) {
2472 continue;
2473 }
2474 let table_root = match table_roots.remove(&entry.table_id) {
2475 Some(root) => root,
2476 None => Arc::new(
2477 durable_root
2478 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))?,
2479 ),
2480 };
2481 let tdir = table_root.io_path()?;
2482 let ctx = SharedCtx {
2483 root_guard: Some(table_root),
2484 epoch: Arc::clone(&epoch),
2485 page_cache: Arc::clone(&page_cache),
2486 decoded_cache: Arc::clone(&decoded_cache),
2487 snapshots: Arc::clone(&snapshots),
2488 kek: kek.clone(),
2489 commit_lock: Arc::clone(&commit_lock),
2490 shared: Some(crate::engine::SharedWalCtx {
2491 wal: Arc::clone(&shared_wal),
2492 group: Arc::clone(&group),
2493 poisoned: Arc::clone(&poisoned),
2494 txn_ids: Arc::clone(&txn_ids),
2495 change_wake: change_wake.clone(),
2496 }),
2497 table_name: Some(entry.name.clone()),
2498 auth: auth_checker.clone(),
2499 read_only,
2500 };
2501 let t = Table::open_in(&tdir, ctx)?;
2502 tables.insert(entry.table_id, TableHandle::new(t));
2503 }
2504
2505 if existing {
2511 recover_shared_wal(&durable_root, &tables, &cat, &epoch, &recovery_records)?;
2512 reconcile_recovered_table_metadata(&tables, epoch.visible())?;
2513 if read_only {
2514 crate::replication::reconcile_replica_epoch_durable(
2515 &durable_root,
2516 epoch.visible().0,
2517 )?;
2518 }
2519 sweep_pending_txn_dirs(&root, &cat);
2522 }
2523
2524 catalog::write_generation(&durable_root, open_generation)?;
2526 shared_wal.lock().seal_open_generation(open_generation)?;
2527 crate::replication::replication_identity_durable(&durable_root)?;
2528 let next_txn_id = (open_generation << 32) | 1;
2529 *txn_ids.lock() = next_txn_id;
2531
2532 Ok(Self {
2533 root,
2534 durable_root,
2535 read_only,
2536 catalog: RwLock::new(cat),
2537 security_coordinator,
2538 security_catalog_disk_reads: AtomicU64::new(0),
2539 rls_cache: Mutex::new(RlsCache::default()),
2540 epoch,
2541 snapshots,
2542 page_cache,
2543 decoded_cache,
2544 commit_lock,
2545 shared_wal,
2546 next_txn_id: txn_ids,
2547 tables: RwLock::new(tables),
2548 kek,
2549 ddl_lock: Mutex::new(()),
2550 meta_dek,
2551 conflicts: crate::txn::ConflictIndex::new(),
2552 active_txns: crate::txn::ActiveTxns::new(),
2553 poisoned,
2554 group,
2555 spill_threshold: std::sync::atomic::AtomicU64::new(64 * 1024 * 1024),
2556 active_spills: Arc::new(crate::retention::ActiveSpills::new()),
2557 replication_barrier: parking_lot::RwLock::new(()),
2558 replication_wal_retention_segments: AtomicUsize::new(0),
2559 backup_pins: Arc::new(Mutex::new(HashMap::new())),
2560 spill_hook: Mutex::new(None),
2561 security_commit_hook: Mutex::new(None),
2562 catalog_commit_hook: Mutex::new(None),
2563 backup_hook: Mutex::new(None),
2564 replication_hook: Mutex::new(None),
2565 trigger_recursive: AtomicBool::new(TriggerConfig::default().recursive_triggers),
2566 trigger_max_depth: AtomicU32::new(TriggerConfig::default().max_depth),
2567 trigger_max_loop_iterations: AtomicU32::new(
2568 TriggerConfig::default().max_loop_iterations,
2569 ),
2570 _lock: Some(lock),
2571 notify: {
2572 let (tx, _rx) = tokio::sync::broadcast::channel(256);
2573 tx
2574 },
2575 change_wake,
2576 principal: RwLock::new(principal),
2577 auth_state,
2578 })
2579 }
2580
2581 pub fn visible_epoch(&self) -> Epoch {
2583 self.epoch.visible()
2584 }
2585
2586 pub fn catalog_snapshot(&self) -> Catalog {
2588 self.catalog.read().clone()
2589 }
2590
2591 pub fn sql_pragma_i64(&self, key: &str) -> Result<Option<i64>> {
2593 let catalog = self.catalog.read();
2594 match key {
2595 "user_version" => Ok(catalog.user_version),
2596 "application_id" => Ok(catalog.application_id),
2597 _ => Err(MongrelError::InvalidArgument(format!(
2598 "unsupported persistent SQL pragma {key:?}"
2599 ))),
2600 }
2601 }
2602
2603 pub fn set_sql_pragma_i64_with_epoch(&self, key: &str, value: i64) -> Result<Option<Epoch>> {
2606 self.set_sql_pragma_i64_with_epoch_inner(key, value, None)
2607 }
2608
2609 pub fn set_sql_pragma_i64_with_epoch_controlled<F>(
2610 &self,
2611 key: &str,
2612 value: i64,
2613 mut before_commit: F,
2614 ) -> Result<Option<Epoch>>
2615 where
2616 F: FnMut() -> Result<()>,
2617 {
2618 self.set_sql_pragma_i64_with_epoch_inner(key, value, Some(&mut before_commit))
2619 }
2620
2621 fn set_sql_pragma_i64_with_epoch_inner(
2622 &self,
2623 key: &str,
2624 value: i64,
2625 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
2626 ) -> Result<Option<Epoch>> {
2627 use crate::wal::DdlOp;
2628
2629 self.require(&crate::auth::Permission::Ddl)?;
2630 if self.read_only {
2631 return Err(MongrelError::ReadOnlyReplica);
2632 }
2633 if self.poisoned.load(Ordering::Relaxed) {
2634 return Err(MongrelError::Other(
2635 "database poisoned by fsync error".into(),
2636 ));
2637 }
2638 let _ddl = self.ddl_lock.lock();
2639 let _security_write = self.security_write()?;
2640 self.require(&crate::auth::Permission::Ddl)?;
2641 let mut next_catalog = self.catalog.read().clone();
2642 let target = match key {
2643 "user_version" => &mut next_catalog.user_version,
2644 "application_id" => &mut next_catalog.application_id,
2645 _ => {
2646 return Err(MongrelError::InvalidArgument(format!(
2647 "unsupported persistent SQL pragma {key:?}"
2648 )))
2649 }
2650 };
2651 if *target == Some(value) {
2652 return Ok(None);
2653 }
2654 *target = Some(value);
2655
2656 let _commit = self.commit_lock.lock();
2657 let epoch = self.epoch.bump_assigned();
2658 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2659 let txn_id = self.alloc_txn_id()?;
2660 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
2661 let commit_seq = {
2662 let mut wal = self.shared_wal.lock();
2663 if let Some(before_commit) = before_commit {
2664 before_commit()?;
2665 }
2666 let append: Result<u64> = (|| {
2667 wal.append(
2668 txn_id,
2669 WAL_TABLE_ID,
2670 crate::wal::Op::Ddl(DdlOp::SetSqlPragma {
2671 key: key.to_string(),
2672 value,
2673 }),
2674 )?;
2675 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
2676 wal.append_commit(txn_id, epoch, &[])
2677 })();
2678 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
2679 };
2680 self.await_durable_commit(commit_seq, epoch)?;
2681 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
2682 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
2683 Ok(Some(epoch))
2684 }
2685
2686 pub fn materialized_view(&self, name: &str) -> Option<crate::catalog::MaterializedViewEntry> {
2687 self.catalog
2688 .read()
2689 .materialized_views
2690 .iter()
2691 .find(|definition| definition.name == name)
2692 .cloned()
2693 }
2694
2695 pub fn materialized_views(&self) -> Vec<crate::catalog::MaterializedViewEntry> {
2696 self.catalog.read().materialized_views.clone()
2697 }
2698
2699 pub fn security_catalog(&self) -> crate::security::SecurityCatalog {
2700 self.catalog.read().security.clone()
2701 }
2702
2703 pub fn security_active_for(&self, table: &str) -> bool {
2704 self.catalog.read().security.table_has_security(table)
2705 }
2706
2707 fn refresh_security_catalog_if_stale(&self, expected_version: u64) -> Result<()> {
2708 if self.catalog.read().security_version == expected_version {
2709 return Ok(());
2710 }
2711 self.security_catalog_disk_reads
2712 .fetch_add(1, Ordering::Relaxed);
2713 let fresh = catalog::read_durable(&self.durable_root, self.meta_dek.as_ref())?
2714 .ok_or_else(|| MongrelError::NotFound("catalog vanished during write".into()))?;
2715 let principal = self.principal.read().clone();
2716 let principal = if fresh.require_auth {
2717 principal
2718 .as_ref()
2719 .and_then(|principal| Self::resolve_bound_principal_from_catalog(&fresh, principal))
2720 } else {
2721 principal
2722 };
2723 self.auth_state.set_require_auth(fresh.require_auth);
2724 *self.catalog.write() = fresh;
2725 *self.principal.write() = principal.clone();
2726 self.auth_state.set_principal(principal);
2727 Ok(())
2728 }
2729
2730 fn security_write(&self) -> Result<parking_lot::RwLockWriteGuard<'_, ()>> {
2731 let guard = self.security_coordinator.gate.write();
2732 let version = self.security_coordinator.version.load(Ordering::Acquire);
2733 self.refresh_security_catalog_if_stale(version)?;
2734 Ok(guard)
2735 }
2736
2737 fn publish_catalog_candidate(
2741 &self,
2742 catalog: Catalog,
2743 epoch: Epoch,
2744 epoch_guard: &mut EpochGuard<'_>,
2745 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
2746 ) -> Result<()> {
2747 self.publish_catalog_candidate_with_prelude(
2748 catalog,
2749 epoch,
2750 epoch_guard,
2751 before_publish,
2752 Vec::new(),
2753 )
2754 }
2755
2756 fn publish_catalog_candidate_with_prelude(
2757 &self,
2758 catalog: Catalog,
2759 epoch: Epoch,
2760 epoch_guard: &mut EpochGuard<'_>,
2761 mut before_publish: Option<&mut dyn FnMut() -> Result<()>>,
2762 prelude: Vec<(u64, crate::wal::Op)>,
2763 ) -> Result<()> {
2764 use crate::wal::DdlOp;
2765
2766 if self.read_only {
2767 return Err(MongrelError::ReadOnlyReplica);
2768 }
2769 if self.poisoned.load(Ordering::Relaxed) {
2770 return Err(MongrelError::Other(
2771 "database poisoned by fsync error".into(),
2772 ));
2773 }
2774 if let Some(before_publish) = before_publish.as_mut() {
2775 (**before_publish)()?;
2776 }
2777 if catalog.db_epoch != epoch.0 {
2778 return Err(MongrelError::InvalidArgument(format!(
2779 "catalog epoch {} does not match commit epoch {}",
2780 catalog.db_epoch, epoch.0
2781 )));
2782 }
2783 {
2784 let current = self.catalog.read();
2785 validate_catalog_transition(¤t, &catalog)?;
2786 }
2787 validate_recovered_catalog(&catalog)?;
2788 let catalog_json = DdlOp::encode_catalog(&catalog)?;
2789 let txn_id = self.alloc_txn_id()?;
2790 let commit_seq = {
2791 let mut wal = self.shared_wal.lock();
2792 let append: Result<u64> = (|| {
2793 for (table_id, op) in prelude {
2794 wal.append(txn_id, table_id, op)?;
2795 }
2796 wal.append(
2797 txn_id,
2798 WAL_TABLE_ID,
2799 crate::wal::Op::Ddl(DdlOp::CatalogSnapshot { catalog_json }),
2800 )?;
2801 wal.append_commit(txn_id, epoch, &[])
2802 })();
2803 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
2804 };
2805 self.await_durable_commit(commit_seq, epoch)?;
2806 let checkpoint = self.checkpoint_catalog_after_durable(catalog);
2807 self.finish_durable_publish(epoch, epoch_guard, checkpoint)
2808 }
2809
2810 fn checkpoint_catalog_after_durable(&self, catalog: Catalog) -> Result<()> {
2814 let checkpoint = catalog::write_atomic(&self.root, &catalog, self.meta_dek.as_ref());
2815 let version = catalog.security_version;
2816 let principal = self.principal.read().clone();
2817 let principal = if catalog.require_auth {
2818 principal.as_ref().and_then(|principal| {
2819 Self::resolve_bound_principal_from_catalog(&catalog, principal)
2820 })
2821 } else {
2822 principal
2823 };
2824 *self.catalog.write() = catalog;
2825 self.security_coordinator
2826 .version
2827 .store(version, Ordering::Release);
2828 self.auth_state
2829 .set_require_auth(self.catalog.read().require_auth);
2830 *self.principal.write() = principal.clone();
2831 self.auth_state.set_principal(principal);
2832 checkpoint
2833 }
2834
2835 fn finish_durable_publish(
2836 &self,
2837 epoch: Epoch,
2838 epoch_guard: &mut EpochGuard<'_>,
2839 post_step: Result<()>,
2840 ) -> Result<()> {
2841 self.epoch.publish_in_order(epoch);
2842 epoch_guard.disarm();
2843 match post_step {
2844 Ok(()) => Ok(()),
2845 Err(error) => {
2846 self.poisoned.store(true, Ordering::Relaxed);
2847 Err(MongrelError::DurableCommit {
2848 epoch: epoch.0,
2849 message: error.to_string(),
2850 })
2851 }
2852 }
2853 }
2854
2855 fn await_durable_commit(&self, commit_seq: u64, epoch: Epoch) -> Result<()> {
2859 match self.group.await_durable(&self.shared_wal, commit_seq) {
2860 Ok(()) => Ok(()),
2861 Err(error) => {
2862 self.poisoned.store(true, Ordering::Relaxed);
2863 Err(MongrelError::CommitOutcomeUnknown {
2864 epoch: epoch.0,
2865 message: error.to_string(),
2866 })
2867 }
2868 }
2869 }
2870
2871 fn commit_outcome_unknown(&self, epoch: Epoch, error: impl std::fmt::Display) -> MongrelError {
2872 self.poisoned.store(true, Ordering::Relaxed);
2873 MongrelError::CommitOutcomeUnknown {
2874 epoch: epoch.0,
2875 message: error.to_string(),
2876 }
2877 }
2878
2879 pub fn set_security_catalog(&self, security: crate::security::SecurityCatalog) -> Result<()> {
2881 self.set_security_catalog_as_with_epoch(security, None)
2882 .map(|_| ())
2883 }
2884
2885 pub fn set_security_catalog_as(
2887 &self,
2888 security: crate::security::SecurityCatalog,
2889 principal: Option<&crate::auth::Principal>,
2890 ) -> Result<()> {
2891 self.set_security_catalog_as_with_epoch(security, principal)
2892 .map(|_| ())
2893 }
2894
2895 pub fn set_security_catalog_as_with_epoch(
2897 &self,
2898 security: crate::security::SecurityCatalog,
2899 principal: Option<&crate::auth::Principal>,
2900 ) -> Result<Epoch> {
2901 self.set_security_catalog_as_with_epoch_inner(security, principal, None)
2902 }
2903
2904 pub fn set_security_catalog_as_with_epoch_controlled<F>(
2907 &self,
2908 security: crate::security::SecurityCatalog,
2909 principal: Option<&crate::auth::Principal>,
2910 mut before_commit: F,
2911 ) -> Result<Epoch>
2912 where
2913 F: FnMut() -> Result<()>,
2914 {
2915 self.set_security_catalog_as_with_epoch_inner(security, principal, Some(&mut before_commit))
2916 }
2917
2918 fn set_security_catalog_as_with_epoch_inner(
2919 &self,
2920 security: crate::security::SecurityCatalog,
2921 principal: Option<&crate::auth::Principal>,
2922 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
2923 ) -> Result<Epoch> {
2924 use crate::wal::DdlOp;
2925 use std::sync::atomic::Ordering;
2926
2927 self.require_for(principal, &crate::auth::Permission::Admin)?;
2928 if self.poisoned.load(Ordering::Relaxed) {
2929 return Err(MongrelError::Other(
2930 "database poisoned by fsync error".into(),
2931 ));
2932 }
2933 let _ddl = self.ddl_lock.lock();
2934 let _security_write = self.security_write()?;
2937 self.require_for(principal, &crate::auth::Permission::Admin)?;
2938 let mut next_catalog = self.catalog.read().clone();
2939 validate_security_catalog(&next_catalog, &security)?;
2940 let payload = DdlOp::encode_security(&security)?;
2941 let _commit = self.commit_lock.lock();
2942 let epoch = self.epoch.bump_assigned();
2943 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2944 let txn_id = self.alloc_txn_id()?;
2945 next_catalog.security = security;
2946 advance_security_version(&mut next_catalog)?;
2947 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
2948 let commit_seq = {
2949 let mut wal = self.shared_wal.lock();
2950 if let Some(before_commit) = before_commit {
2951 before_commit()?;
2952 }
2953 let append: Result<u64> = (|| {
2954 wal.append(
2955 txn_id,
2956 WAL_TABLE_ID,
2957 crate::wal::Op::Ddl(DdlOp::SetSecurityCatalog {
2958 security_json: payload,
2959 }),
2960 )?;
2961 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
2962 wal.append_commit(txn_id, epoch, &[])
2963 })();
2964 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
2965 };
2966 self.await_durable_commit(commit_seq, epoch)?;
2967 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
2968 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
2969 Ok(epoch)
2970 }
2971
2972 pub fn require_for(
2973 &self,
2974 principal: Option<&crate::auth::Principal>,
2975 permission: &crate::auth::Permission,
2976 ) -> Result<()> {
2977 let Some(principal) = principal else {
2978 return self.require(permission);
2979 };
2980 let resolved;
2981 let principal = if self.auth_state.require_auth() || principal.user_id != 0 {
2982 resolved = Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
2983 .ok_or(MongrelError::AuthRequired)?;
2984 &resolved
2985 } else {
2986 principal
2987 };
2988 #[cfg(test)]
2989 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
2990 if principal.has_permission(permission) {
2991 Ok(())
2992 } else {
2993 Err(MongrelError::PermissionDenied {
2994 required: permission.clone(),
2995 principal: principal.username.clone(),
2996 })
2997 }
2998 }
2999
3000 fn require_exact_principal_current(
3004 &self,
3005 principal: Option<&crate::auth::Principal>,
3006 permission: &crate::auth::Permission,
3007 ) -> Result<()> {
3008 let catalog = self.catalog.read();
3009 if !catalog.require_auth {
3010 return Ok(());
3011 }
3012 let supplied = principal.ok_or(MongrelError::AuthRequired)?;
3013 let current = Self::resolve_bound_principal_from_catalog(&catalog, supplied)
3014 .ok_or(MongrelError::AuthRequired)?;
3015 if current.has_permission(permission) {
3016 Ok(())
3017 } else {
3018 Err(MongrelError::PermissionDenied {
3019 required: permission.clone(),
3020 principal: current.username,
3021 })
3022 }
3023 }
3024
3025 pub(crate) fn with_exact_principal_current<T, F>(
3026 &self,
3027 principal: Option<&crate::auth::Principal>,
3028 permission: &crate::auth::Permission,
3029 operation: F,
3030 ) -> Result<T>
3031 where
3032 F: FnOnce() -> Result<T>,
3033 {
3034 let _security = self.security_coordinator.gate.read();
3035 self.require_exact_principal_current(principal, permission)?;
3036 operation()
3037 }
3038
3039 pub fn principal_snapshot(&self) -> Option<crate::auth::Principal> {
3040 self.principal.read().clone()
3041 }
3042
3043 #[cfg(test)]
3044 pub(crate) fn set_cached_principal_for_test(&self, principal: Option<crate::auth::Principal>) {
3045 *self.principal.write() = principal.clone();
3046 self.auth_state.set_principal(principal);
3047 }
3048
3049 pub fn require_columns_for(
3050 &self,
3051 table: &str,
3052 operation: crate::auth::ColumnOperation,
3053 column_ids: &[u16],
3054 principal: Option<&crate::auth::Principal>,
3055 ) -> Result<()> {
3056 if principal.is_none() && !self.auth_state.require_auth() {
3057 return Ok(());
3058 }
3059 let cached = self.principal.read().clone();
3060 let principal = principal.or(cached.as_ref());
3061 let Some(principal) = principal else {
3062 let permission = match operation {
3063 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
3064 table: table.to_string(),
3065 },
3066 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
3067 table: table.to_string(),
3068 },
3069 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
3070 table: table.to_string(),
3071 },
3072 };
3073 return self.require(&permission);
3074 };
3075 let catalog = self.catalog.read();
3076 let resolved;
3077 let principal = if catalog.require_auth || principal.user_id != 0 {
3078 resolved = Self::resolve_bound_principal_from_catalog(&catalog, principal)
3079 .ok_or(MongrelError::AuthRequired)?;
3080 &resolved
3081 } else {
3082 principal
3083 };
3084 let schema = &catalog
3085 .live(table)
3086 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
3087 .schema;
3088 Self::require_columns_for_principal(table, schema, operation, column_ids, principal)
3089 }
3090
3091 fn require_columns_for_principal(
3092 table: &str,
3093 schema: &Schema,
3094 operation: crate::auth::ColumnOperation,
3095 column_ids: &[u16],
3096 principal: &crate::auth::Principal,
3097 ) -> Result<()> {
3098 #[cfg(test)]
3099 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
3100 match principal.column_access(table, operation) {
3101 crate::auth::ColumnAccess::All => Ok(()),
3102 crate::auth::ColumnAccess::Columns(allowed) => {
3103 let denied = column_ids.iter().find_map(|column_id| {
3104 schema
3105 .columns
3106 .iter()
3107 .find(|column| column.id == *column_id)
3108 .filter(|column| !allowed.contains(&column.name))
3109 });
3110 if denied.is_none() {
3111 Ok(())
3112 } else {
3113 Err(MongrelError::PermissionDenied {
3114 required: match operation {
3115 crate::auth::ColumnOperation::Select => {
3116 crate::auth::Permission::SelectColumns {
3117 table: table.to_string(),
3118 columns: denied
3119 .into_iter()
3120 .map(|column| column.name.clone())
3121 .collect(),
3122 }
3123 }
3124 crate::auth::ColumnOperation::Insert => {
3125 crate::auth::Permission::InsertColumns {
3126 table: table.to_string(),
3127 columns: denied
3128 .into_iter()
3129 .map(|column| column.name.clone())
3130 .collect(),
3131 }
3132 }
3133 crate::auth::ColumnOperation::Update => {
3134 crate::auth::Permission::UpdateColumns {
3135 table: table.to_string(),
3136 columns: denied
3137 .into_iter()
3138 .map(|column| column.name.clone())
3139 .collect(),
3140 }
3141 }
3142 },
3143 principal: principal.username.clone(),
3144 })
3145 }
3146 }
3147 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
3148 required: match operation {
3149 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
3150 table: table.to_string(),
3151 },
3152 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
3153 table: table.to_string(),
3154 },
3155 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
3156 table: table.to_string(),
3157 },
3158 },
3159 principal: principal.username.clone(),
3160 }),
3161 }
3162 }
3163
3164 pub fn select_column_ids_for(
3165 &self,
3166 table: &str,
3167 principal: Option<&crate::auth::Principal>,
3168 ) -> Result<Vec<u16>> {
3169 let catalog = self.catalog.read();
3170 let columns = catalog
3171 .live(table)
3172 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
3173 .schema
3174 .columns
3175 .iter()
3176 .map(|column| (column.id, column.name.clone()))
3177 .collect::<Vec<_>>();
3178 let principal = self.principal_for_authorized_read(&catalog, principal, false)?;
3179 drop(catalog);
3180 let Some(principal) = principal.as_ref() else {
3181 self.require(&crate::auth::Permission::Select {
3182 table: table.to_string(),
3183 })?;
3184 return Ok(columns.iter().map(|(id, _)| *id).collect());
3185 };
3186 match principal.column_access(table, crate::auth::ColumnOperation::Select) {
3187 crate::auth::ColumnAccess::All => Ok(columns.iter().map(|(id, _)| *id).collect()),
3188 crate::auth::ColumnAccess::Columns(allowed) => Ok(columns
3189 .iter()
3190 .filter(|(_, name)| allowed.contains(name))
3191 .map(|(id, _)| *id)
3192 .collect()),
3193 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
3194 required: crate::auth::Permission::Select {
3195 table: table.to_string(),
3196 },
3197 principal: principal.username.clone(),
3198 }),
3199 }
3200 }
3201
3202 pub fn secure_rows_for(
3203 &self,
3204 table: &str,
3205 rows: Vec<crate::memtable::Row>,
3206 principal: Option<&crate::auth::Principal>,
3207 ) -> Result<Vec<crate::memtable::Row>> {
3208 self.secure_rows_for_with_context(table, rows, principal, None)
3209 }
3210
3211 pub fn secure_rows_for_with_context(
3212 &self,
3213 table: &str,
3214 rows: Vec<crate::memtable::Row>,
3215 principal: Option<&crate::auth::Principal>,
3216 context: Option<&crate::query::AiExecutionContext>,
3217 ) -> Result<Vec<crate::memtable::Row>> {
3218 let (security, principal) = {
3219 let catalog = self.catalog.read();
3220 (
3221 catalog.security.clone(),
3222 self.principal_for_authorized_read(&catalog, principal, false)?,
3223 )
3224 };
3225 if !security.table_has_security(table) {
3226 return Ok(rows);
3227 }
3228 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
3229 let mut output = Vec::new();
3230 for mut row in rows {
3231 if let Some(context) = context {
3232 context.consume(1)?;
3233 }
3234 if security.row_allowed(
3235 table,
3236 crate::security::PolicyCommand::Select,
3237 &row,
3238 principal,
3239 false,
3240 ) {
3241 security.apply_masks(table, &mut row, principal);
3242 output.push(row);
3243 }
3244 }
3245 Ok(output)
3246 }
3247
3248 pub fn mask_search_hits_for(
3251 &self,
3252 table: &str,
3253 hits: &mut [crate::query::SearchHit],
3254 principal: Option<&crate::auth::Principal>,
3255 ) -> Result<()> {
3256 let (security, principal) = {
3257 let catalog = self.catalog.read();
3258 (
3259 catalog.security.clone(),
3260 self.principal_for_authorized_read(&catalog, principal, false)?,
3261 )
3262 };
3263 if !security.table_has_security(table) {
3264 return Ok(());
3265 }
3266 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
3267 for hit in hits {
3268 security.apply_masks_to_cells(table, &mut hit.cells, principal);
3269 }
3270 Ok(())
3271 }
3272
3273 pub fn mask_rows_for(
3275 &self,
3276 table: &str,
3277 rows: &mut [crate::memtable::Row],
3278 principal: Option<&crate::auth::Principal>,
3279 ) -> Result<()> {
3280 let (security, principal) = {
3281 let catalog = self.catalog.read();
3282 (
3283 catalog.security.clone(),
3284 self.principal_for_authorized_read(&catalog, principal, false)?,
3285 )
3286 };
3287 if !security.table_has_security(table) {
3288 return Ok(());
3289 }
3290 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
3291 for row in rows {
3292 security.apply_masks(table, row, principal);
3293 }
3294 Ok(())
3295 }
3296
3297 pub fn authorized_candidate_ids_for(
3299 &self,
3300 table: &str,
3301 principal: Option<&crate::auth::Principal>,
3302 ) -> Result<Option<std::collections::HashSet<RowId>>> {
3303 Ok(self
3304 .authorized_read_snapshot(table, principal)?
3305 .allowed_row_ids)
3306 }
3307
3308 fn allowed_row_ids_locked(
3309 &self,
3310 table_name: &str,
3311 table: &Table,
3312 table_snapshot: Snapshot,
3313 security_state: (&crate::security::SecurityCatalog, u64),
3314 principal: Option<&crate::auth::Principal>,
3315 context: Option<&crate::query::AiExecutionContext>,
3316 ) -> Result<Option<Arc<HashSet<RowId>>>> {
3317 let (security, security_version) = security_state;
3318 if !security.rls_enabled(table_name) {
3319 return Ok(None);
3320 }
3321 let authorization_started = std::time::Instant::now();
3322 let principal = principal.ok_or(MongrelError::AuthRequired)?;
3323 let mut roles = principal.roles.clone();
3324 roles.sort_unstable();
3325 let principal_key = format!(
3326 "{}:{}:{}:{}:{roles:?}",
3327 principal.user_id, principal.created_epoch, principal.username, principal.is_admin
3328 );
3329 let cache_key = (
3330 table_name.to_string(),
3331 table.data_generation(),
3332 security_version,
3333 principal_key,
3334 );
3335 if let Some(allowed) = self.rls_cache.lock().get(&cache_key) {
3336 crate::trace::QueryTrace::record(|trace| {
3337 trace.rls_cache_hit = true;
3338 trace.authorization_nanos = trace
3339 .authorization_nanos
3340 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
3341 });
3342 return Ok(Some(allowed));
3343 }
3344 if let Some(context) = context {
3345 context.checkpoint()?;
3346 }
3347 let started = std::time::Instant::now();
3349 let rows = table.visible_rows(table_snapshot)?;
3350 let rows_evaluated = rows.len() as u64;
3351 let mut allowed = HashSet::new();
3352 for chunk in rows.chunks(256) {
3353 if let Some(context) = context {
3354 context.consume(chunk.len())?;
3355 }
3356 allowed.extend(chunk.iter().filter_map(|row| {
3357 security
3358 .row_allowed(
3359 table_name,
3360 crate::security::PolicyCommand::Select,
3361 row,
3362 principal,
3363 false,
3364 )
3365 .then_some(row.row_id)
3366 }));
3367 }
3368 let allowed = Arc::new(allowed);
3369 let mut cache = self.rls_cache.lock();
3370 cache.build_nanos = cache
3371 .build_nanos
3372 .saturating_add(started.elapsed().as_nanos() as u64);
3373 cache.rows_evaluated = cache.rows_evaluated.saturating_add(rows_evaluated);
3374 cache.insert(cache_key, Arc::clone(&allowed));
3375 crate::trace::QueryTrace::record(|trace| {
3376 trace.rls_rows_evaluated = trace
3377 .rls_rows_evaluated
3378 .saturating_add(rows_evaluated as usize);
3379 trace.authorization_nanos = trace
3380 .authorization_nanos
3381 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
3382 });
3383 Ok(Some(allowed))
3384 }
3385
3386 fn principal_for_authorized_read(
3387 &self,
3388 catalog: &Catalog,
3389 principal: Option<&crate::auth::Principal>,
3390 catalog_bound: bool,
3391 ) -> Result<Option<crate::auth::Principal>> {
3392 let principal = principal.cloned().or_else(|| self.principal.read().clone());
3393 let Some(principal) = principal else {
3394 return Ok(None);
3395 };
3396 if catalog.require_auth || catalog_bound || principal.user_id != 0 {
3397 return Self::resolve_bound_principal_from_catalog(catalog, &principal)
3398 .map(Some)
3399 .ok_or(MongrelError::AuthRequired);
3400 }
3401 Ok(Some(principal))
3402 }
3403
3404 pub fn with_authorized_read<T, F>(
3408 &self,
3409 table_name: &str,
3410 principal: Option<&crate::auth::Principal>,
3411 catalog_bound: bool,
3412 read: F,
3413 ) -> Result<T>
3414 where
3415 F: FnMut(
3416 &mut Table,
3417 Snapshot,
3418 Option<&HashSet<RowId>>,
3419 Option<&crate::auth::Principal>,
3420 ) -> Result<T>,
3421 {
3422 self.with_authorized_read_context(
3423 table_name,
3424 principal,
3425 catalog_bound,
3426 None,
3427 None,
3428 None,
3429 read,
3430 )
3431 }
3432
3433 #[allow(clippy::too_many_arguments)]
3434 pub fn with_authorized_read_context<T, F>(
3435 &self,
3436 table_name: &str,
3437 principal: Option<&crate::auth::Principal>,
3438 catalog_bound: bool,
3439 authorization: Option<&ReadAuthorization>,
3440 context: Option<&crate::query::AiExecutionContext>,
3441 snapshot_override: Option<Snapshot>,
3442 read: F,
3443 ) -> Result<T>
3444 where
3445 F: FnMut(
3446 &mut Table,
3447 Snapshot,
3448 Option<&HashSet<RowId>>,
3449 Option<&crate::auth::Principal>,
3450 ) -> Result<T>,
3451 {
3452 self.with_authorized_read_context_stamped(
3453 table_name,
3454 principal,
3455 catalog_bound,
3456 authorization,
3457 context,
3458 snapshot_override,
3459 read,
3460 )
3461 .map(|(result, _)| result)
3462 }
3463
3464 #[allow(clippy::too_many_arguments)]
3465 pub fn with_authorized_read_context_stamped<T, F>(
3466 &self,
3467 table_name: &str,
3468 principal: Option<&crate::auth::Principal>,
3469 catalog_bound: bool,
3470 authorization: Option<&ReadAuthorization>,
3471 context: Option<&crate::query::AiExecutionContext>,
3472 snapshot_override: Option<Snapshot>,
3473 mut read: F,
3474 ) -> Result<(T, AuthorizedReadStamp)>
3475 where
3476 F: FnMut(
3477 &mut Table,
3478 Snapshot,
3479 Option<&HashSet<RowId>>,
3480 Option<&crate::auth::Principal>,
3481 ) -> Result<T>,
3482 {
3483 if principal.is_none() && self.principal.read().is_some() {
3484 self.refresh_principal()?;
3485 }
3486 const RETRIES: usize = 3;
3487 let handle = self.table(table_name)?;
3488 for attempt in 0..RETRIES {
3489 crate::trace::QueryTrace::record(|trace| {
3490 trace.authorization_retries = attempt;
3491 });
3492 let (security, security_version, effective_principal) = {
3493 let catalog = self.catalog.read();
3494 (
3495 catalog.security.clone(),
3496 catalog.security_version,
3497 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
3498 )
3499 };
3500 if let Some(authorization) = authorization {
3501 for permission in &authorization.permissions {
3502 self.require_for(effective_principal.as_ref(), permission)?;
3503 }
3504 self.require_columns_for(
3505 table_name,
3506 authorization.operation,
3507 &authorization.columns,
3508 effective_principal.as_ref(),
3509 )?;
3510 }
3511 let result = {
3512 let mut table = lock_table_with_context(&handle, context)?;
3513 let snapshot = snapshot_override.unwrap_or_else(|| table.snapshot());
3514 let allowed = self.allowed_row_ids_locked(
3515 table_name,
3516 &table,
3517 snapshot,
3518 (&security, security_version),
3519 effective_principal.as_ref(),
3520 context,
3521 )?;
3522 let stamp = AuthorizedReadStamp {
3523 table_id: table.table_id(),
3524 schema_id: table.schema().schema_id,
3525 data_generation: table.data_generation(),
3526 security_version,
3527 snapshot,
3528 };
3529 let result = read(
3530 &mut table,
3531 snapshot,
3532 allowed.as_deref(),
3533 effective_principal.as_ref(),
3534 )?;
3535 (result, stamp)
3536 };
3537 if let Some(context) = context {
3538 context.checkpoint()?;
3539 }
3540 if self.catalog.read().security_version == security_version {
3541 return Ok(result);
3542 }
3543 if attempt + 1 == RETRIES {
3544 return Err(MongrelError::Conflict(
3545 "security policy changed during scored read".into(),
3546 ));
3547 }
3548 }
3549 Err(MongrelError::Conflict(
3550 "authorization retry loop exhausted".into(),
3551 ))
3552 }
3553
3554 fn with_authorized_aggregate_table<T, F>(
3555 &self,
3556 table_name: &str,
3557 columns: &[u16],
3558 principal: Option<&crate::auth::Principal>,
3559 catalog_bound: bool,
3560 allow_table_security: bool,
3561 mut aggregate: F,
3562 ) -> Result<T>
3563 where
3564 F: FnMut(
3565 &mut Table,
3566 Option<&crate::security::CandidateAuthorization<'_>>,
3567 Option<&crate::auth::Principal>,
3568 u64,
3569 ) -> Result<T>,
3570 {
3571 if principal.is_none() && self.principal.read().is_some() {
3572 self.refresh_principal()?;
3573 }
3574 const RETRIES: usize = 3;
3575 let handle = self.table(table_name)?;
3576 for attempt in 0..RETRIES {
3577 let (security, security_version, effective_principal) = {
3578 let catalog = self.catalog.read();
3579 (
3580 catalog.security.clone(),
3581 catalog.security_version,
3582 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
3583 )
3584 };
3585 self.require_columns_for(
3586 table_name,
3587 crate::auth::ColumnOperation::Select,
3588 columns,
3589 effective_principal.as_ref(),
3590 )?;
3591 if !allow_table_security && security.table_has_security(table_name) {
3592 return Err(MongrelError::InvalidArgument(
3593 "incremental aggregate is unsupported while RLS or column masks are active"
3594 .into(),
3595 ));
3596 }
3597 let result = {
3598 let mut table = handle.lock();
3599 let authorization = if security.rls_enabled(table_name) {
3600 Some(crate::security::CandidateAuthorization {
3601 table: table_name,
3602 security: &security,
3603 principal: effective_principal
3604 .as_ref()
3605 .ok_or(MongrelError::AuthRequired)?,
3606 })
3607 } else {
3608 None
3609 };
3610 aggregate(
3611 &mut table,
3612 authorization.as_ref(),
3613 effective_principal.as_ref(),
3614 security_version,
3615 )?
3616 };
3617 if self.catalog.read().security_version == security_version {
3618 return Ok(result);
3619 }
3620 if attempt + 1 == RETRIES {
3621 return Err(MongrelError::Conflict(
3622 "security policy changed during aggregate read".into(),
3623 ));
3624 }
3625 }
3626 Err(MongrelError::Conflict(
3627 "aggregate authorization retry loop exhausted".into(),
3628 ))
3629 }
3630
3631 pub fn with_authorized_scored_read_context<T, F>(
3635 &self,
3636 table_name: &str,
3637 principal: Option<&crate::auth::Principal>,
3638 catalog_bound: bool,
3639 authorization: Option<&ReadAuthorization>,
3640 context: Option<&crate::query::AiExecutionContext>,
3641 mut read: F,
3642 ) -> Result<T>
3643 where
3644 F: FnMut(
3645 &mut Table,
3646 Snapshot,
3647 Option<&crate::security::CandidateAuthorization<'_>>,
3648 Option<&crate::auth::Principal>,
3649 ) -> Result<T>,
3650 {
3651 self.with_authorized_scored_read_context_at(
3652 table_name,
3653 principal,
3654 catalog_bound,
3655 authorization,
3656 context,
3657 None,
3658 |table, snapshot, authorization, principal| {
3659 let mut table = table.clone();
3660 read(&mut table, snapshot, authorization, principal)
3661 },
3662 )
3663 }
3664
3665 #[allow(clippy::too_many_arguments)]
3666 pub fn with_authorized_scored_read_context_at<T, F>(
3667 &self,
3668 table_name: &str,
3669 principal: Option<&crate::auth::Principal>,
3670 catalog_bound: bool,
3671 authorization: Option<&ReadAuthorization>,
3672 context: Option<&crate::query::AiExecutionContext>,
3673 snapshot_override: Option<Snapshot>,
3674 read: F,
3675 ) -> Result<T>
3676 where
3677 F: FnMut(
3678 &Table,
3679 Snapshot,
3680 Option<&crate::security::CandidateAuthorization<'_>>,
3681 Option<&crate::auth::Principal>,
3682 ) -> Result<T>,
3683 {
3684 self.with_authorized_scored_read_context_at_stamped(
3685 table_name,
3686 principal,
3687 catalog_bound,
3688 authorization,
3689 context,
3690 snapshot_override,
3691 read,
3692 )
3693 .map(|(result, _)| result)
3694 }
3695
3696 #[allow(clippy::too_many_arguments)]
3697 pub fn with_authorized_scored_read_context_at_stamped<T, F>(
3698 &self,
3699 table_name: &str,
3700 principal: Option<&crate::auth::Principal>,
3701 catalog_bound: bool,
3702 authorization: Option<&ReadAuthorization>,
3703 context: Option<&crate::query::AiExecutionContext>,
3704 snapshot_override: Option<Snapshot>,
3705 mut read: F,
3706 ) -> Result<(T, AuthorizedReadStamp)>
3707 where
3708 F: FnMut(
3709 &Table,
3710 Snapshot,
3711 Option<&crate::security::CandidateAuthorization<'_>>,
3712 Option<&crate::auth::Principal>,
3713 ) -> Result<T>,
3714 {
3715 if principal.is_none() && self.principal.read().is_some() {
3716 self.refresh_principal()?;
3717 }
3718 const RETRIES: usize = 3;
3719 let handle = self.table(table_name)?;
3720 for attempt in 0..RETRIES {
3721 if let Some(context) = context {
3722 context.checkpoint()?;
3723 }
3724 crate::trace::QueryTrace::record(|trace| {
3725 trace.authorization_retries = attempt;
3726 });
3727 let (security, security_version, effective_principal) = {
3728 let catalog = self.catalog.read();
3729 (
3730 catalog.security.clone(),
3731 catalog.security_version,
3732 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
3733 )
3734 };
3735 if let Some(authorization) = authorization {
3736 for permission in &authorization.permissions {
3737 self.require_for(effective_principal.as_ref(), permission)?;
3738 }
3739 self.require_columns_for(
3740 table_name,
3741 authorization.operation,
3742 &authorization.columns,
3743 effective_principal.as_ref(),
3744 )?;
3745 }
3746 let result = {
3747 let (table, snapshot, _snapshot_guard, _run_pins) =
3748 self.scored_read_generation(&handle, context, snapshot_override)?;
3749 let candidate_authorization = if security.rls_enabled(table_name) {
3750 Some(crate::security::CandidateAuthorization {
3751 table: table_name,
3752 security: &security,
3753 principal: effective_principal
3754 .as_ref()
3755 .ok_or(MongrelError::AuthRequired)?,
3756 })
3757 } else {
3758 None
3759 };
3760 let stamp = AuthorizedReadStamp {
3761 table_id: table.table_id(),
3762 schema_id: table.schema().schema_id,
3763 data_generation: table.data_generation(),
3764 security_version,
3765 snapshot,
3766 };
3767 let result = read(
3768 table.as_ref(),
3769 snapshot,
3770 candidate_authorization.as_ref(),
3771 effective_principal.as_ref(),
3772 )?;
3773 (result, stamp)
3774 };
3775 if let Some(context) = context {
3776 context.checkpoint()?;
3777 }
3778 if self.catalog.read().security_version == security_version {
3779 return Ok(result);
3780 }
3781 if attempt + 1 == RETRIES {
3782 return Err(MongrelError::Conflict(
3783 "security policy changed during scored read".into(),
3784 ));
3785 }
3786 }
3787 Err(MongrelError::Conflict(
3788 "scored-read authorization retry loop exhausted".into(),
3789 ))
3790 }
3791
3792 fn scored_read_generation(
3793 &self,
3794 handle: &TableHandle,
3795 context: Option<&crate::query::AiExecutionContext>,
3796 snapshot_override: Option<Snapshot>,
3797 ) -> Result<(
3798 Arc<TableReadGeneration>,
3799 Snapshot,
3800 crate::retention::OwnedSnapshotGuard,
3801 RunPins,
3802 )> {
3803 let mut table = if let Some(context) = context {
3804 loop {
3805 context.checkpoint()?;
3806 let wait = context
3807 .remaining_duration()
3808 .unwrap_or(std::time::Duration::from_millis(5))
3809 .min(std::time::Duration::from_millis(5));
3810 if let Some(table) = handle.try_lock_for(wait) {
3811 break table;
3812 }
3813 }
3814 } else {
3815 handle.lock()
3816 };
3817 let (snapshot, snapshot_guard) = if let Some(snapshot) = snapshot_override {
3818 self.snapshot_at_owned(snapshot.epoch)?
3819 } else {
3820 let snapshot = table.snapshot();
3821 let guard = self.snapshots.register_owned(snapshot.epoch);
3822 (snapshot, guard)
3823 };
3824 let table_id = table.table_id();
3825 let run_keys: Vec<_> = table
3826 .active_run_ids()
3827 .map(|run_id| (table_id, run_id))
3828 .collect();
3829 let generation = handle
3830 .generation_metrics
3831 .activate(table.clone_read_generation()?);
3832 let run_pins = self.pin_runs(&run_keys);
3833 Ok((generation, snapshot, snapshot_guard, run_pins))
3834 }
3835
3836 fn pin_runs(&self, runs: &[(u64, u128)]) -> RunPins {
3837 let mut pins = self.backup_pins.lock();
3838 for run in runs {
3839 *pins.entry(*run).or_insert(0) += 1;
3840 }
3841 drop(pins);
3842 RunPins {
3843 pins: Arc::clone(&self.backup_pins),
3844 runs: runs.to_vec(),
3845 }
3846 }
3847
3848 pub fn query_for_current_principal(
3852 &self,
3853 table_name: &str,
3854 query: &crate::query::Query,
3855 projection: Option<&[u16]>,
3856 ) -> Result<Vec<crate::memtable::Row>> {
3857 let condition_columns = crate::query::condition_columns(&query.conditions);
3858 self.with_authorized_read(
3859 table_name,
3860 None,
3861 true,
3862 |table, snapshot, allowed, principal| {
3863 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
3864 self.require_columns_for(
3865 table_name,
3866 crate::auth::ColumnOperation::Select,
3867 &condition_columns,
3868 principal,
3869 )?;
3870 if let Some(projection) = projection {
3871 self.require_columns_for(
3872 table_name,
3873 crate::auth::ColumnOperation::Select,
3874 projection,
3875 principal,
3876 )?;
3877 }
3878 let mut rows = table.query_at_with_allowed(query, snapshot, allowed)?;
3879 let projection =
3880 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
3881 for row in &mut rows {
3882 row.columns.retain(|column, _| {
3883 allowed_columns.contains(column)
3884 && projection
3885 .as_ref()
3886 .is_none_or(|projection| projection.contains(column))
3887 });
3888 }
3889 self.secure_rows_for(table_name, rows, principal)
3890 },
3891 )
3892 }
3893
3894 pub fn approx_aggregate_for_current_principal(
3897 &self,
3898 table_name: &str,
3899 conditions: &[crate::query::Condition],
3900 column: Option<u16>,
3901 agg: crate::engine::ApproxAgg,
3902 z: f64,
3903 ) -> Result<Option<crate::engine::ApproxResult>> {
3904 if !z.is_finite() || z <= 0.0 {
3905 return Err(MongrelError::InvalidArgument(
3906 "z must be finite and > 0".into(),
3907 ));
3908 }
3909 let mut columns = crate::query::condition_columns(conditions);
3910 columns.extend(column);
3911 columns.sort_unstable();
3912 columns.dedup();
3913 self.with_authorized_aggregate_table(
3914 table_name,
3915 &columns,
3916 None,
3917 true,
3918 true,
3919 |table, authorization, _, _| {
3920 table.approx_aggregate_with_candidate_authorization(
3921 conditions,
3922 column,
3923 agg,
3924 z,
3925 authorization,
3926 )
3927 },
3928 )
3929 }
3930
3931 pub fn incremental_aggregate_for_current_principal(
3935 &self,
3936 table_name: &str,
3937 conditions: &[crate::query::Condition],
3938 column: Option<u16>,
3939 agg: crate::engine::NativeAgg,
3940 ) -> Result<crate::engine::IncrementalAggResult> {
3941 self.incremental_aggregate_for_principal(table_name, conditions, column, agg, None, true)
3942 }
3943
3944 pub fn incremental_aggregate_for_principal(
3948 &self,
3949 table_name: &str,
3950 conditions: &[crate::query::Condition],
3951 column: Option<u16>,
3952 agg: crate::engine::NativeAgg,
3953 principal: Option<&crate::auth::Principal>,
3954 catalog_bound: bool,
3955 ) -> Result<crate::engine::IncrementalAggResult> {
3956 let mut columns = crate::query::condition_columns(conditions);
3957 columns.extend(column);
3958 columns.sort_unstable();
3959 columns.dedup();
3960 self.with_authorized_aggregate_table(
3961 table_name,
3962 &columns,
3963 principal,
3964 catalog_bound,
3965 false,
3966 |table, _, principal, security_version| {
3967 let cache_key = incremental_aggregate_cache_key(
3968 table_name,
3969 conditions,
3970 column,
3971 agg,
3972 principal,
3973 security_version,
3974 );
3975 table.aggregate_incremental(cache_key, conditions, column, agg)
3976 },
3977 )
3978 }
3979
3980 pub fn get_for_current_principal(
3983 &self,
3984 table_name: &str,
3985 row_id: RowId,
3986 ) -> Result<Option<crate::memtable::Row>> {
3987 self.with_authorized_read(
3988 table_name,
3989 None,
3990 true,
3991 |table, snapshot, allowed, principal| {
3992 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
3993 let Some(row) = table.get(row_id, snapshot) else {
3994 return Ok(None);
3995 };
3996 if allowed.is_some_and(|allowed| !allowed.contains(&row.row_id)) {
3997 return Ok(None);
3998 }
3999 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
4000 if let Some(row) = rows.first_mut() {
4001 row.columns
4002 .retain(|column, _| allowed_columns.contains(column));
4003 }
4004 Ok(rows.pop())
4005 },
4006 )
4007 }
4008
4009 pub fn ann_rerank_for_current_principal(
4012 &self,
4013 table_name: &str,
4014 request: &crate::query::AnnRerankRequest,
4015 ) -> Result<Vec<crate::query::AnnRerankHit>> {
4016 self.with_authorized_scored_read_context_at(
4017 table_name,
4018 None,
4019 true,
4020 Some(&ReadAuthorization {
4021 operation: crate::auth::ColumnOperation::Select,
4022 columns: vec![request.column_id],
4023 permissions: Vec::new(),
4024 }),
4025 None,
4026 None,
4027 |table, snapshot, authorization, principal| {
4028 self.require_columns_for(
4029 table_name,
4030 crate::auth::ColumnOperation::Select,
4031 &[request.column_id],
4032 principal,
4033 )?;
4034 table.ann_rerank_at_with_candidate_authorization_on_generation(
4035 request,
4036 snapshot,
4037 authorization,
4038 None,
4039 )
4040 },
4041 )
4042 }
4043
4044 pub fn authorized_read_snapshot(
4047 &self,
4048 table: &str,
4049 principal: Option<&crate::auth::Principal>,
4050 ) -> Result<AuthorizedReadSnapshot> {
4051 let (security, security_version, effective_principal) = {
4052 let catalog = self.catalog.read();
4053 (
4054 catalog.security.clone(),
4055 catalog.security_version,
4056 self.principal_for_authorized_read(&catalog, principal, false)?,
4057 )
4058 };
4059 let handle = self.table(table)?;
4060 let (table_snapshot, data_generation, allowed_row_ids) = {
4061 let table_handle = handle.lock();
4062 let table_snapshot = table_handle.snapshot();
4063 let data_generation = table_handle.data_generation();
4064 let allowed = self.allowed_row_ids_locked(
4065 table,
4066 &table_handle,
4067 table_snapshot,
4068 (&security, security_version),
4069 effective_principal.as_ref(),
4070 None,
4071 )?;
4072 (
4073 table_snapshot,
4074 data_generation,
4075 allowed.map(|allowed| (*allowed).clone()),
4076 )
4077 };
4078 Ok(AuthorizedReadSnapshot {
4079 table: table.to_string(),
4080 table_snapshot,
4081 data_generation,
4082 security_version,
4083 allowed_row_ids,
4084 })
4085 }
4086
4087 pub fn authorized_read_snapshot_valid(&self, snapshot: &AuthorizedReadSnapshot) -> bool {
4088 if self.catalog.read().security_version != snapshot.security_version {
4089 return false;
4090 }
4091 self.table(&snapshot.table)
4092 .ok()
4093 .is_some_and(|table| table.lock().data_generation() == snapshot.data_generation)
4094 }
4095
4096 pub fn rls_cache_stats(&self) -> RlsCacheStats {
4097 self.rls_cache.lock().stats()
4098 }
4099
4100 pub fn rows_for(
4102 &self,
4103 table: &str,
4104 principal: Option<&crate::auth::Principal>,
4105 ) -> Result<Vec<crate::memtable::Row>> {
4106 if principal.is_none() && self.principal.read().is_some() {
4107 self.refresh_principal()?;
4108 }
4109 let allowed = self.select_column_ids_for(table, principal)?;
4110 let handle = self.table(table)?;
4111 let rows = {
4112 let table = handle.lock();
4113 table.visible_rows(table.snapshot())?
4114 };
4115 let mut rows = self.secure_rows_for(table, rows, principal)?;
4116 for row in &mut rows {
4117 row.columns.retain(|column, _| allowed.contains(column));
4118 }
4119 Ok(rows)
4120 }
4121
4122 pub fn rows_at_epoch_for_current_principal(
4125 &self,
4126 table_name: &str,
4127 snapshot: Snapshot,
4128 ) -> Result<Vec<crate::memtable::Row>> {
4129 self.with_authorized_read_context(
4130 table_name,
4131 None,
4132 true,
4133 Some(&ReadAuthorization {
4134 operation: crate::auth::ColumnOperation::Select,
4135 columns: Vec::new(),
4136 permissions: Vec::new(),
4137 }),
4138 None,
4139 Some(snapshot),
4140 |table, snapshot, allowed, principal| {
4141 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
4142 let mut rows = table.visible_rows(snapshot)?;
4143 if let Some(allowed) = allowed {
4144 rows.retain(|row| allowed.contains(&row.row_id));
4145 }
4146 rows = self.secure_rows_for(table_name, rows, principal)?;
4147 for row in &mut rows {
4148 row.columns
4149 .retain(|column, _| allowed_columns.contains(column));
4150 }
4151 Ok(rows)
4152 },
4153 )
4154 }
4155
4156 pub fn count_for(
4158 &self,
4159 table: &str,
4160 principal: Option<&crate::auth::Principal>,
4161 ) -> Result<u64> {
4162 if principal.is_none() && self.principal.read().is_some() {
4163 self.refresh_principal()?;
4164 }
4165 self.select_column_ids_for(table, principal)?;
4166 if self.security_active_for(table) {
4167 Ok(self.rows_for(table, principal)?.len() as u64)
4168 } else {
4169 Ok(self.table(table)?.lock().count())
4170 }
4171 }
4172
4173 pub fn put_for(
4175 &self,
4176 table: &str,
4177 mut cells: Vec<(u16, crate::memtable::Value)>,
4178 principal: Option<&crate::auth::Principal>,
4179 ) -> Result<RowId> {
4180 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
4181 self.require_columns_for(
4182 table,
4183 crate::auth::ColumnOperation::Insert,
4184 &columns,
4185 principal,
4186 )?;
4187 let handle = self.table(table)?;
4188 let mut table_handle = handle.lock();
4189 table_handle.fill_auto_inc(&mut cells)?;
4190 table_handle.apply_defaults(&mut cells)?;
4191 let mut row = crate::memtable::Row::new(RowId(0), self.epoch.visible());
4192 row.columns.extend(cells.iter().cloned());
4193 self.check_row_policy_for(
4194 table,
4195 crate::security::PolicyCommand::Insert,
4196 &row,
4197 true,
4198 principal,
4199 )?;
4200 table_handle.put(cells)
4201 }
4202
4203 pub fn check_row_policy_for(
4204 &self,
4205 table: &str,
4206 command: crate::security::PolicyCommand,
4207 row: &crate::memtable::Row,
4208 check_new: bool,
4209 principal: Option<&crate::auth::Principal>,
4210 ) -> Result<()> {
4211 let security = self.catalog.read().security.clone();
4212 if !security.rls_enabled(table) {
4213 return Ok(());
4214 }
4215 let cached = self.principal.read().clone();
4216 let principal = principal
4217 .or(cached.as_ref())
4218 .ok_or(MongrelError::AuthRequired)?;
4219 if security.row_allowed(table, command, row, principal, check_new) {
4220 return Ok(());
4221 }
4222 let required = match command {
4223 crate::security::PolicyCommand::Insert => crate::auth::Permission::Insert {
4224 table: table.to_string(),
4225 },
4226 crate::security::PolicyCommand::Update => crate::auth::Permission::Update {
4227 table: table.to_string(),
4228 },
4229 crate::security::PolicyCommand::Select => crate::auth::Permission::Select {
4230 table: table.to_string(),
4231 },
4232 crate::security::PolicyCommand::Delete | crate::security::PolicyCommand::All => {
4233 crate::auth::Permission::Delete {
4234 table: table.to_string(),
4235 }
4236 }
4237 };
4238 Err(MongrelError::PermissionDenied {
4239 required,
4240 principal: principal.username.clone(),
4241 })
4242 }
4243
4244 pub fn set_materialized_view(
4247 &self,
4248 definition: crate::catalog::MaterializedViewEntry,
4249 ) -> Result<()> {
4250 self.set_materialized_view_with_epoch(definition)
4251 .map(|_| ())
4252 }
4253
4254 pub fn set_materialized_view_with_epoch(
4256 &self,
4257 definition: crate::catalog::MaterializedViewEntry,
4258 ) -> Result<Epoch> {
4259 use crate::wal::DdlOp;
4260 use std::sync::atomic::Ordering;
4261
4262 self.require(&crate::auth::Permission::Ddl)?;
4263 if self.poisoned.load(Ordering::Relaxed) {
4264 return Err(MongrelError::Other(
4265 "database poisoned by fsync error".into(),
4266 ));
4267 }
4268 if definition.name.is_empty() || definition.query.trim().is_empty() {
4269 return Err(MongrelError::InvalidArgument(
4270 "materialized view name and query must not be empty".into(),
4271 ));
4272 }
4273
4274 let _ddl = self.ddl_lock.lock();
4275 let _security_write = self.security_write()?;
4276 self.require(&crate::auth::Permission::Ddl)?;
4277 let table_id = self
4278 .catalog
4279 .read()
4280 .live(&definition.name)
4281 .ok_or_else(|| {
4282 MongrelError::NotFound(format!(
4283 "materialized view table {:?} not found",
4284 definition.name
4285 ))
4286 })?
4287 .table_id;
4288 let definition_json = DdlOp::encode_materialized_view(&definition)?;
4289 let _commit = self.commit_lock.lock();
4290 let epoch = self.epoch.bump_assigned();
4291 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
4292 let txn_id = self.alloc_txn_id()?;
4293 let mut next_catalog = self.catalog.read().clone();
4294 if let Some(existing) = next_catalog
4295 .materialized_views
4296 .iter_mut()
4297 .find(|existing| existing.name == definition.name)
4298 {
4299 *existing = definition.clone();
4300 } else {
4301 next_catalog.materialized_views.push(definition.clone());
4302 }
4303 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
4304 let commit_seq = {
4305 let mut wal = self.shared_wal.lock();
4306 let append: Result<u64> = (|| {
4307 wal.append(
4308 txn_id,
4309 table_id,
4310 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
4311 name: definition.name.clone(),
4312 definition_json,
4313 }),
4314 )?;
4315 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
4316 wal.append_commit(txn_id, epoch, &[])
4317 })();
4318 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
4319 };
4320 self.await_durable_commit(commit_seq, epoch)?;
4321
4322 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
4323 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
4324 Ok(epoch)
4325 }
4326
4327 pub fn root(&self) -> &Path {
4329 self.durable_root.canonical_path()
4330 }
4331
4332 pub fn durable_root(&self) -> Arc<crate::durable_file::DurableRoot> {
4335 Arc::clone(&self.durable_root)
4336 }
4337
4338 #[cfg(feature = "encryption")]
4342 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
4343 self.kek
4344 .as_deref()
4345 .map(|kek| kek.derive_subkey(b"mongreldb/server/idempotency/v1"))
4346 }
4347
4348 #[cfg(not(feature = "encryption"))]
4349 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
4350 None
4351 }
4352
4353 pub fn is_read_only_replica(&self) -> bool {
4354 self.read_only
4355 }
4356
4357 pub fn ensure_consistent_read(&self) -> Result<()> {
4361 if self.poisoned.load(Ordering::Relaxed) {
4362 return Err(MongrelError::Other(
4363 "database poisoned by post-commit failure; reopen required".into(),
4364 ));
4365 }
4366 Ok(())
4367 }
4368
4369 pub fn set_replication_wal_retention_segments(&self, segments: usize) {
4370 self.replication_wal_retention_segments
4371 .store(segments, std::sync::atomic::Ordering::Relaxed);
4372 }
4373
4374 pub fn replication_snapshot(&self) -> Result<crate::replication::ReplicationSnapshot> {
4379 let admin = crate::auth::Permission::Admin;
4380 self.require(&admin)?;
4381 let operation_principal = self.principal_snapshot();
4382 let _barrier = self.replication_barrier.write();
4383 let _ddl = self.ddl_lock.lock();
4384 let _security = self.security_coordinator.gate.read();
4385 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4386 let mut handles: Vec<_> = self
4387 .tables
4388 .read()
4389 .iter()
4390 .map(|(id, handle)| (*id, handle.clone()))
4391 .collect();
4392 handles.sort_by_key(|(id, _)| *id);
4393 let _table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
4394 let _commit = self.commit_lock.lock();
4395 let mut wal = self.shared_wal.lock();
4396 wal.group_sync()?;
4397 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
4398 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
4399 let epoch = records
4400 .iter()
4401 .filter_map(|record| match &record.op {
4402 crate::wal::Op::TxnCommit { epoch, .. } => Some(*epoch),
4403 _ => None,
4404 })
4405 .max()
4406 .unwrap_or(0)
4407 .max(self.epoch.committed().0);
4408 let files = crate::replication::capture_files(&self.root)?;
4409 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
4410 drop(wal);
4411 Ok(crate::replication::ReplicationSnapshot::new(
4412 source_id, epoch, files,
4413 ))
4414 }
4415
4416 pub fn hot_backup(&self, destination: impl AsRef<Path>) -> Result<crate::backup::BackupReport> {
4424 let control = crate::ExecutionControl::new(None);
4425 self.hot_backup_controlled(destination, &control, || true)
4426 }
4427
4428 pub(crate) fn hot_backup_to_durable_child(
4429 &self,
4430 parent: &crate::durable_file::DurableRoot,
4431 child: &Path,
4432 control: &crate::ExecutionControl,
4433 ) -> Result<crate::backup::BackupReport> {
4434 let mut components = child.components();
4435 if !matches!(components.next(), Some(std::path::Component::Normal(_)))
4436 || components.next().is_some()
4437 {
4438 return Err(MongrelError::InvalidArgument(
4439 "durable backup child must be one relative path component".into(),
4440 ));
4441 }
4442 let destination_name = child.file_name().ok_or_else(|| {
4443 MongrelError::InvalidArgument("durable backup child has no filename".into())
4444 })?;
4445 let prepared = prepare_backup_destination_in(&self.root, parent, destination_name)?;
4446 self.hot_backup_prepared(prepared, control, || true)
4447 }
4448
4449 #[doc(hidden)]
4452 pub fn hot_backup_controlled<F>(
4453 &self,
4454 destination: impl AsRef<Path>,
4455 control: &crate::ExecutionControl,
4456 before_publish: F,
4457 ) -> Result<crate::backup::BackupReport>
4458 where
4459 F: FnOnce() -> bool,
4460 {
4461 let prepared = prepare_backup_destination(&self.root, destination.as_ref())?;
4462 self.hot_backup_prepared(prepared, control, before_publish)
4463 }
4464
4465 fn hot_backup_prepared<F>(
4466 &self,
4467 mut prepared: PreparedBackupDestination,
4468 control: &crate::ExecutionControl,
4469 before_publish: F,
4470 ) -> Result<crate::backup::BackupReport>
4471 where
4472 F: FnOnce() -> bool,
4473 {
4474 let admin = crate::auth::Permission::Admin;
4475 self.require(&admin)?;
4476 let operation_principal = self.principal_snapshot();
4477 control.checkpoint()?;
4478 let destination = prepared.destination_path.clone();
4479 let mut before_publish = Some(before_publish);
4480
4481 let outcome = (|| {
4482 control.checkpoint()?;
4483 let barrier = self.replication_barrier.write();
4484 let ddl = self.ddl_lock.lock();
4485 let security = self.security_coordinator.gate.read();
4486 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4487 let mut handles: Vec<_> = self
4488 .tables
4489 .read()
4490 .iter()
4491 .map(|(id, handle)| (*id, handle.clone()))
4492 .collect();
4493 handles.sort_by_key(|(id, _)| *id);
4494 let table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
4495 let commit = self.commit_lock.lock();
4496 let mut wal = self.shared_wal.lock();
4497 wal.group_sync()?;
4498 let epoch = self.epoch.committed().0;
4499 let boundary_unix_nanos = current_unix_nanos();
4500
4501 let pin_nonce = std::time::SystemTime::now()
4502 .duration_since(std::time::UNIX_EPOCH)
4503 .unwrap_or_default()
4504 .as_nanos();
4505 let file_pin_root = self
4506 .root
4507 .join(META_DIR)
4508 .join("backup-pins")
4509 .join(format!("{}-{pin_nonce}", std::process::id()));
4510 std::fs::create_dir_all(&file_pin_root)?;
4511 let _file_pins = BackupFilePins {
4512 root: file_pin_root.clone(),
4513 };
4514 let mut run_files = Vec::new();
4515 for (index, (table_id, _)) in handles.iter().enumerate() {
4516 if index % 256 == 0 {
4517 control.checkpoint()?;
4518 }
4519 let table = &table_guards[index];
4520 for (run_index, run) in table.run_refs().iter().enumerate() {
4521 if run_index % 256 == 0 {
4522 control.checkpoint()?;
4523 }
4524 let source = table.run_path(run.run_id as u64);
4525 let relative = Path::new(TABLES_DIR)
4526 .join(table_id.to_string())
4527 .join(crate::engine::RUNS_DIR)
4528 .join(format!("r-{}.sr", run.run_id));
4529 let pinned = file_pin_root.join(format!("{table_id}-{}.sr", run.run_id));
4530 if std::fs::hard_link(&source, &pinned).is_err() {
4531 crate::backup::copy_file_synced(&source, &pinned)?;
4532 }
4533 run_files.push(((*table_id, run.run_id), pinned, relative));
4534 }
4535 }
4536 crate::durable_file::sync_directory(&file_pin_root)?;
4537 let run_keys: Vec<_> = run_files.iter().map(|(key, _, _)| *key).collect();
4538 {
4539 let mut pins = self.backup_pins.lock();
4540 for key in &run_keys {
4541 *pins.entry(*key).or_insert(0) += 1;
4542 }
4543 }
4544 let _run_pins = RunPins {
4545 pins: Arc::clone(&self.backup_pins),
4546 runs: run_keys,
4547 };
4548 let deferred: HashSet<_> = run_files
4549 .iter()
4550 .map(|(_, _, relative)| relative.clone())
4551 .collect();
4552 let mut copied = Vec::new();
4553 copy_backup_boundary(
4554 &self.root,
4555 prepared.stage.as_deref().ok_or_else(|| {
4556 MongrelError::Other("backup staging root was already released".into())
4557 })?,
4558 &deferred,
4559 &mut copied,
4560 Some(control),
4561 )?;
4562
4563 drop(wal);
4564 drop(commit);
4565 drop(table_guards);
4566 drop(security);
4567 drop(ddl);
4568 drop(barrier);
4569
4570 if let Some(hook) = self.backup_hook.lock().as_ref() {
4571 hook();
4572 }
4573 for (index, (_, source, relative)) in run_files.into_iter().enumerate() {
4574 if index % 256 == 0 {
4575 control.checkpoint()?;
4576 }
4577 let mut source = crate::durable_file::open_regular_nofollow(&source)?;
4578 prepared
4579 .stage
4580 .as_deref()
4581 .ok_or_else(|| {
4582 MongrelError::Other("backup staging root was already released".into())
4583 })?
4584 .copy_new_from(&relative, &mut source)?;
4585 copied.push(relative);
4586 }
4587
4588 let manifest = crate::backup::BackupManifest::create_controlled_durable(
4589 prepared.stage.as_deref().ok_or_else(|| {
4590 MongrelError::Other("backup staging root was already released".into())
4591 })?,
4592 epoch,
4593 &copied,
4594 control,
4595 )?;
4596 manifest.write_to_durable(prepared.stage.as_deref().ok_or_else(|| {
4597 MongrelError::Other("backup staging root was already released".into())
4598 })?)?;
4599 control.checkpoint()?;
4600 let publish = before_publish.take().ok_or_else(|| {
4601 MongrelError::Other("backup publication callback already consumed".into())
4602 })?;
4603 if !publish() {
4604 return Err(MongrelError::Cancelled);
4605 }
4606 let final_security = self.security_coordinator.gate.read();
4607 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4608 drop(prepared.stage.take().ok_or_else(|| {
4612 MongrelError::Other("backup staging root was already released".into())
4613 })?);
4614 let published = std::cell::Cell::new(false);
4615 if let Err(error) = prepared.parent.rename_directory_new_with_after(
4616 Path::new(&prepared.stage_name),
4617 &prepared.parent,
4618 Path::new(&prepared.destination_name),
4619 || published.set(true),
4620 ) {
4621 if published.get() {
4622 return Err(MongrelError::CommitOutcomeUnknown {
4623 epoch,
4624 message: format!("backup publication was not durable: {error}"),
4625 });
4626 }
4627 return Err(error.into());
4628 }
4629 drop(final_security);
4630 Ok(crate::backup::BackupReport {
4631 destination,
4632 epoch,
4633 boundary_unix_nanos,
4634 files: manifest.files.len(),
4635 bytes: manifest.total_bytes(),
4636 })
4637 })();
4638
4639 if outcome.is_err() {
4640 drop(prepared.stage.take());
4641 let _ = prepared
4642 .parent
4643 .remove_directory_all(Path::new(&prepared.stage_name));
4644 }
4645 outcome
4646 }
4647
4648 pub fn replication_batch_since(
4651 &self,
4652 since_epoch: u64,
4653 ) -> Result<crate::replication::ReplicationBatch> {
4654 use crate::wal::Op;
4655
4656 let admin = crate::auth::Permission::Admin;
4657 self.require(&admin)?;
4658 let operation_principal = self.principal_snapshot();
4659
4660 let mut wal = self.shared_wal.lock();
4661 wal.group_sync()?;
4662 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
4663 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
4664 drop(wal);
4665
4666 let commits: HashMap<u64, u64> = records
4667 .iter()
4668 .filter_map(|record| match &record.op {
4669 Op::TxnCommit { epoch, .. } => Some((record.txn_id, *epoch)),
4670 _ => None,
4671 })
4672 .collect();
4673 let earliest_epoch = commits.values().copied().min();
4674 let current_epoch = commits
4675 .values()
4676 .copied()
4677 .max()
4678 .unwrap_or(0)
4679 .max(self.epoch.committed().0);
4680 let selected: HashSet<u64> = commits
4681 .iter()
4682 .filter_map(|(txn_id, epoch)| (*epoch > since_epoch).then_some(*txn_id))
4683 .collect();
4684 let retention_gap = since_epoch < current_epoch
4685 && since_epoch < crate::replication::replication_wal_floor(&self.root)?;
4686 let spilled = records.iter().any(|record| {
4687 selected.contains(&record.txn_id)
4688 && matches!(
4689 &record.op,
4690 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty()
4691 )
4692 });
4693 let records = records
4694 .into_iter()
4695 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
4696 .filter(|record| selected.contains(&record.txn_id))
4697 .collect();
4698 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
4699 let batch = crate::replication::ReplicationBatch::complete_for_source(
4700 source_id,
4701 since_epoch,
4702 current_epoch,
4703 earliest_epoch,
4704 retention_gap,
4705 spilled,
4706 records,
4707 )?;
4708 if let Some(hook) = self.replication_hook.lock().as_ref() {
4709 hook();
4710 }
4711 let _security = self.security_coordinator.gate.read();
4712 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4713 Ok(batch)
4714 }
4715
4716 pub fn append_replication_batch(
4720 &self,
4721 batch: &crate::replication::ReplicationBatch,
4722 ) -> Result<u64> {
4723 use crate::wal::Op;
4724
4725 if !self.read_only {
4726 return Err(MongrelError::InvalidArgument(
4727 "replication batches may only target a marked replica".into(),
4728 ));
4729 }
4730 let current = crate::replication::replica_epoch(&self.root)?;
4731 if batch.is_source_bound() {
4732 let source_id = crate::replication::replica_source_id_durable(&self.durable_root)?;
4733 if batch.source_id != source_id {
4734 return Err(MongrelError::Conflict(
4735 "replication batch source does not match follower binding".into(),
4736 ));
4737 }
4738 }
4739 if batch.requires_snapshot {
4740 return Err(MongrelError::Conflict(
4741 "replication snapshot required for this batch".into(),
4742 ));
4743 }
4744 batch.validate_proof()?;
4745 if batch.from_epoch != current {
4746 if batch.from_epoch < current && batch.current_epoch == current {
4747 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
4748 let _wal = self.shared_wal.lock();
4749 let existing: HashSet<(u64, u64)> =
4750 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
4751 .into_iter()
4752 .filter_map(|record| match record.op {
4753 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
4754 _ => None,
4755 })
4756 .collect();
4757 let already_applied = batch.records.iter().all(|record| match &record.op {
4758 Op::TxnCommit { epoch, .. } => existing.contains(&(record.txn_id, *epoch)),
4759 _ => true,
4760 });
4761 if already_applied {
4762 return Ok(current);
4763 }
4764 }
4765 return Err(MongrelError::Conflict(format!(
4766 "replication batch starts at epoch {}, follower is at epoch {current}",
4767 batch.from_epoch
4768 )));
4769 }
4770 if batch.current_epoch < current {
4771 return Err(MongrelError::InvalidArgument(format!(
4772 "replication batch current epoch {} precedes follower epoch {current}",
4773 batch.current_epoch
4774 )));
4775 }
4776 let records = &batch.records;
4777 let mut commits = HashMap::new();
4778 let mut commit_epochs = HashSet::new();
4779 let mut commit_timestamps = HashMap::new();
4780 for record in records {
4781 match &record.op {
4782 Op::TxnCommit { epoch, added_runs } => {
4783 if !added_runs.is_empty() {
4784 return Err(MongrelError::Conflict(
4785 "replication snapshot required for spilled-run transaction".into(),
4786 ));
4787 }
4788 if commits.insert(record.txn_id, *epoch).is_some() {
4789 return Err(MongrelError::InvalidArgument(format!(
4790 "duplicate commit for replication transaction {}",
4791 record.txn_id
4792 )));
4793 }
4794 if *epoch <= current || *epoch > batch.current_epoch {
4795 return Err(MongrelError::InvalidArgument(format!(
4796 "replication commit epoch {epoch} is outside ({current}, {}]",
4797 batch.current_epoch
4798 )));
4799 }
4800 if !commit_epochs.insert(*epoch) {
4801 return Err(MongrelError::InvalidArgument(format!(
4802 "duplicate replication commit epoch {epoch}"
4803 )));
4804 }
4805 }
4806 Op::CommitTimestamp { unix_nanos } => {
4807 commit_timestamps.insert(record.txn_id, *unix_nanos);
4808 }
4809 _ => {}
4810 }
4811 }
4812 for record in records {
4813 if record.txn_id == crate::wal::SYSTEM_TXN_ID
4814 || matches!(&record.op, Op::TxnAbort | Op::Flush { .. })
4815 {
4816 return Err(MongrelError::InvalidArgument(
4817 "replication batch contains a non-committed record".into(),
4818 ));
4819 }
4820 if !commits.contains_key(&record.txn_id) {
4821 return Err(MongrelError::InvalidArgument(format!(
4822 "incomplete replication transaction {}",
4823 record.txn_id
4824 )));
4825 }
4826 }
4827 let target_epoch = commits
4828 .values()
4829 .copied()
4830 .filter(|epoch| *epoch > current)
4831 .max()
4832 .unwrap_or(current);
4833 if target_epoch != batch.current_epoch {
4834 return Err(MongrelError::InvalidArgument(format!(
4835 "replication batch ends at epoch {target_epoch}, expected {}",
4836 batch.current_epoch
4837 )));
4838 }
4839 let mut selected: HashSet<u64> = commits
4840 .iter()
4841 .filter_map(|(txn_id, epoch)| (*epoch > current).then_some(*txn_id))
4842 .collect();
4843 if selected.is_empty() {
4844 return Ok(current);
4845 }
4846 let mut wal = self.shared_wal.lock();
4847 wal.group_sync()?;
4848 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
4849 let existing: HashSet<(u64, u64)> =
4850 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
4851 .into_iter()
4852 .filter_map(|record| match record.op {
4853 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
4854 _ => None,
4855 })
4856 .collect();
4857 selected.retain(|txn_id| {
4858 commits
4859 .get(txn_id)
4860 .is_some_and(|epoch| !existing.contains(&(*txn_id, *epoch)))
4861 });
4862 for record in records {
4863 if !selected.contains(&record.txn_id) {
4864 continue;
4865 }
4866 match &record.op {
4867 Op::TxnCommit { epoch, added_runs } => {
4868 let timestamp = commit_timestamps
4869 .get(&record.txn_id)
4870 .copied()
4871 .unwrap_or_else(current_unix_nanos);
4872 wal.append_commit_at(record.txn_id, Epoch(*epoch), added_runs, timestamp)?;
4873 }
4874 Op::TxnAbort | Op::Flush { .. } | Op::CommitTimestamp { .. } => {}
4875 op => {
4876 wal.append(record.txn_id, 0, op.clone())?;
4877 }
4878 }
4879 }
4880 if !selected.is_empty() {
4881 wal.group_sync()?;
4882 }
4883 drop(wal);
4884
4885 let mut recovered_catalog = self.catalog.read().clone();
4889 if let Err(error) = recover_ddl_from_wal(
4890 &self.root,
4891 Some(&self.durable_root),
4892 &mut recovered_catalog,
4893 self.meta_dek.as_ref(),
4894 wal_dek.as_ref(),
4895 true,
4896 None,
4897 ) {
4898 return Err(MongrelError::DurableCommit {
4899 epoch: target_epoch,
4900 message: format!(
4901 "replication WAL is durable but catalog checkpoint failed: {error}"
4902 ),
4903 });
4904 }
4905 let _security = self.security_coordinator.gate.write();
4906 let old_security_version = self.catalog.read().security_version;
4907 let security_changed = old_security_version != recovered_catalog.security_version
4908 || self.catalog.read().require_auth != recovered_catalog.require_auth;
4909 let require_auth = recovered_catalog.require_auth;
4910 let principal = if security_changed {
4911 None
4912 } else {
4913 self.principal.read().as_ref().and_then(|principal| {
4914 Self::resolve_bound_principal_from_catalog(&recovered_catalog, principal)
4915 })
4916 };
4917 if require_auth {
4918 self.auth_state.set_require_auth(true);
4919 }
4920 self.auth_state.set_principal(principal.clone());
4921 *self.principal.write() = principal;
4922 let security_version = recovered_catalog.security_version;
4923 *self.catalog.write() = recovered_catalog;
4924 self.security_coordinator
4925 .version
4926 .store(security_version, Ordering::Release);
4927 if !require_auth {
4928 self.auth_state.set_require_auth(false);
4929 }
4930 if let Err(error) =
4931 crate::replication::reconcile_replica_epoch_durable(&self.durable_root, target_epoch)
4932 {
4933 return Err(MongrelError::DurableCommit {
4934 epoch: target_epoch,
4935 message: format!(
4936 "replication WAL and catalog are durable but follower watermark failed: {error}"
4937 ),
4938 });
4939 }
4940 Ok(target_epoch)
4941 }
4942
4943 pub fn table_id(&self, name: &str) -> Result<u64> {
4946 let cat = self.catalog.read();
4947 cat.live(name)
4948 .map(|e| e.table_id)
4949 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
4950 }
4951
4952 pub fn table_identity(&self, name: &str) -> Result<(u64, u64)> {
4957 let catalog = self.catalog.read();
4958 catalog
4959 .live(name)
4960 .map(|entry| (entry.table_id, entry.schema.schema_id))
4961 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
4962 }
4963
4964 pub(crate) fn building_table_id(&self, name: &str) -> Result<u64> {
4965 self.catalog
4966 .read()
4967 .building(name)
4968 .map(|entry| entry.table_id)
4969 .ok_or_else(|| MongrelError::NotFound(format!("building table {name:?} not found")))
4970 }
4971
4972 pub fn procedures(&self) -> Vec<StoredProcedure> {
4973 self.catalog
4974 .read()
4975 .procedures
4976 .iter()
4977 .map(|p| p.procedure.clone())
4978 .collect()
4979 }
4980
4981 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
4982 self.catalog
4983 .read()
4984 .procedures
4985 .iter()
4986 .find(|p| p.procedure.name == name)
4987 .map(|p| p.procedure.clone())
4988 }
4989
4990 pub fn create_procedure(&self, procedure: StoredProcedure) -> Result<StoredProcedure> {
4991 self.create_procedure_inner(procedure, None)
4992 }
4993
4994 pub fn create_procedure_controlled<F>(
4995 &self,
4996 procedure: StoredProcedure,
4997 mut before_publish: F,
4998 ) -> Result<StoredProcedure>
4999 where
5000 F: FnMut() -> Result<()>,
5001 {
5002 self.create_procedure_inner(procedure, Some(&mut before_publish))
5003 }
5004
5005 fn create_procedure_inner(
5006 &self,
5007 mut procedure: StoredProcedure,
5008 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5009 ) -> Result<StoredProcedure> {
5010 self.require(&crate::auth::Permission::Ddl)?;
5011 let _g = self.ddl_lock.lock();
5012 let _security_write = self.security_write()?;
5013 self.require(&crate::auth::Permission::Ddl)?;
5014 procedure.validate()?;
5015 self.validate_procedure_references(&procedure)?;
5016 {
5017 let cat = self.catalog.read();
5018 if cat
5019 .procedures
5020 .iter()
5021 .any(|p| p.procedure.name == procedure.name)
5022 {
5023 return Err(MongrelError::InvalidArgument(format!(
5024 "procedure {:?} already exists",
5025 procedure.name
5026 )));
5027 }
5028 }
5029 let commit_lock = Arc::clone(&self.commit_lock);
5030 let _c = commit_lock.lock();
5031 let epoch = self.epoch.bump_assigned();
5032 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5033 procedure.created_epoch = epoch.0;
5034 procedure.updated_epoch = epoch.0;
5035 let mut next_catalog = self.catalog.read().clone();
5036 next_catalog
5037 .procedures
5038 .push(ProcedureEntry::from(procedure.clone()));
5039 next_catalog.db_epoch = epoch.0;
5040 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5041 Ok(procedure)
5042 }
5043
5044 pub fn create_or_replace_procedure(
5045 &self,
5046 procedure: StoredProcedure,
5047 ) -> Result<StoredProcedure> {
5048 self.create_or_replace_procedure_inner(procedure, None)
5049 }
5050
5051 pub fn create_or_replace_procedure_controlled<F>(
5052 &self,
5053 procedure: StoredProcedure,
5054 mut before_publish: F,
5055 ) -> Result<StoredProcedure>
5056 where
5057 F: FnMut() -> Result<()>,
5058 {
5059 self.create_or_replace_procedure_inner(procedure, Some(&mut before_publish))
5060 }
5061
5062 fn create_or_replace_procedure_inner(
5063 &self,
5064 procedure: StoredProcedure,
5065 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5066 ) -> Result<StoredProcedure> {
5067 self.require(&crate::auth::Permission::Ddl)?;
5068 let _g = self.ddl_lock.lock();
5069 let _security_write = self.security_write()?;
5070 self.require(&crate::auth::Permission::Ddl)?;
5071 procedure.validate()?;
5072 self.validate_procedure_references(&procedure)?;
5073 let commit_lock = Arc::clone(&self.commit_lock);
5074 let _c = commit_lock.lock();
5075 let epoch = self.epoch.bump_assigned();
5076 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5077 let mut next_catalog = self.catalog.read().clone();
5078 let replaced = {
5079 let next = match next_catalog
5080 .procedures
5081 .iter()
5082 .position(|p| p.procedure.name == procedure.name)
5083 {
5084 Some(idx) => {
5085 let next = next_catalog.procedures[idx]
5086 .procedure
5087 .replaced(procedure.clone(), epoch.0)?;
5088 next_catalog.procedures[idx] = ProcedureEntry::from(next.clone());
5089 next
5090 }
5091 None => {
5092 let mut next = procedure;
5093 next.created_epoch = epoch.0;
5094 next.updated_epoch = epoch.0;
5095 next_catalog
5096 .procedures
5097 .push(ProcedureEntry::from(next.clone()));
5098 next
5099 }
5100 };
5101 next_catalog.db_epoch = epoch.0;
5102 next
5103 };
5104 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5105 Ok(replaced)
5106 }
5107
5108 pub fn drop_procedure(&self, name: &str) -> Result<()> {
5109 self.drop_procedure_with_epoch(name).map(|_| ())
5110 }
5111
5112 pub fn drop_procedure_with_epoch(&self, name: &str) -> Result<Epoch> {
5113 self.drop_procedure_with_epoch_inner(name, None)
5114 }
5115
5116 pub fn drop_procedure_with_epoch_controlled<F>(
5117 &self,
5118 name: &str,
5119 mut before_publish: F,
5120 ) -> Result<Epoch>
5121 where
5122 F: FnMut() -> Result<()>,
5123 {
5124 self.drop_procedure_with_epoch_inner(name, Some(&mut before_publish))
5125 }
5126
5127 fn drop_procedure_with_epoch_inner(
5128 &self,
5129 name: &str,
5130 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5131 ) -> Result<Epoch> {
5132 self.require(&crate::auth::Permission::Ddl)?;
5133 let _g = self.ddl_lock.lock();
5134 let _security_write = self.security_write()?;
5135 self.require(&crate::auth::Permission::Ddl)?;
5136 let commit_lock = Arc::clone(&self.commit_lock);
5137 let _c = commit_lock.lock();
5138 let epoch = self.epoch.bump_assigned();
5139 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5140 let mut next_catalog = self.catalog.read().clone();
5141 let before = next_catalog.procedures.len();
5142 next_catalog.procedures.retain(|p| p.procedure.name != name);
5143 if next_catalog.procedures.len() == before {
5144 return Err(MongrelError::NotFound(format!(
5145 "procedure {name:?} not found"
5146 )));
5147 }
5148 next_catalog.db_epoch = epoch.0;
5149 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5150 Ok(epoch)
5151 }
5152
5153 pub fn users(&self) -> Vec<crate::auth::UserEntry> {
5158 self.catalog.read().users.clone()
5159 }
5160
5161 pub fn user_identity(&self, username: &str) -> Option<(u64, u64)> {
5164 self.catalog
5165 .read()
5166 .users
5167 .iter()
5168 .find(|user| user.username == username)
5169 .map(|user| (user.id, user.created_epoch))
5170 }
5171
5172 pub fn security_version(&self) -> u64 {
5175 self.catalog.read().security_version
5176 }
5177
5178 pub fn roles(&self) -> Vec<crate::auth::RoleEntry> {
5180 self.catalog.read().roles.clone()
5181 }
5182
5183 pub fn create_user(&self, username: &str, password: &str) -> Result<crate::auth::UserEntry> {
5185 self.require(&crate::auth::Permission::Admin)?;
5186 let hash = crate::auth::hash_password(password).map_err(MongrelError::Other)?;
5187 self.create_user_with_password_hash(username, hash)
5188 }
5189
5190 pub fn create_user_with_password_hash(
5192 &self,
5193 username: &str,
5194 hash: String,
5195 ) -> Result<crate::auth::UserEntry> {
5196 self.create_user_with_password_hash_inner(username, hash, None)
5197 }
5198
5199 pub fn create_user_with_password_hash_controlled<F>(
5200 &self,
5201 username: &str,
5202 hash: String,
5203 mut before_publish: F,
5204 ) -> Result<crate::auth::UserEntry>
5205 where
5206 F: FnMut() -> Result<()>,
5207 {
5208 self.create_user_with_password_hash_inner(username, hash, Some(&mut before_publish))
5209 }
5210
5211 fn create_user_with_password_hash_inner(
5212 &self,
5213 username: &str,
5214 hash: String,
5215 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5216 ) -> Result<crate::auth::UserEntry> {
5217 self.require(&crate::auth::Permission::Admin)?;
5218 let _ddl = self.ddl_lock.lock();
5219 let _security_write = self.security_write()?;
5220 self.require(&crate::auth::Permission::Admin)?;
5221 let _commit = self.commit_lock.lock();
5222 let epoch = self.epoch.bump_assigned();
5223 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5224 let mut next_catalog = self.catalog.read().clone();
5225 if next_catalog.users.iter().any(|u| u.username == username) {
5226 return Err(MongrelError::InvalidArgument(format!(
5227 "user {username:?} already exists"
5228 )));
5229 }
5230 next_catalog.next_user_id = next_catalog.next_user_id.max(1);
5231 let id = next_catalog.next_user_id;
5232 next_catalog.next_user_id = id
5233 .checked_add(1)
5234 .ok_or_else(|| MongrelError::Full("user-id namespace exhausted".into()))?;
5235 let entry = crate::auth::UserEntry {
5236 id,
5237 username: username.into(),
5238 password_hash: hash,
5239 roles: Vec::new(),
5240 is_admin: false,
5241 created_epoch: epoch.0,
5242 };
5243 next_catalog.users.push(entry.clone());
5244 advance_security_version(&mut next_catalog)?;
5245 next_catalog.db_epoch = epoch.0;
5246 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5247 Ok(entry)
5248 }
5249
5250 pub fn drop_user(&self, username: &str) -> Result<()> {
5252 self.drop_user_with_epoch(username).map(|_| ())
5253 }
5254
5255 pub fn drop_user_with_epoch(&self, username: &str) -> Result<Epoch> {
5256 self.drop_user_with_epoch_inner(username, None)
5257 }
5258
5259 pub fn drop_user_with_epoch_controlled<F>(
5260 &self,
5261 username: &str,
5262 mut before_publish: F,
5263 ) -> Result<Epoch>
5264 where
5265 F: FnMut() -> Result<()>,
5266 {
5267 self.drop_user_with_epoch_inner(username, Some(&mut before_publish))
5268 }
5269
5270 fn drop_user_with_epoch_inner(
5271 &self,
5272 username: &str,
5273 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5274 ) -> Result<Epoch> {
5275 self.require(&crate::auth::Permission::Admin)?;
5276 let _ddl = self.ddl_lock.lock();
5277 let _security_write = self.security_write()?;
5278 self.require(&crate::auth::Permission::Admin)?;
5279 let _commit = self.commit_lock.lock();
5280 let epoch = self.epoch.bump_assigned();
5281 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5282 let mut next_catalog = self.catalog.read().clone();
5283 let before = next_catalog.users.len();
5284 next_catalog.users.retain(|u| u.username != username);
5285 if next_catalog.users.len() == before {
5286 return Err(MongrelError::NotFound(format!(
5287 "user {username:?} not found"
5288 )));
5289 }
5290 advance_security_version(&mut next_catalog)?;
5291 next_catalog.db_epoch = epoch.0;
5292 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5293 Ok(epoch)
5294 }
5295
5296 pub fn alter_user_password(&self, username: &str, new_password: &str) -> Result<()> {
5298 self.alter_user_password_with_epoch(username, new_password)
5299 .map(|_| ())
5300 }
5301
5302 pub fn alter_user_password_with_epoch(
5303 &self,
5304 username: &str,
5305 new_password: &str,
5306 ) -> Result<Epoch> {
5307 self.require(&crate::auth::Permission::Admin)?;
5308 let hash = crate::auth::hash_password(new_password).map_err(MongrelError::Other)?;
5309 self.alter_user_password_hash_with_epoch(username, hash)
5310 }
5311
5312 pub fn alter_user_password_hash_with_epoch(
5313 &self,
5314 username: &str,
5315 hash: String,
5316 ) -> Result<Epoch> {
5317 self.alter_user_password_hash_with_epoch_inner(username, hash, None)
5318 }
5319
5320 pub fn alter_user_password_hash_with_epoch_controlled<F>(
5321 &self,
5322 username: &str,
5323 hash: String,
5324 mut before_publish: F,
5325 ) -> Result<Epoch>
5326 where
5327 F: FnMut() -> Result<()>,
5328 {
5329 self.alter_user_password_hash_with_epoch_inner(username, hash, Some(&mut before_publish))
5330 }
5331
5332 fn alter_user_password_hash_with_epoch_inner(
5333 &self,
5334 username: &str,
5335 hash: String,
5336 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5337 ) -> Result<Epoch> {
5338 self.require(&crate::auth::Permission::Admin)?;
5339 let _ddl = self.ddl_lock.lock();
5340 let _security_write = self.security_write()?;
5341 self.require(&crate::auth::Permission::Admin)?;
5342 let _commit = self.commit_lock.lock();
5343 let epoch = self.epoch.bump_assigned();
5344 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5345 let mut next_catalog = self.catalog.read().clone();
5346 let user = next_catalog
5347 .users
5348 .iter_mut()
5349 .find(|u| u.username == username)
5350 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5351 user.password_hash = hash;
5352 advance_security_version(&mut next_catalog)?;
5353 next_catalog.db_epoch = epoch.0;
5354 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5355 Ok(epoch)
5356 }
5357
5358 pub fn verify_user(
5361 &self,
5362 username: &str,
5363 password: &str,
5364 ) -> Result<Option<crate::auth::UserEntry>> {
5365 let cat = self.catalog.read();
5366 let Some(user) = cat.users.iter().find(|u| u.username == username) else {
5367 return Ok(None);
5368 };
5369 if user.password_hash.is_empty() {
5370 return Ok(None);
5371 }
5372 let ok = crate::auth::verify_password(password, &user.password_hash)
5373 .map_err(MongrelError::Other)?;
5374 if ok {
5375 Ok(Some(user.clone()))
5376 } else {
5377 Ok(None)
5378 }
5379 }
5380
5381 pub fn authenticate_principal(
5385 &self,
5386 username: &str,
5387 password: &str,
5388 ) -> Result<Option<crate::auth::Principal>> {
5389 self.authenticate_principal_inner(username, password, || {})
5390 }
5391
5392 fn authenticate_principal_inner<F>(
5393 &self,
5394 username: &str,
5395 password: &str,
5396 after_verify: F,
5397 ) -> Result<Option<crate::auth::Principal>>
5398 where
5399 F: FnOnce(),
5400 {
5401 let catalog = self.catalog.read();
5402 let Some(user) = catalog.users.iter().find(|user| user.username == username) else {
5403 return Ok(None);
5404 };
5405 if user.password_hash.is_empty()
5406 || !crate::auth::verify_password(password, &user.password_hash)
5407 .map_err(MongrelError::Other)?
5408 {
5409 return Ok(None);
5410 }
5411 after_verify();
5412 Ok(Self::resolve_user_principal_from_catalog(&catalog, user))
5413 }
5414
5415 pub fn set_user_admin(&self, username: &str, is_admin: bool) -> Result<()> {
5417 self.set_user_admin_with_epoch(username, is_admin)
5418 .map(|_| ())
5419 }
5420
5421 pub fn set_user_admin_with_epoch(
5422 &self,
5423 username: &str,
5424 is_admin: bool,
5425 ) -> Result<Option<Epoch>> {
5426 self.set_user_admin_with_epoch_inner(username, is_admin, None)
5427 }
5428
5429 pub fn set_user_admin_with_epoch_controlled<F>(
5430 &self,
5431 username: &str,
5432 is_admin: bool,
5433 mut before_publish: F,
5434 ) -> Result<Option<Epoch>>
5435 where
5436 F: FnMut() -> Result<()>,
5437 {
5438 self.set_user_admin_with_epoch_inner(username, is_admin, Some(&mut before_publish))
5439 }
5440
5441 fn set_user_admin_with_epoch_inner(
5442 &self,
5443 username: &str,
5444 is_admin: bool,
5445 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5446 ) -> Result<Option<Epoch>> {
5447 self.require(&crate::auth::Permission::Admin)?;
5448 let _ddl = self.ddl_lock.lock();
5449 let _security_write = self.security_write()?;
5450 self.require(&crate::auth::Permission::Admin)?;
5451 let _commit = self.commit_lock.lock();
5452 let mut next_catalog = self.catalog.read().clone();
5453 let user = next_catalog
5454 .users
5455 .iter_mut()
5456 .find(|u| u.username == username)
5457 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5458 if user.is_admin == is_admin {
5459 return Ok(None);
5460 }
5461 user.is_admin = is_admin;
5462 let epoch = self.epoch.bump_assigned();
5463 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5464 advance_security_version(&mut next_catalog)?;
5465 next_catalog.db_epoch = epoch.0;
5466 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5467 Ok(Some(epoch))
5468 }
5469
5470 pub fn create_role(&self, name: &str) -> Result<crate::auth::RoleEntry> {
5472 self.create_role_inner(name, None)
5473 }
5474
5475 pub fn create_role_controlled<F>(
5476 &self,
5477 name: &str,
5478 mut before_publish: F,
5479 ) -> Result<crate::auth::RoleEntry>
5480 where
5481 F: FnMut() -> Result<()>,
5482 {
5483 self.create_role_inner(name, Some(&mut before_publish))
5484 }
5485
5486 fn create_role_inner(
5487 &self,
5488 name: &str,
5489 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5490 ) -> Result<crate::auth::RoleEntry> {
5491 self.require(&crate::auth::Permission::Admin)?;
5492 let _ddl = self.ddl_lock.lock();
5493 let _security_write = self.security_write()?;
5494 self.require(&crate::auth::Permission::Admin)?;
5495 let _commit = self.commit_lock.lock();
5496 let epoch = self.epoch.bump_assigned();
5497 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5498 let mut next_catalog = self.catalog.read().clone();
5499 if next_catalog.roles.iter().any(|r| r.name == name) {
5500 return Err(MongrelError::InvalidArgument(format!(
5501 "role {name:?} already exists"
5502 )));
5503 }
5504 let entry = crate::auth::RoleEntry {
5505 name: name.into(),
5506 permissions: Vec::new(),
5507 created_epoch: epoch.0,
5508 };
5509 next_catalog.roles.push(entry.clone());
5510 advance_security_version(&mut next_catalog)?;
5511 next_catalog.db_epoch = epoch.0;
5512 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5513 Ok(entry)
5514 }
5515
5516 pub fn drop_role(&self, name: &str) -> Result<()> {
5518 self.drop_role_with_epoch(name).map(|_| ())
5519 }
5520
5521 pub fn drop_role_with_epoch(&self, name: &str) -> Result<Epoch> {
5522 self.drop_role_with_epoch_inner(name, None)
5523 }
5524
5525 pub fn drop_role_with_epoch_controlled<F>(
5526 &self,
5527 name: &str,
5528 mut before_publish: F,
5529 ) -> Result<Epoch>
5530 where
5531 F: FnMut() -> Result<()>,
5532 {
5533 self.drop_role_with_epoch_inner(name, Some(&mut before_publish))
5534 }
5535
5536 fn drop_role_with_epoch_inner(
5537 &self,
5538 name: &str,
5539 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5540 ) -> Result<Epoch> {
5541 self.require(&crate::auth::Permission::Admin)?;
5542 let _ddl = self.ddl_lock.lock();
5543 let _security_write = self.security_write()?;
5544 self.require(&crate::auth::Permission::Admin)?;
5545 let _commit = self.commit_lock.lock();
5546 let epoch = self.epoch.bump_assigned();
5547 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5548 let mut next_catalog = self.catalog.read().clone();
5549 let before = next_catalog.roles.len();
5550 next_catalog.roles.retain(|r| r.name != name);
5551 if next_catalog.roles.len() == before {
5552 return Err(MongrelError::NotFound(format!("role {name:?} not found")));
5553 }
5554 for user in &mut next_catalog.users {
5555 user.roles.retain(|r| r != name);
5556 }
5557 advance_security_version(&mut next_catalog)?;
5558 next_catalog.db_epoch = epoch.0;
5559 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5560 Ok(epoch)
5561 }
5562
5563 pub fn grant_role(&self, username: &str, role_name: &str) -> Result<()> {
5565 self.grant_role_with_epoch(username, role_name).map(|_| ())
5566 }
5567
5568 pub fn grant_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
5569 self.grant_role_with_epoch_inner(username, role_name, None)
5570 }
5571
5572 pub fn grant_role_with_epoch_controlled<F>(
5573 &self,
5574 username: &str,
5575 role_name: &str,
5576 mut before_publish: F,
5577 ) -> Result<Option<Epoch>>
5578 where
5579 F: FnMut() -> Result<()>,
5580 {
5581 self.grant_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
5582 }
5583
5584 fn grant_role_with_epoch_inner(
5585 &self,
5586 username: &str,
5587 role_name: &str,
5588 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5589 ) -> Result<Option<Epoch>> {
5590 self.require(&crate::auth::Permission::Admin)?;
5591 let _ddl = self.ddl_lock.lock();
5592 let _security_write = self.security_write()?;
5593 self.require(&crate::auth::Permission::Admin)?;
5594 let _commit = self.commit_lock.lock();
5595 let mut next_catalog = self.catalog.read().clone();
5596 if !next_catalog.roles.iter().any(|r| r.name == role_name) {
5597 return Err(MongrelError::NotFound(format!(
5598 "role {role_name:?} not found"
5599 )));
5600 }
5601 let user = next_catalog
5602 .users
5603 .iter_mut()
5604 .find(|u| u.username == username)
5605 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5606 if user.roles.iter().any(|role| role == role_name) {
5607 return Ok(None);
5608 }
5609 user.roles.push(role_name.into());
5610 let epoch = self.epoch.bump_assigned();
5611 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5612 advance_security_version(&mut next_catalog)?;
5613 next_catalog.db_epoch = epoch.0;
5614 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5615 Ok(Some(epoch))
5616 }
5617
5618 pub fn revoke_role(&self, username: &str, role_name: &str) -> Result<()> {
5620 self.revoke_role_with_epoch(username, role_name).map(|_| ())
5621 }
5622
5623 pub fn revoke_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
5624 self.revoke_role_with_epoch_inner(username, role_name, None)
5625 }
5626
5627 pub fn revoke_role_with_epoch_controlled<F>(
5628 &self,
5629 username: &str,
5630 role_name: &str,
5631 mut before_publish: F,
5632 ) -> Result<Option<Epoch>>
5633 where
5634 F: FnMut() -> Result<()>,
5635 {
5636 self.revoke_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
5637 }
5638
5639 fn revoke_role_with_epoch_inner(
5640 &self,
5641 username: &str,
5642 role_name: &str,
5643 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5644 ) -> Result<Option<Epoch>> {
5645 self.require(&crate::auth::Permission::Admin)?;
5646 let _ddl = self.ddl_lock.lock();
5647 let _security_write = self.security_write()?;
5648 self.require(&crate::auth::Permission::Admin)?;
5649 let _commit = self.commit_lock.lock();
5650 let mut next_catalog = self.catalog.read().clone();
5651 let user = next_catalog
5652 .users
5653 .iter_mut()
5654 .find(|u| u.username == username)
5655 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5656 let before = user.roles.len();
5657 user.roles.retain(|r| r != role_name);
5658 if user.roles.len() == before {
5659 return Ok(None);
5660 }
5661 let epoch = self.epoch.bump_assigned();
5662 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5663 advance_security_version(&mut next_catalog)?;
5664 next_catalog.db_epoch = epoch.0;
5665 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5666 Ok(Some(epoch))
5667 }
5668
5669 pub fn grant_permission(
5671 &self,
5672 role_name: &str,
5673 permission: crate::auth::Permission,
5674 ) -> Result<()> {
5675 self.grant_permission_with_epoch(role_name, permission)
5676 .map(|_| ())
5677 }
5678
5679 pub fn grant_permission_with_epoch(
5680 &self,
5681 role_name: &str,
5682 permission: crate::auth::Permission,
5683 ) -> Result<Option<Epoch>> {
5684 self.grant_permission_with_epoch_inner(role_name, permission, None)
5685 }
5686
5687 pub fn grant_permission_with_epoch_controlled<F>(
5688 &self,
5689 role_name: &str,
5690 permission: crate::auth::Permission,
5691 mut before_publish: F,
5692 ) -> Result<Option<Epoch>>
5693 where
5694 F: FnMut() -> Result<()>,
5695 {
5696 self.grant_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
5697 }
5698
5699 fn grant_permission_with_epoch_inner(
5700 &self,
5701 role_name: &str,
5702 permission: crate::auth::Permission,
5703 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5704 ) -> Result<Option<Epoch>> {
5705 self.require(&crate::auth::Permission::Admin)?;
5706 let _ddl = self.ddl_lock.lock();
5707 let _security_write = self.security_write()?;
5708 self.require(&crate::auth::Permission::Admin)?;
5709 let _commit = self.commit_lock.lock();
5710 let mut next_catalog = self.catalog.read().clone();
5711 let role = next_catalog
5712 .roles
5713 .iter_mut()
5714 .find(|r| r.name == role_name)
5715 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
5716 let before = role.permissions.clone();
5717 merge_permission(&mut role.permissions, permission);
5718 if role.permissions == before {
5719 return Ok(None);
5720 }
5721 let epoch = self.epoch.bump_assigned();
5722 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5723 advance_security_version(&mut next_catalog)?;
5724 next_catalog.db_epoch = epoch.0;
5725 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5726 Ok(Some(epoch))
5727 }
5728
5729 pub fn revoke_permission(
5731 &self,
5732 role_name: &str,
5733 permission: crate::auth::Permission,
5734 ) -> Result<()> {
5735 self.revoke_permission_with_epoch(role_name, permission)
5736 .map(|_| ())
5737 }
5738
5739 pub fn revoke_permission_with_epoch(
5740 &self,
5741 role_name: &str,
5742 permission: crate::auth::Permission,
5743 ) -> Result<Option<Epoch>> {
5744 self.revoke_permission_with_epoch_inner(role_name, permission, None)
5745 }
5746
5747 pub fn revoke_permission_with_epoch_controlled<F>(
5748 &self,
5749 role_name: &str,
5750 permission: crate::auth::Permission,
5751 mut before_publish: F,
5752 ) -> Result<Option<Epoch>>
5753 where
5754 F: FnMut() -> Result<()>,
5755 {
5756 self.revoke_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
5757 }
5758
5759 fn revoke_permission_with_epoch_inner(
5760 &self,
5761 role_name: &str,
5762 permission: crate::auth::Permission,
5763 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5764 ) -> Result<Option<Epoch>> {
5765 self.require(&crate::auth::Permission::Admin)?;
5766 let _ddl = self.ddl_lock.lock();
5767 let _security_write = self.security_write()?;
5768 self.require(&crate::auth::Permission::Admin)?;
5769 let _commit = self.commit_lock.lock();
5770 let mut next_catalog = self.catalog.read().clone();
5771 let role = next_catalog
5772 .roles
5773 .iter_mut()
5774 .find(|r| r.name == role_name)
5775 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
5776 let before = role.permissions.clone();
5777 revoke_permission_from(&mut role.permissions, &permission);
5778 if role.permissions == before {
5779 return Ok(None);
5780 }
5781 let epoch = self.epoch.bump_assigned();
5782 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5783 advance_security_version(&mut next_catalog)?;
5784 next_catalog.db_epoch = epoch.0;
5785 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5786 Ok(Some(epoch))
5787 }
5788
5789 pub fn resolve_principal(&self, username: &str) -> Option<crate::auth::Principal> {
5792 let cat = self.catalog.read();
5793 Self::resolve_principal_from_catalog(&cat, username)
5794 }
5795
5796 pub fn resolve_current_principal(
5799 &self,
5800 principal: &crate::auth::Principal,
5801 ) -> Option<crate::auth::Principal> {
5802 Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
5803 }
5804
5805 fn resolve_principal_from_catalog(
5810 cat: &Catalog,
5811 username: &str,
5812 ) -> Option<crate::auth::Principal> {
5813 let user = cat.users.iter().find(|u| u.username == username)?;
5814 Self::resolve_user_principal_from_catalog(cat, user)
5815 }
5816
5817 fn resolve_bound_principal_from_catalog(
5818 cat: &Catalog,
5819 principal: &crate::auth::Principal,
5820 ) -> Option<crate::auth::Principal> {
5821 let user = cat.users.iter().find(|user| {
5822 user.id == principal.user_id
5823 && user.created_epoch == principal.created_epoch
5824 && user.username == principal.username
5825 })?;
5826 Self::resolve_user_principal_from_catalog(cat, user)
5827 }
5828
5829 fn resolve_user_principal_from_catalog(
5830 cat: &Catalog,
5831 user: &crate::auth::UserEntry,
5832 ) -> Option<crate::auth::Principal> {
5833 let mut permissions = Vec::new();
5834 for role_name in &user.roles {
5835 if let Some(role) = cat.roles.iter().find(|r| &r.name == role_name) {
5836 permissions.extend(role.permissions.iter().cloned());
5837 }
5838 }
5839 Some(crate::auth::Principal {
5840 user_id: user.id,
5841 created_epoch: user.created_epoch,
5842 username: user.username.clone(),
5843 is_admin: user.is_admin,
5844 roles: user.roles.clone(),
5845 permissions,
5846 })
5847 }
5848
5849 pub fn check_permission(&self, username: &str, permission: &crate::auth::Permission) -> bool {
5851 match self.resolve_principal(username) {
5852 Some(p) => p.has_permission(permission),
5853 None => false,
5854 }
5855 }
5856
5857 pub fn require_auth_enabled(&self) -> bool {
5861 self.catalog.read().require_auth
5862 }
5863
5864 pub fn principal(&self) -> Option<crate::auth::Principal> {
5868 self.principal.read().clone()
5869 }
5870
5871 fn table_auth_checker(&self) -> Option<Arc<dyn crate::auth_state::TableAuthChecker>> {
5877 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
5878 self.auth_state.clone(),
5879 )))
5880 }
5881
5882 pub fn refresh_principal(&self) -> Result<()> {
5896 let previous = match self.principal.read().clone() {
5897 Some(principal) => principal,
5898 None => return Ok(()),
5899 };
5900 let observed_version = self.security_coordinator.version.load(Ordering::Acquire);
5901 self.refresh_security_catalog_if_stale(observed_version)?;
5902 let cat = self.catalog.read();
5903 match Self::resolve_bound_principal_from_catalog(&cat, &previous) {
5904 Some(p) => {
5905 *self.principal.write() = Some(p.clone());
5906 self.auth_state.set_principal(Some(p));
5910 Ok(())
5911 }
5912 None => Err(MongrelError::InvalidCredentials {
5913 username: previous.username,
5914 }),
5915 }
5916 }
5917
5918 pub fn security_catalog_disk_read_count(&self) -> u64 {
5921 self.security_catalog_disk_reads.load(Ordering::Relaxed)
5922 }
5923
5924 pub fn enable_auth(&self, admin_username: &str, admin_password: &str) -> Result<()> {
5936 let password_hash =
5937 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
5938 let _ddl = self.ddl_lock.lock();
5939 let _security_write = self.security_write()?;
5940 let _commit = self.commit_lock.lock();
5941 let epoch = self.epoch.bump_assigned();
5942 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5943 let mut next_catalog = self.catalog.read().clone();
5944 if next_catalog.require_auth {
5945 return Err(MongrelError::InvalidArgument(
5946 "database already has require_auth enabled".into(),
5947 ));
5948 }
5949 if next_catalog
5950 .users
5951 .iter()
5952 .any(|u| u.username == admin_username)
5953 {
5954 return Err(MongrelError::InvalidArgument(format!(
5955 "user {admin_username:?} already exists"
5956 )));
5957 }
5958 next_catalog.next_user_id = next_catalog.next_user_id.max(1);
5959 let id = next_catalog.next_user_id;
5960 next_catalog.next_user_id = id
5961 .checked_add(1)
5962 .ok_or_else(|| MongrelError::Full("user-id namespace exhausted".into()))?;
5963 next_catalog.users.push(crate::auth::UserEntry {
5964 id,
5965 username: admin_username.to_string(),
5966 password_hash,
5967 roles: Vec::new(),
5968 is_admin: true,
5969 created_epoch: epoch.0,
5970 });
5971 next_catalog.require_auth = true;
5972 advance_security_version(&mut next_catalog)?;
5973 next_catalog.db_epoch = epoch.0;
5974 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
5975 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
5979 let principal = crate::auth::Principal {
5980 user_id: id,
5981 created_epoch: epoch.0,
5982 username: admin_username.to_string(),
5983 is_admin: true,
5984 roles: Vec::new(),
5985 permissions: Vec::new(),
5986 };
5987 *self.principal.write() = Some(principal.clone());
5988 self.auth_state.set_principal(Some(principal));
5989 }
5990 publish
5991 }
5992
5993 pub fn disable_auth(&self) -> Result<()> {
6010 let _ddl = self.ddl_lock.lock();
6011 let _security_write = self.security_write()?;
6012 let _commit = self.commit_lock.lock();
6013 let epoch = self.epoch.bump_assigned();
6014 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6015 let mut next_catalog = self.catalog.read().clone();
6016 if !next_catalog.require_auth {
6017 return Err(MongrelError::InvalidArgument(
6018 "database does not have require_auth enabled".into(),
6019 ));
6020 }
6021 next_catalog.require_auth = false;
6022 advance_security_version(&mut next_catalog)?;
6023 next_catalog.db_epoch = epoch.0;
6024 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
6025 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
6027 *self.principal.write() = None;
6028 }
6029 publish
6030 }
6031
6032 pub fn require(&self, perm: &crate::auth::Permission) -> Result<()> {
6039 if self.read_only && !matches!(perm, crate::auth::Permission::Select { .. }) {
6040 return Err(MongrelError::ReadOnlyReplica);
6041 }
6042 if self.principal.read().is_some() {
6043 self.refresh_principal().map_err(|error| match error {
6044 MongrelError::InvalidCredentials { .. } => MongrelError::AuthRequired,
6045 error => error,
6046 })?;
6047 }
6048 if !self.catalog.read().require_auth {
6049 return Ok(());
6050 }
6051 let guard = self.principal.read();
6052 let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
6053 if p.has_permission(perm) {
6054 Ok(())
6055 } else {
6056 Err(MongrelError::PermissionDenied {
6057 required: perm.clone(),
6058 principal: p.username.clone(),
6059 })
6060 }
6061 }
6062
6063 pub fn require_table(
6068 &self,
6069 table: &str,
6070 perm: crate::auth_state::RequiredPermission,
6071 ) -> Result<()> {
6072 self.require(&perm.into_permission(table))
6073 }
6074
6075 pub fn triggers(&self) -> Vec<StoredTrigger> {
6076 self.catalog
6077 .read()
6078 .triggers
6079 .iter()
6080 .map(|t| t.trigger.clone())
6081 .collect()
6082 }
6083
6084 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
6085 self.catalog
6086 .read()
6087 .triggers
6088 .iter()
6089 .find(|t| t.trigger.name == name)
6090 .map(|t| t.trigger.clone())
6091 }
6092
6093 pub fn create_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
6094 self.create_trigger_inner(trigger, None, None)
6095 }
6096
6097 pub fn create_trigger_controlled<F>(
6098 &self,
6099 trigger: StoredTrigger,
6100 mut before_publish: F,
6101 ) -> Result<StoredTrigger>
6102 where
6103 F: FnMut() -> Result<()>,
6104 {
6105 self.create_trigger_inner(trigger, None, Some(&mut before_publish))
6106 }
6107
6108 pub fn create_trigger_as_controlled<F>(
6109 &self,
6110 trigger: StoredTrigger,
6111 principal: Option<&crate::auth::Principal>,
6112 mut before_publish: F,
6113 ) -> Result<StoredTrigger>
6114 where
6115 F: FnMut() -> Result<()>,
6116 {
6117 self.create_trigger_inner(trigger, principal, Some(&mut before_publish))
6118 }
6119
6120 fn create_trigger_inner(
6121 &self,
6122 mut trigger: StoredTrigger,
6123 principal: Option<&crate::auth::Principal>,
6124 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6125 ) -> Result<StoredTrigger> {
6126 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6127 let _g = self.ddl_lock.lock();
6128 let _security_write = self.security_write()?;
6129 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6130 trigger.validate()?;
6131 self.validate_trigger_references(&trigger)
6132 .map_err(trigger_validation_error)?;
6133 {
6134 let cat = self.catalog.read();
6135 if cat.triggers.iter().any(|t| t.trigger.name == trigger.name) {
6136 return Err(MongrelError::TriggerValidation(format!(
6137 "trigger {:?} already exists",
6138 trigger.name
6139 )));
6140 }
6141 }
6142 let commit_lock = Arc::clone(&self.commit_lock);
6143 let _c = commit_lock.lock();
6144 let epoch = self.epoch.bump_assigned();
6145 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6146 trigger.created_epoch = epoch.0;
6147 trigger.updated_epoch = epoch.0;
6148 let mut next_catalog = self.catalog.read().clone();
6149 next_catalog
6150 .triggers
6151 .push(TriggerEntry::from(trigger.clone()));
6152 next_catalog.db_epoch = epoch.0;
6153 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6154 Ok(trigger)
6155 }
6156
6157 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
6158 self.create_or_replace_trigger_inner(trigger, None, None)
6159 }
6160
6161 pub fn create_or_replace_trigger_controlled<F>(
6162 &self,
6163 trigger: StoredTrigger,
6164 mut before_publish: F,
6165 ) -> Result<StoredTrigger>
6166 where
6167 F: FnMut() -> Result<()>,
6168 {
6169 self.create_or_replace_trigger_inner(trigger, None, Some(&mut before_publish))
6170 }
6171
6172 pub fn create_or_replace_trigger_as_controlled<F>(
6173 &self,
6174 trigger: StoredTrigger,
6175 principal: Option<&crate::auth::Principal>,
6176 mut before_publish: F,
6177 ) -> Result<StoredTrigger>
6178 where
6179 F: FnMut() -> Result<()>,
6180 {
6181 self.create_or_replace_trigger_inner(trigger, principal, Some(&mut before_publish))
6182 }
6183
6184 fn create_or_replace_trigger_inner(
6185 &self,
6186 trigger: StoredTrigger,
6187 principal: Option<&crate::auth::Principal>,
6188 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6189 ) -> Result<StoredTrigger> {
6190 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6191 let _g = self.ddl_lock.lock();
6192 let _security_write = self.security_write()?;
6193 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6194 trigger.validate()?;
6195 self.validate_trigger_references(&trigger)
6196 .map_err(trigger_validation_error)?;
6197 let commit_lock = Arc::clone(&self.commit_lock);
6198 let _c = commit_lock.lock();
6199 let epoch = self.epoch.bump_assigned();
6200 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6201 let mut next_catalog = self.catalog.read().clone();
6202 let replaced = {
6203 let next = match next_catalog
6204 .triggers
6205 .iter()
6206 .position(|t| t.trigger.name == trigger.name)
6207 {
6208 Some(idx) => {
6209 let next = next_catalog.triggers[idx]
6210 .trigger
6211 .replaced(trigger.clone(), epoch.0)?;
6212 next_catalog.triggers[idx] = TriggerEntry::from(next.clone());
6213 next
6214 }
6215 None => {
6216 let mut next = trigger;
6217 next.created_epoch = epoch.0;
6218 next.updated_epoch = epoch.0;
6219 next_catalog.triggers.push(TriggerEntry::from(next.clone()));
6220 next
6221 }
6222 };
6223 next_catalog.db_epoch = epoch.0;
6224 next
6225 };
6226 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6227 Ok(replaced)
6228 }
6229
6230 pub fn drop_trigger(&self, name: &str) -> Result<()> {
6231 self.drop_trigger_with_epoch(name).map(|_| ())
6232 }
6233
6234 pub fn drop_trigger_with_epoch(&self, name: &str) -> Result<Epoch> {
6236 self.drop_triggers_with_epoch(&[name.to_string()])
6237 }
6238
6239 pub fn drop_trigger_with_epoch_controlled<F>(
6240 &self,
6241 name: &str,
6242 before_publish: F,
6243 ) -> Result<Epoch>
6244 where
6245 F: FnMut() -> Result<()>,
6246 {
6247 self.drop_triggers_with_epoch_controlled(&[name.to_string()], before_publish)
6248 }
6249
6250 pub fn drop_triggers_with_epoch(&self, names: &[String]) -> Result<Epoch> {
6252 self.drop_triggers_with_epoch_inner(names, None, None)
6253 }
6254
6255 pub fn drop_triggers_with_epoch_controlled<F>(
6256 &self,
6257 names: &[String],
6258 mut before_publish: F,
6259 ) -> Result<Epoch>
6260 where
6261 F: FnMut() -> Result<()>,
6262 {
6263 self.drop_triggers_with_epoch_inner(names, None, Some(&mut before_publish))
6264 }
6265
6266 pub fn drop_triggers_with_epoch_as_controlled<F>(
6267 &self,
6268 names: &[String],
6269 principal: Option<&crate::auth::Principal>,
6270 mut before_publish: F,
6271 ) -> Result<Epoch>
6272 where
6273 F: FnMut() -> Result<()>,
6274 {
6275 self.drop_triggers_with_epoch_inner(names, principal, Some(&mut before_publish))
6276 }
6277
6278 fn drop_triggers_with_epoch_inner(
6279 &self,
6280 names: &[String],
6281 principal: Option<&crate::auth::Principal>,
6282 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6283 ) -> Result<Epoch> {
6284 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6285 if names.is_empty() {
6286 return Err(MongrelError::InvalidArgument(
6287 "at least one trigger name is required".into(),
6288 ));
6289 }
6290 let _g = self.ddl_lock.lock();
6291 let _security_write = self.security_write()?;
6292 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6293 {
6294 let cat = self.catalog.read();
6295 for name in names {
6296 if !cat.triggers.iter().any(|t| t.trigger.name == *name) {
6297 return Err(MongrelError::NotFound(format!(
6298 "trigger {name:?} not found"
6299 )));
6300 }
6301 }
6302 }
6303 let commit_lock = Arc::clone(&self.commit_lock);
6304 let _c = commit_lock.lock();
6305 let epoch = self.epoch.bump_assigned();
6306 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6307 let mut next_catalog = self.catalog.read().clone();
6308 next_catalog
6309 .triggers
6310 .retain(|trigger| !names.contains(&trigger.trigger.name));
6311 next_catalog.db_epoch = epoch.0;
6312 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6313 Ok(epoch)
6314 }
6315
6316 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
6317 self.catalog.read().external_tables.clone()
6318 }
6319
6320 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
6321 self.catalog
6322 .read()
6323 .external_tables
6324 .iter()
6325 .find(|t| t.name == name)
6326 .cloned()
6327 }
6328
6329 pub fn create_external_table(&self, entry: ExternalTableEntry) -> Result<ExternalTableEntry> {
6330 self.create_external_table_inner(entry, None)
6331 }
6332
6333 pub fn create_external_table_controlled<F>(
6334 &self,
6335 entry: ExternalTableEntry,
6336 mut before_publish: F,
6337 ) -> Result<ExternalTableEntry>
6338 where
6339 F: FnMut() -> Result<()>,
6340 {
6341 self.create_external_table_inner(entry, Some(&mut before_publish))
6342 }
6343
6344 fn create_external_table_inner(
6345 &self,
6346 mut entry: ExternalTableEntry,
6347 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6348 ) -> Result<ExternalTableEntry> {
6349 self.require(&crate::auth::Permission::Ddl)?;
6350 let _g = self.ddl_lock.lock();
6351 let _security_write = self.security_write()?;
6352 self.require(&crate::auth::Permission::Ddl)?;
6353 entry.validate()?;
6354 {
6355 let cat = self.catalog.read();
6356 if cat.live(&entry.name).is_some()
6357 || cat.external_tables.iter().any(|t| t.name == entry.name)
6358 {
6359 return Err(MongrelError::InvalidArgument(format!(
6360 "table {:?} already exists",
6361 entry.name
6362 )));
6363 }
6364 }
6365 let commit_lock = Arc::clone(&self.commit_lock);
6366 let _c = commit_lock.lock();
6367 crate::durable_file::create_directory(&self.root.join(VTAB_DIR))?;
6371 crate::durable_file::remove_directory_all(&self.root.join(VTAB_DIR).join(&entry.name))?;
6372 let epoch = self.epoch.bump_assigned();
6373 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6374 entry.created_epoch = epoch.0;
6375 let mut next_catalog = self.catalog.read().clone();
6376 next_catalog.external_tables.push(entry.clone());
6377 next_catalog.db_epoch = epoch.0;
6378 self.publish_catalog_candidate_with_prelude(
6379 next_catalog,
6380 epoch,
6381 &mut _epoch_guard,
6382 before_publish,
6383 vec![(
6384 EXTERNAL_TABLE_ID,
6385 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
6386 name: entry.name.clone(),
6387 generation_epoch: epoch.0,
6388 }),
6389 )],
6390 )?;
6391 Ok(entry)
6392 }
6393
6394 pub fn drop_external_table(&self, name: &str) -> Result<()> {
6395 self.drop_external_table_with_epoch(name).map(|_| ())
6396 }
6397
6398 pub fn drop_external_table_with_epoch(&self, name: &str) -> Result<Epoch> {
6400 self.drop_external_table_with_epoch_inner(name, None)
6401 }
6402
6403 pub fn drop_external_table_with_epoch_controlled<F>(
6404 &self,
6405 name: &str,
6406 mut before_publish: F,
6407 ) -> Result<Epoch>
6408 where
6409 F: FnMut() -> Result<()>,
6410 {
6411 self.drop_external_table_with_epoch_inner(name, Some(&mut before_publish))
6412 }
6413
6414 fn drop_external_table_with_epoch_inner(
6415 &self,
6416 name: &str,
6417 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6418 ) -> Result<Epoch> {
6419 self.require(&crate::auth::Permission::Ddl)?;
6420 let _g = self.ddl_lock.lock();
6421 let _security_write = self.security_write()?;
6422 self.require(&crate::auth::Permission::Ddl)?;
6423 let commit_lock = Arc::clone(&self.commit_lock);
6424 let _c = commit_lock.lock();
6425 let epoch = self.epoch.bump_assigned();
6426 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6427 let mut next_catalog = self.catalog.read().clone();
6428 let before = next_catalog.external_tables.len();
6429 next_catalog.external_tables.retain(|t| t.name != name);
6430 if next_catalog.external_tables.len() == before {
6431 return Err(MongrelError::NotFound(format!(
6432 "external table {name:?} not found"
6433 )));
6434 }
6435 next_catalog.db_epoch = epoch.0;
6436 self.publish_catalog_candidate_with_prelude(
6437 next_catalog,
6438 epoch,
6439 &mut _epoch_guard,
6440 before_publish,
6441 vec![(
6442 EXTERNAL_TABLE_ID,
6443 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
6444 name: name.to_string(),
6445 generation_epoch: epoch.0,
6446 }),
6447 )],
6448 )?;
6449 let state_dir = self.root.join(VTAB_DIR).join(name);
6450 if let Err(error) = crate::durable_file::remove_directory_all(&state_dir) {
6451 return Err(MongrelError::DurableCommit {
6452 epoch: epoch.0,
6453 message: format!(
6454 "external table was dropped but connector-state cleanup failed: {error}"
6455 ),
6456 });
6457 }
6458 Ok(epoch)
6459 }
6460
6461 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
6462 let txn_id = self.alloc_txn_id()?;
6463 let (principal, catalog_bound) = self.transaction_principal_snapshot();
6464 self.commit_transaction_with_external_states(
6465 txn_id,
6466 self.epoch.visible(),
6467 Vec::new(),
6468 vec![(name.to_string(), state.to_vec())],
6469 Vec::new(),
6470 principal,
6471 catalog_bound,
6472 None,
6473 )
6474 .map(|(epoch, _)| epoch)
6475 }
6476
6477 pub fn trigger_config(&self) -> TriggerConfig {
6478 use std::sync::atomic::Ordering;
6479 TriggerConfig {
6480 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
6481 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
6482 max_loop_iterations: self.trigger_max_loop_iterations.load(Ordering::Relaxed),
6483 }
6484 }
6485
6486 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
6487 use std::sync::atomic::Ordering;
6488 if config.max_depth == 0 {
6489 return Err(MongrelError::InvalidArgument(
6490 "trigger max_depth must be greater than 0".into(),
6491 ));
6492 }
6493 self.trigger_recursive
6494 .store(config.recursive_triggers, Ordering::Relaxed);
6495 self.trigger_max_depth
6496 .store(config.max_depth, Ordering::Relaxed);
6497 self.trigger_max_loop_iterations
6498 .store(config.max_loop_iterations, Ordering::Relaxed);
6499 Ok(())
6500 }
6501
6502 pub fn set_recursive_triggers(&self, recursive: bool) {
6503 use std::sync::atomic::Ordering;
6504 self.trigger_recursive.store(recursive, Ordering::Relaxed);
6505 }
6506
6507 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent> {
6511 self.notify.subscribe()
6512 }
6513
6514 pub fn subscribe_change_commits(&self) -> tokio::sync::broadcast::Receiver<()> {
6515 self.change_wake.subscribe()
6516 }
6517
6518 pub fn change_events_since(&self, last_event_id: Option<&str>) -> Result<CdcBatch> {
6523 let control = crate::ExecutionControl::new(None);
6524 self.change_events_since_controlled(last_event_id, &control)
6525 }
6526
6527 pub fn change_events_since_controlled(
6529 &self,
6530 last_event_id: Option<&str>,
6531 control: &crate::ExecutionControl,
6532 ) -> Result<CdcBatch> {
6533 use crate::wal::Op;
6534
6535 control.checkpoint()?;
6536 let resume = match last_event_id {
6537 Some(id) => {
6538 let (epoch, index) = id.split_once(':').ok_or_else(|| {
6539 MongrelError::InvalidArgument(format!(
6540 "invalid CDC event id {id:?}; expected <epoch>:<index>"
6541 ))
6542 })?;
6543 Some((
6544 epoch.parse::<u64>().map_err(|error| {
6545 MongrelError::InvalidArgument(format!("invalid CDC epoch: {error}"))
6546 })?,
6547 index.parse::<u32>().map_err(|error| {
6548 MongrelError::InvalidArgument(format!("invalid CDC index: {error}"))
6549 })?,
6550 ))
6551 }
6552 None => None,
6553 };
6554
6555 let mut wal = self.shared_wal.lock();
6556 wal.group_sync()?;
6557 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
6558 let records = crate::wal::SharedWal::replay_with_dek_controlled(
6559 &self.root,
6560 wal_dek.as_ref(),
6561 control,
6562 CDC_MAX_WAL_RECORDS,
6563 CDC_MAX_WAL_REPLAY_BYTES,
6564 )?;
6565 drop(wal);
6566 control.checkpoint()?;
6567
6568 let mut commits: HashMap<u64, (u64, Vec<crate::wal::AddedRun>)> = HashMap::new();
6569 let mut spilled_payloads: HashMap<(u64, u64), Vec<&[u8]>> = HashMap::new();
6570 for (index, record) in records.iter().enumerate() {
6571 if index % 256 == 0 {
6572 control.checkpoint()?;
6573 }
6574 if let Op::TxnCommit { epoch, added_runs } = &record.op {
6575 commits.insert(record.txn_id, (*epoch, added_runs.clone()));
6576 }
6577 if let Op::SpilledRows { table_id, rows } = &record.op {
6578 spilled_payloads
6579 .entry((record.txn_id, *table_id))
6580 .or_default()
6581 .push(rows);
6582 }
6583 }
6584 let earliest_epoch = commits.values().map(|(epoch, _)| *epoch).min();
6585 let current_epoch = self.epoch.committed().0;
6586 let retention_floor = crate::replication::replication_wal_floor(&self.root)?;
6587 let gap = resume.is_some_and(|(epoch, _)| {
6588 retention_floor != 0 && epoch <= retention_floor && epoch <= current_epoch
6589 });
6590 if gap {
6591 return Ok(CdcBatch {
6592 events: Vec::new(),
6593 current_epoch,
6594 earliest_epoch,
6595 gap: true,
6596 });
6597 }
6598
6599 let table_names: HashMap<u64, String> = self
6600 .catalog
6601 .read()
6602 .tables
6603 .iter()
6604 .map(|entry| (entry.table_id, entry.name.clone()))
6605 .collect();
6606 let mut before_images: HashMap<(u64, u64, u64), crate::memtable::Row> = HashMap::new();
6607 let mut retained_bytes = 0_usize;
6608 for (index, record) in records.iter().enumerate() {
6609 if index % 256 == 0 {
6610 control.checkpoint()?;
6611 }
6612 if !commits.contains_key(&record.txn_id) {
6613 continue;
6614 }
6615 let Op::BeforeImage {
6616 table_id,
6617 row_id,
6618 row,
6619 } = &record.op
6620 else {
6621 continue;
6622 };
6623 if row.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
6624 return Err(MongrelError::ResourceLimitExceeded {
6625 resource: "CDC before-image bytes",
6626 requested: row.len(),
6627 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
6628 });
6629 }
6630 let before: crate::memtable::Row = bincode::deserialize(row)?;
6631 if before_images.len() >= CDC_MAX_ROWS {
6632 return Err(MongrelError::ResourceLimitExceeded {
6633 resource: "CDC before-image rows",
6634 requested: before_images.len().saturating_add(1),
6635 limit: CDC_MAX_ROWS,
6636 });
6637 }
6638 charge_cdc_bytes(
6639 &mut retained_bytes,
6640 cdc_row_storage_bytes(&before),
6641 "CDC retained bytes",
6642 )?;
6643 before_images.insert((record.txn_id, *table_id, row_id.0), before);
6644 }
6645 let mut operation_indices: HashMap<u64, u32> = HashMap::new();
6646 let mut events = Vec::new();
6647 let mut decoded_rows = before_images.len();
6648 for (record_index, record) in records.iter().enumerate() {
6649 if record_index % 256 == 0 {
6650 control.checkpoint()?;
6651 }
6652 let Some((commit_epoch, _)) = commits.get(&record.txn_id) else {
6653 continue;
6654 };
6655 let event = match &record.op {
6656 Op::Put { table_id, rows } => {
6657 if rows.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
6658 return Err(MongrelError::ResourceLimitExceeded {
6659 resource: "CDC inline row bytes",
6660 requested: rows.len(),
6661 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
6662 });
6663 }
6664 let rows: Vec<crate::memtable::Row> = bincode::deserialize(rows)?;
6665 decoded_rows = decoded_rows.saturating_add(rows.len());
6666 if decoded_rows > CDC_MAX_ROWS {
6667 return Err(MongrelError::ResourceLimitExceeded {
6668 resource: "CDC decoded rows",
6669 requested: decoded_rows,
6670 limit: CDC_MAX_ROWS,
6671 });
6672 }
6673 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(512);
6674 let mut peak_bytes = retained_bytes;
6675 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
6676 let data = serde_json::to_value(rows)
6677 .map_err(|error| MongrelError::Other(format!("CDC JSON: {error}")))?;
6678 Some((*table_id, "put", data, event_bytes))
6679 }
6680 Op::Delete { table_id, row_ids } => {
6681 let before = row_ids
6682 .iter()
6683 .filter_map(|row_id| {
6684 before_images
6685 .get(&(record.txn_id, *table_id, row_id.0))
6686 .cloned()
6687 })
6688 .collect::<Vec<_>>();
6689 let event_bytes = cdc_rows_json_bytes(&before)
6690 .saturating_add(
6691 row_ids
6692 .len()
6693 .saturating_mul(std::mem::size_of::<serde_json::Value>()),
6694 )
6695 .saturating_add(512);
6696 let mut peak_bytes = retained_bytes;
6697 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
6698 Some((
6699 *table_id,
6700 "delete",
6701 serde_json::json!({
6702 "row_ids": row_ids.iter().map(|row_id| row_id.0).collect::<Vec<_>>(),
6703 "before": before,
6704 }),
6705 event_bytes,
6706 ))
6707 }
6708 Op::TruncateTable { table_id } => {
6709 Some((*table_id, "truncate", serde_json::Value::Null, 512))
6710 }
6711 _ => None,
6712 };
6713 if let Some((table_id, op, data, event_bytes)) = event {
6714 let index = operation_indices.entry(record.txn_id).or_insert(0);
6715 let event_position = (*commit_epoch, *index);
6716 *index = index.saturating_add(1);
6717 if resume.is_some_and(|position| event_position <= position) {
6718 continue;
6719 }
6720 if events.len() >= CDC_MAX_EVENTS {
6721 return Err(MongrelError::ResourceLimitExceeded {
6722 resource: "CDC events",
6723 requested: events.len().saturating_add(1),
6724 limit: CDC_MAX_EVENTS,
6725 });
6726 }
6727 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
6728 events.push(ChangeEvent {
6729 id: Some(format!("{}:{}", event_position.0, event_position.1)),
6730 channel: "changes".into(),
6731 table_id: Some(table_id),
6732 table: table_names.get(&table_id).cloned().unwrap_or_default(),
6733 op: op.into(),
6734 epoch: *commit_epoch,
6735 txn_id: Some(record.txn_id),
6736 message: None,
6737 data: Some(data),
6738 });
6739 }
6740 if let Op::TxnCommit { added_runs, .. } = &record.op {
6741 for run in added_runs {
6742 control.checkpoint()?;
6743 let index = operation_indices.entry(record.txn_id).or_insert(0);
6744 let event_position = (*commit_epoch, *index);
6745 *index = index.saturating_add(1);
6746 if resume.is_some_and(|position| event_position <= position) {
6747 continue;
6748 }
6749 let mut rows = if let Some(payloads) =
6750 spilled_payloads.get(&(record.txn_id, run.table_id))
6751 {
6752 let mut rows = Vec::new();
6753 for payload in payloads {
6754 control.checkpoint()?;
6755 if payload.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
6756 return Err(MongrelError::ResourceLimitExceeded {
6757 resource: "CDC spilled row bytes",
6758 requested: payload.len(),
6759 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
6760 });
6761 }
6762 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(payload)?;
6763 if decoded_rows
6764 .saturating_add(rows.len())
6765 .saturating_add(chunk.len())
6766 > CDC_MAX_ROWS
6767 {
6768 return Err(MongrelError::ResourceLimitExceeded {
6769 resource: "CDC decoded rows",
6770 requested: decoded_rows
6771 .saturating_add(rows.len())
6772 .saturating_add(chunk.len()),
6773 limit: CDC_MAX_ROWS,
6774 });
6775 }
6776 rows.extend(chunk);
6777 }
6778 rows
6779 } else {
6780 let Some(handle) = self.tables.read().get(&run.table_id).cloned() else {
6781 return Ok(CdcBatch {
6782 events: Vec::new(),
6783 current_epoch,
6784 earliest_epoch,
6785 gap: true,
6786 });
6787 };
6788 let table = handle.lock();
6789 let mut reader = match table.open_reader(run.run_id) {
6790 Ok(reader) => reader,
6791 Err(_) => {
6792 return Ok(CdcBatch {
6793 events: Vec::new(),
6794 current_epoch,
6795 earliest_epoch,
6796 gap: true,
6797 })
6798 }
6799 };
6800 let remaining = CDC_MAX_ROWS.saturating_sub(decoded_rows);
6801 let rows = reader.all_rows_controlled(control, remaining)?;
6802 drop(reader);
6803 drop(table);
6804 rows
6805 };
6806 for row in &mut rows {
6807 row.committed_epoch = Epoch(*commit_epoch);
6808 }
6809 decoded_rows = decoded_rows.saturating_add(rows.len());
6810 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(768);
6811 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
6812 if events.len() >= CDC_MAX_EVENTS {
6813 return Err(MongrelError::ResourceLimitExceeded {
6814 resource: "CDC events",
6815 requested: events.len().saturating_add(1),
6816 limit: CDC_MAX_EVENTS,
6817 });
6818 }
6819 events.push(ChangeEvent {
6820 id: Some(format!("{}:{}", event_position.0, event_position.1)),
6821 channel: "changes".into(),
6822 table_id: Some(run.table_id),
6823 table: table_names.get(&run.table_id).cloned().unwrap_or_default(),
6824 op: "put_run".into(),
6825 epoch: *commit_epoch,
6826 txn_id: Some(record.txn_id),
6827 message: None,
6828 data: Some(serde_json::json!({
6829 "run_id": run.run_id.to_string(),
6830 "row_count": run.row_count,
6831 "min_row_id": run.min_row_id,
6832 "max_row_id": run.max_row_id,
6833 "rows": rows,
6834 })),
6835 });
6836 }
6837 }
6838 }
6839 control.checkpoint()?;
6840 Ok(CdcBatch {
6841 events,
6842 current_epoch,
6843 earliest_epoch,
6844 gap: false,
6845 })
6846 }
6847
6848 pub fn notify(&self, channel: &str, message: Option<String>) {
6851 let _ = self.notify.send(ChangeEvent {
6852 id: None,
6853 channel: channel.to_string(),
6854 table_id: None,
6855 table: String::new(),
6856 op: "notify".into(),
6857 epoch: self.epoch.visible().0,
6858 txn_id: None,
6859 message,
6860 data: None,
6861 });
6862 }
6863
6864 pub fn call_procedure(
6865 &self,
6866 name: &str,
6867 args: HashMap<String, crate::Value>,
6868 ) -> Result<ProcedureCallResult> {
6869 self.call_procedure_as(name, args, None)
6870 }
6871
6872 pub fn call_procedure_as(
6873 &self,
6874 name: &str,
6875 args: HashMap<String, crate::Value>,
6876 principal: Option<&crate::auth::Principal>,
6877 ) -> Result<ProcedureCallResult> {
6878 let control = crate::ExecutionControl::new(None);
6879 self.call_procedure_as_controlled(name, args, principal, &control, || true)
6880 }
6881
6882 #[doc(hidden)]
6885 pub fn call_procedure_as_bound(
6886 &self,
6887 expected: &StoredProcedure,
6888 args: HashMap<String, crate::Value>,
6889 principal: Option<&crate::auth::Principal>,
6890 ) -> Result<ProcedureCallResult> {
6891 self.require_for(principal, &crate::auth::Permission::All)?;
6892 let procedure = self.procedure(&expected.name).ok_or_else(|| {
6893 MongrelError::NotFound(format!("procedure {:?} not found", expected.name))
6894 })?;
6895 if &procedure != expected {
6896 return Err(MongrelError::Conflict(format!(
6897 "procedure {:?} changed after request authorization",
6898 expected.name
6899 )));
6900 }
6901 let control = crate::ExecutionControl::new(None);
6902 self.execute_procedure_as_controlled(procedure, args, principal, &control, || true)
6903 }
6904
6905 #[doc(hidden)]
6910 pub fn call_procedure_as_controlled<F>(
6911 &self,
6912 name: &str,
6913 args: HashMap<String, crate::Value>,
6914 principal: Option<&crate::auth::Principal>,
6915 control: &crate::ExecutionControl,
6916 before_commit: F,
6917 ) -> Result<ProcedureCallResult>
6918 where
6919 F: FnOnce() -> bool,
6920 {
6921 self.require_for(principal, &crate::auth::Permission::All)?;
6925 let procedure = self
6926 .procedure(name)
6927 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
6928 self.execute_procedure_as_controlled(procedure, args, principal, control, before_commit)
6929 }
6930
6931 fn execute_procedure_as_controlled<F>(
6932 &self,
6933 procedure: StoredProcedure,
6934 args: HashMap<String, crate::Value>,
6935 principal: Option<&crate::auth::Principal>,
6936 control: &crate::ExecutionControl,
6937 before_commit: F,
6938 ) -> Result<ProcedureCallResult>
6939 where
6940 F: FnOnce() -> bool,
6941 {
6942 let args = bind_procedure_args(&procedure, args)?;
6943 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
6944 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
6945 if has_writes {
6946 let mut tx = self.begin_as(principal.cloned());
6947 let run = (|| {
6948 for (step_index, step) in procedure.body.steps.iter().enumerate() {
6949 if step_index % 256 == 0 {
6950 control.checkpoint()?;
6951 }
6952 let output = self.execute_procedure_step(
6953 step,
6954 &args,
6955 &outputs,
6956 Some(&mut tx),
6957 principal,
6958 Some(control),
6959 )?;
6960 outputs.insert(step.id().to_string(), output);
6961 }
6962 control.checkpoint()?;
6963 eval_return_output(&procedure.body.return_value, &args, &outputs)
6964 })();
6965 match run {
6966 Ok(output) => {
6967 control.checkpoint()?;
6968 if !before_commit() {
6969 tx.rollback();
6970 return Err(MongrelError::Cancelled);
6971 }
6972 let epoch = tx.commit()?.0;
6973 Ok(ProcedureCallResult {
6974 epoch: Some(epoch),
6975 output,
6976 })
6977 }
6978 Err(e) => {
6979 tx.rollback();
6980 Err(e)
6981 }
6982 }
6983 } else {
6984 for (step_index, step) in procedure.body.steps.iter().enumerate() {
6985 if step_index % 256 == 0 {
6986 control.checkpoint()?;
6987 }
6988 let output = self.execute_procedure_step(
6989 step,
6990 &args,
6991 &outputs,
6992 None,
6993 principal,
6994 Some(control),
6995 )?;
6996 outputs.insert(step.id().to_string(), output);
6997 }
6998 control.checkpoint()?;
6999 Ok(ProcedureCallResult {
7000 epoch: None,
7001 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
7002 })
7003 }
7004 }
7005
7006 fn execute_procedure_step(
7007 &self,
7008 step: &ProcedureStep,
7009 args: &HashMap<String, crate::Value>,
7010 outputs: &HashMap<String, ProcedureCallOutput>,
7011 tx: Option<&mut crate::txn::Transaction<'_>>,
7012 principal: Option<&crate::auth::Principal>,
7013 control: Option<&crate::ExecutionControl>,
7014 ) -> Result<ProcedureCallOutput> {
7015 if let Some(control) = control {
7016 control.checkpoint()?;
7017 }
7018 match step {
7019 ProcedureStep::NativeQuery {
7020 table,
7021 conditions,
7022 projection,
7023 limit,
7024 ..
7025 } => {
7026 let mut q = crate::Query::new();
7027 for condition in conditions {
7028 q = q.and(eval_condition(condition, args, outputs)?);
7029 }
7030 let handle = self.table(table)?;
7031 let rows = handle.lock().query(&q)?;
7032 let mut rows = self.secure_rows_for(table, rows, principal)?;
7033 if let Some(limit) = limit {
7034 rows.truncate(*limit);
7035 }
7036 let projection = projection.as_ref();
7037 let mut output = Vec::with_capacity(rows.len());
7038 for (row_index, row) in rows.into_iter().enumerate() {
7039 if row_index % 256 == 0 {
7040 if let Some(control) = control {
7041 control.checkpoint()?;
7042 }
7043 }
7044 output.push(ProcedureCallRow {
7045 row_id: Some(row.row_id),
7046 columns: match projection {
7047 Some(ids) => row
7048 .columns
7049 .into_iter()
7050 .filter(|(id, _)| ids.contains(id))
7051 .collect(),
7052 None => row.columns,
7053 },
7054 });
7055 }
7056 Ok(ProcedureCallOutput::Rows(output))
7057 }
7058 ProcedureStep::Put {
7059 table,
7060 cells,
7061 returning,
7062 ..
7063 } => {
7064 let tx = tx.ok_or_else(|| {
7065 MongrelError::InvalidArgument(
7066 "write procedure step requires a transaction".into(),
7067 )
7068 })?;
7069 let cells = eval_cells(cells, args, outputs)?;
7070 if *returning {
7071 let out = tx.put_returning(table, cells)?;
7072 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
7073 row_id: None,
7074 columns: out.row.columns.into_iter().collect(),
7075 }))
7076 } else {
7077 tx.put(table, cells)?;
7078 Ok(ProcedureCallOutput::Null)
7079 }
7080 }
7081 ProcedureStep::Upsert {
7082 table,
7083 cells,
7084 update_cells,
7085 returning,
7086 ..
7087 } => {
7088 let tx = tx.ok_or_else(|| {
7089 MongrelError::InvalidArgument(
7090 "write procedure step requires a transaction".into(),
7091 )
7092 })?;
7093 let cells = eval_cells(cells, args, outputs)?;
7094 let action = match update_cells {
7095 Some(update_cells) => {
7096 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
7097 }
7098 None => crate::UpsertAction::DoNothing,
7099 };
7100 let out = tx.upsert(table, cells, action)?;
7101 if *returning {
7102 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
7103 row_id: None,
7104 columns: out.row.columns.into_iter().collect(),
7105 }))
7106 } else {
7107 Ok(ProcedureCallOutput::Null)
7108 }
7109 }
7110 ProcedureStep::DeleteByPk { table, pk, .. } => {
7111 let tx = tx.ok_or_else(|| {
7112 MongrelError::InvalidArgument(
7113 "write procedure step requires a transaction".into(),
7114 )
7115 })?;
7116 let pk = eval_value(pk, args, outputs)?;
7117 let handle = self.table(table)?;
7118 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
7119 MongrelError::NotFound("procedure delete_by_pk target not found".into())
7120 })?;
7121 tx.delete(table, row_id)?;
7122 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
7123 }
7124 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
7125 "DeleteRows procedure step is not supported by the core executor yet".into(),
7126 )),
7127 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
7128 "SqlQuery procedure step must be executed by mongreldb-query".into(),
7129 )),
7130 }
7131 }
7132
7133 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
7134 let cat = self.catalog.read();
7135 for step in &procedure.body.steps {
7136 let Some(table_name) = step.table() else {
7137 continue;
7138 };
7139 let schema = &cat
7140 .live(table_name)
7141 .ok_or_else(|| {
7142 MongrelError::InvalidArgument(format!(
7143 "procedure {:?} references unknown table {table_name:?}",
7144 procedure.name
7145 ))
7146 })?
7147 .schema;
7148 match step {
7149 ProcedureStep::NativeQuery {
7150 conditions,
7151 projection,
7152 ..
7153 } => {
7154 for condition in conditions {
7155 validate_condition_columns(condition, schema)?;
7156 }
7157 if let Some(projection) = projection {
7158 for id in projection {
7159 validate_column_id(*id, schema)?;
7160 }
7161 }
7162 }
7163 ProcedureStep::Put { cells, .. } => {
7164 for cell in cells {
7165 validate_column_id(cell.column_id, schema)?;
7166 }
7167 }
7168 ProcedureStep::Upsert {
7169 cells,
7170 update_cells,
7171 ..
7172 } => {
7173 for cell in cells {
7174 validate_column_id(cell.column_id, schema)?;
7175 }
7176 if let Some(update_cells) = update_cells {
7177 for cell in update_cells {
7178 validate_column_id(cell.column_id, schema)?;
7179 }
7180 }
7181 }
7182 ProcedureStep::DeleteByPk { .. } => {
7183 if schema.primary_key().is_none() {
7184 return Err(MongrelError::InvalidArgument(format!(
7185 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
7186 procedure.name
7187 )));
7188 }
7189 }
7190 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
7191 }
7192 }
7193 Ok(())
7194 }
7195
7196 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
7197 let cat = self.catalog.read();
7198 let target_schema = match &trigger.target {
7199 TriggerTarget::Table(target_name) => cat
7200 .live(target_name)
7201 .ok_or_else(|| {
7202 MongrelError::InvalidArgument(format!(
7203 "trigger {:?} references unknown target table {target_name:?}",
7204 trigger.name
7205 ))
7206 })?
7207 .schema
7208 .clone(),
7209 TriggerTarget::View(_) => Schema {
7210 columns: trigger.target_columns.clone(),
7211 ..Schema::default()
7212 },
7213 };
7214 for col in &trigger.update_of {
7215 if target_schema.column(col).is_none() {
7216 return Err(MongrelError::InvalidArgument(format!(
7217 "trigger {:?} UPDATE OF references unknown column {col:?}",
7218 trigger.name
7219 )));
7220 }
7221 }
7222 if let Some(expr) = &trigger.when {
7223 validate_trigger_expr(expr, &target_schema, trigger.event)?;
7224 }
7225 let mut select_schemas: HashMap<String, &Schema> = HashMap::new();
7226 for step in &trigger.program.steps {
7227 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
7228 {
7229 return Err(MongrelError::InvalidArgument(
7230 "SetNew trigger steps are only valid in BEFORE triggers".into(),
7231 ));
7232 }
7233 validate_trigger_step(
7234 step,
7235 &cat,
7236 &target_schema,
7237 trigger.event,
7238 &mut select_schemas,
7239 )?;
7240 }
7241 Ok(())
7242 }
7243
7244 pub fn begin(&self) -> crate::txn::Transaction<'_> {
7246 self.begin_with_isolation(crate::txn::IsolationLevel::default())
7247 }
7248
7249 fn transaction_principal_snapshot(&self) -> (Option<crate::auth::Principal>, bool) {
7250 let principal = self.principal.read().clone();
7251 let catalog_bound = principal.as_ref().is_some_and(|principal| {
7252 let catalog = self.catalog.read();
7253 catalog.require_auth || principal.user_id != 0
7254 });
7255 (principal, catalog_bound)
7256 }
7257
7258 pub fn begin_as(
7259 &self,
7260 principal: Option<crate::auth::Principal>,
7261 ) -> crate::txn::Transaction<'_> {
7262 let catalog_bound = principal.as_ref().is_some_and(|principal| {
7263 let catalog = self.catalog.read();
7264 catalog.require_auth || principal.user_id != 0
7265 });
7266 let txn_id = self.alloc_txn_id();
7267 let read = Snapshot::at(self.epoch.visible());
7268 crate::txn::Transaction::new(self, txn_id, read).with_principal(principal, catalog_bound)
7269 }
7270
7271 pub fn begin_with_isolation(
7273 &self,
7274 level: crate::txn::IsolationLevel,
7275 ) -> crate::txn::Transaction<'_> {
7276 let txn_id = self.alloc_txn_id();
7277 let epoch = match level {
7278 crate::txn::IsolationLevel::ReadCommitted => self.epoch.visible(),
7279 _ => self.epoch.visible(),
7280 };
7281 let read = Snapshot::at(epoch);
7282 let (principal, catalog_bound) = self.transaction_principal_snapshot();
7283 crate::txn::Transaction::new(self, txn_id, read).with_principal(principal, catalog_bound)
7284 }
7285
7286 pub fn begin_with_external_trigger_bridge<'a>(
7289 &'a self,
7290 bridge: &'a dyn ExternalTriggerBridge,
7291 ) -> crate::txn::Transaction<'a> {
7292 let txn_id = self.alloc_txn_id();
7293 let read = Snapshot::at(self.epoch.visible());
7294 let (principal, catalog_bound) = self.transaction_principal_snapshot();
7295 crate::txn::Transaction::new(self, txn_id, read)
7296 .with_external_trigger_bridge(bridge)
7297 .with_principal(principal, catalog_bound)
7298 }
7299
7300 pub fn begin_with_external_trigger_bridge_as<'a>(
7301 &'a self,
7302 bridge: &'a dyn ExternalTriggerBridge,
7303 principal: Option<crate::auth::Principal>,
7304 ) -> crate::txn::Transaction<'a> {
7305 let catalog_bound = principal.as_ref().is_some_and(|principal| {
7306 let catalog = self.catalog.read();
7307 catalog.require_auth || principal.user_id != 0
7308 });
7309 let txn_id = self.alloc_txn_id();
7310 let read = Snapshot::at(self.epoch.visible());
7311 crate::txn::Transaction::new(self, txn_id, read)
7312 .with_external_trigger_bridge(bridge)
7313 .with_principal(principal, catalog_bound)
7314 }
7315
7316 pub fn transaction<T>(
7318 &self,
7319 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7320 ) -> Result<T> {
7321 let mut tx = self.begin();
7322 match f(&mut tx) {
7323 Ok(out) => {
7324 tx.commit()?;
7325 Ok(out)
7326 }
7327 Err(e) => {
7328 tx.rollback();
7329 Err(e)
7330 }
7331 }
7332 }
7333
7334 pub fn transaction_with_row_ids<T>(
7335 &self,
7336 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7337 ) -> Result<(T, Vec<RowId>)> {
7338 let mut tx = self.begin();
7339 match f(&mut tx) {
7340 Ok(output) => {
7341 let (_, row_ids) = tx.commit_with_row_ids()?;
7342 Ok((output, row_ids))
7343 }
7344 Err(error) => {
7345 tx.rollback();
7346 Err(error)
7347 }
7348 }
7349 }
7350
7351 pub fn transaction_for_current_principal<T>(
7352 &self,
7353 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7354 ) -> Result<T> {
7355 if self.principal.read().is_some() {
7356 self.refresh_principal()?;
7357 }
7358 let mut transaction = self.begin_as(self.principal.read().clone());
7359 match f(&mut transaction) {
7360 Ok(output) => {
7361 transaction.commit()?;
7362 Ok(output)
7363 }
7364 Err(error) => {
7365 transaction.rollback();
7366 Err(error)
7367 }
7368 }
7369 }
7370
7371 pub fn transaction_for_current_principal_with_epoch<T>(
7372 &self,
7373 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7374 ) -> Result<(Epoch, T)> {
7375 if self.principal.read().is_some() {
7376 self.refresh_principal()?;
7377 }
7378 let mut transaction = self.begin_as(self.principal.read().clone());
7379 match f(&mut transaction) {
7380 Ok(output) => {
7381 let epoch = transaction.commit()?;
7382 Ok((epoch, output))
7383 }
7384 Err(error) => {
7385 transaction.rollback();
7386 Err(error)
7387 }
7388 }
7389 }
7390
7391 pub fn transaction_with_row_ids_for_current_principal<T>(
7392 &self,
7393 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7394 ) -> Result<(T, Vec<RowId>)> {
7395 if self.principal.read().is_some() {
7396 self.refresh_principal()?;
7397 }
7398 let mut transaction = self.begin_as(self.principal.read().clone());
7399 match f(&mut transaction) {
7400 Ok(output) => {
7401 let (_, row_ids) = transaction.commit_with_row_ids()?;
7402 Ok((output, row_ids))
7403 }
7404 Err(error) => {
7405 transaction.rollback();
7406 Err(error)
7407 }
7408 }
7409 }
7410
7411 pub fn transaction_with_external_trigger_bridge<'a, T>(
7414 &'a self,
7415 bridge: &'a dyn ExternalTriggerBridge,
7416 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7417 ) -> Result<T> {
7418 let mut tx = self.begin_with_external_trigger_bridge(bridge);
7419 match f(&mut tx) {
7420 Ok(out) => {
7421 tx.commit()?;
7422 Ok(out)
7423 }
7424 Err(e) => {
7425 tx.rollback();
7426 Err(e)
7427 }
7428 }
7429 }
7430
7431 pub fn transaction_with_external_trigger_bridge_as<'a, T>(
7432 &'a self,
7433 bridge: &'a dyn ExternalTriggerBridge,
7434 principal: Option<crate::auth::Principal>,
7435 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7436 ) -> Result<T> {
7437 let mut tx = self.begin_with_external_trigger_bridge_as(bridge, principal);
7438 match f(&mut tx) {
7439 Ok(output) => {
7440 tx.commit()?;
7441 Ok(output)
7442 }
7443 Err(error) => {
7444 tx.rollback();
7445 Err(error)
7446 }
7447 }
7448 }
7449
7450 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
7453 self.active_txns.register(epoch)
7454 }
7455
7456 fn fill_auto_increment_for_staging(
7457 &self,
7458 staging: &mut [(u64, crate::txn::Staged)],
7459 control: Option<&crate::ExecutionControl>,
7460 ) -> Result<()> {
7461 let mut puts_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
7462 for (index, (table_id, staged)) in staging.iter().enumerate() {
7463 commit_prepare_checkpoint(control, index)?;
7464 if matches!(staged, crate::txn::Staged::Put(_)) {
7465 puts_by_table.entry(*table_id).or_default().push(index);
7466 }
7467 }
7468
7469 let tables = self.tables.read();
7470 for (table_index, (table_id, indexes)) in puts_by_table.into_iter().enumerate() {
7471 commit_prepare_checkpoint(control, table_index)?;
7472 if let Some(handle) = tables.get(&table_id) {
7473 #[cfg(test)]
7474 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
7475 let mut t = handle.lock();
7476 for (fill_index, index) in indexes.into_iter().enumerate() {
7477 commit_prepare_checkpoint(control, fill_index)?;
7478 if let crate::txn::Staged::Put(cells) = &mut staging[index].1 {
7479 t.fill_auto_inc(cells)?;
7480 }
7481 }
7482 }
7483 }
7484 Ok(())
7485 }
7486
7487 fn expand_table_triggers(
7488 &self,
7489 staging: &mut Vec<(u64, crate::txn::Staged)>,
7490 read_epoch: Epoch,
7491 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
7492 external_states: &mut Vec<(String, Vec<u8>)>,
7493 control: Option<&crate::ExecutionControl>,
7494 ) -> Result<()> {
7495 commit_prepare_checkpoint(control, 0)?;
7496 let mut external_writes = Vec::new();
7497 let config = self.trigger_config();
7498 if config.recursive_triggers {
7499 let chunk = std::mem::take(staging);
7500 let stacks = vec![Vec::new(); chunk.len()];
7501 *staging = self.expand_trigger_chunk(
7502 chunk,
7503 stacks,
7504 read_epoch,
7505 0,
7506 config.max_depth,
7507 &mut external_writes,
7508 &config,
7509 control,
7510 )?;
7511 self.apply_external_trigger_writes(
7512 external_writes,
7513 external_trigger_bridge,
7514 external_states,
7515 staging,
7516 control,
7517 )?;
7518 return Ok(());
7519 }
7520
7521 let mut expansion =
7522 self.expand_table_triggers_once(staging, read_epoch, None, &config, control)?;
7523 if !expansion.before.is_empty() {
7524 let mut final_staging = expansion.before;
7525 final_staging.extend(filter_ignored_staging(
7526 std::mem::take(staging),
7527 &expansion.ignored_indices,
7528 ));
7529 *staging = final_staging;
7530 } else if !expansion.ignored_indices.is_empty() {
7531 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
7532 }
7533 staging.append(&mut expansion.after);
7534 external_writes.append(&mut expansion.before_external);
7535 external_writes.append(&mut expansion.after_external);
7536 self.apply_external_trigger_writes(
7537 external_writes,
7538 external_trigger_bridge,
7539 external_states,
7540 staging,
7541 control,
7542 )?;
7543 Ok(())
7544 }
7545
7546 #[allow(clippy::too_many_arguments)]
7547 fn expand_trigger_chunk(
7548 &self,
7549 mut chunk: Vec<(u64, crate::txn::Staged)>,
7550 stacks: Vec<Vec<String>>,
7551 read_epoch: Epoch,
7552 depth: u32,
7553 max_depth: u32,
7554 external_writes: &mut Vec<ExternalTriggerWrite>,
7555 config: &TriggerConfig,
7556 control: Option<&crate::ExecutionControl>,
7557 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
7558 if chunk.is_empty() {
7559 return Ok(Vec::new());
7560 }
7561 commit_prepare_checkpoint(control, 0)?;
7562 self.fill_auto_increment_for_staging(&mut chunk, control)?;
7563 let expansion = self.expand_table_triggers_once(
7564 &mut chunk,
7565 read_epoch,
7566 Some(&stacks),
7567 config,
7568 control,
7569 )?;
7570 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
7571 let stack = expansion
7572 .before_stacks
7573 .first()
7574 .or_else(|| expansion.after_stacks.first())
7575 .cloned()
7576 .unwrap_or_default();
7577 return Err(MongrelError::TriggerValidation(format!(
7578 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
7579 Self::format_trigger_stack(&stack)
7580 )));
7581 }
7582
7583 let mut out = Vec::new();
7584 external_writes.extend(expansion.before_external);
7585 out.extend(self.expand_trigger_chunk(
7586 expansion.before,
7587 expansion.before_stacks,
7588 read_epoch,
7589 depth + 1,
7590 max_depth,
7591 external_writes,
7592 config,
7593 control,
7594 )?);
7595 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
7596 external_writes.extend(expansion.after_external);
7597 out.extend(self.expand_trigger_chunk(
7598 expansion.after,
7599 expansion.after_stacks,
7600 read_epoch,
7601 depth + 1,
7602 max_depth,
7603 external_writes,
7604 config,
7605 control,
7606 )?);
7607 Ok(out)
7608 }
7609
7610 fn apply_external_trigger_writes(
7611 &self,
7612 writes: Vec<ExternalTriggerWrite>,
7613 bridge: Option<&dyn ExternalTriggerBridge>,
7614 external_states: &mut Vec<(String, Vec<u8>)>,
7615 staging: &mut Vec<(u64, crate::txn::Staged)>,
7616 control: Option<&crate::ExecutionControl>,
7617 ) -> Result<()> {
7618 if writes.is_empty() {
7619 return Ok(());
7620 }
7621 let bridge = bridge.ok_or_else(|| {
7622 MongrelError::TriggerValidation(
7623 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
7624 )
7625 })?;
7626 for (write_index, write) in writes.into_iter().enumerate() {
7627 commit_prepare_checkpoint(control, write_index)?;
7628 let table = write.table().to_string();
7629 let entry = self.external_table(&table).ok_or_else(|| {
7630 MongrelError::NotFound(format!("external table {table:?} not found"))
7631 })?;
7632 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
7633 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
7634 external_states.push((table, result.state));
7635 for (base_index, base_write) in result.base_writes.into_iter().enumerate() {
7636 commit_prepare_checkpoint(control, base_index)?;
7637 match base_write {
7638 ExternalTriggerBaseWrite::Put { table, cells } => {
7639 let table_id = self.table_id(&table)?;
7640 staging.push((table_id, crate::txn::Staged::Put(cells)));
7641 }
7642 ExternalTriggerBaseWrite::Delete { table, row_id } => {
7643 let table_id = self.table_id(&table)?;
7644 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
7645 }
7646 }
7647 }
7648 }
7649 dedup_external_states_in_place(external_states);
7650 Ok(())
7651 }
7652
7653 fn expand_table_triggers_once(
7654 &self,
7655 staging: &mut Vec<(u64, crate::txn::Staged)>,
7656 read_epoch: Epoch,
7657 trigger_stacks: Option<&[Vec<String>]>,
7658 config: &TriggerConfig,
7659 control: Option<&crate::ExecutionControl>,
7660 ) -> Result<TriggerExpansion> {
7661 commit_prepare_checkpoint(control, 0)?;
7662 let triggers: Vec<StoredTrigger> = self
7663 .catalog
7664 .read()
7665 .triggers
7666 .iter()
7667 .filter(|entry| {
7668 entry.trigger.enabled
7669 && matches!(
7670 entry.trigger.timing,
7671 TriggerTiming::Before | TriggerTiming::After
7672 )
7673 && matches!(entry.trigger.target, TriggerTarget::Table(_))
7674 })
7675 .map(|entry| entry.trigger.clone())
7676 .collect();
7677 if triggers.is_empty() || staging.is_empty() {
7678 return Ok(TriggerExpansion::default());
7679 }
7680
7681 let before_triggers = triggers
7682 .iter()
7683 .filter(|trigger| trigger.timing == TriggerTiming::Before)
7684 .cloned()
7685 .collect::<Vec<_>>();
7686 let after_triggers = triggers
7687 .iter()
7688 .filter(|trigger| trigger.timing == TriggerTiming::After)
7689 .cloned()
7690 .collect::<Vec<_>>();
7691
7692 let mut before_added = Vec::new();
7693 let mut before_stacks = Vec::new();
7694 let mut before_external = Vec::new();
7695 let mut ignored_indices = std::collections::BTreeSet::new();
7696 if !before_triggers.is_empty() {
7697 let before_events =
7698 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?;
7699 let mut out = TriggerProgramOutput {
7700 added: &mut before_added,
7701 added_stacks: &mut before_stacks,
7702 added_external: &mut before_external,
7703 ignored_indices: &mut ignored_indices,
7704 };
7705 self.execute_triggers_for_events(
7706 &before_triggers,
7707 &before_events,
7708 Some(staging),
7709 &mut out,
7710 config,
7711 read_epoch,
7712 control,
7713 )?;
7714 }
7715
7716 let after_events = if after_triggers.is_empty() {
7717 Vec::new()
7718 } else {
7719 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?
7720 .into_iter()
7721 .filter(|event| {
7722 !event
7723 .op_indices
7724 .iter()
7725 .any(|idx| ignored_indices.contains(idx))
7726 })
7727 .collect()
7728 };
7729
7730 let mut after_added = Vec::new();
7731 let mut after_stacks = Vec::new();
7732 let mut after_external = Vec::new();
7733 let mut out = TriggerProgramOutput {
7734 added: &mut after_added,
7735 added_stacks: &mut after_stacks,
7736 added_external: &mut after_external,
7737 ignored_indices: &mut ignored_indices,
7738 };
7739 self.execute_triggers_for_events(
7740 &after_triggers,
7741 &after_events,
7742 None,
7743 &mut out,
7744 config,
7745 read_epoch,
7746 control,
7747 )?;
7748 Ok(TriggerExpansion {
7749 before: before_added,
7750 before_stacks,
7751 before_external,
7752 after: after_added,
7753 after_stacks,
7754 after_external,
7755 ignored_indices,
7756 })
7757 }
7758
7759 #[allow(clippy::too_many_arguments)]
7760 fn execute_triggers_for_events(
7761 &self,
7762 triggers: &[StoredTrigger],
7763 events: &[WriteEvent],
7764 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
7765 out: &mut TriggerProgramOutput<'_>,
7766 config: &TriggerConfig,
7767 read_epoch: Epoch,
7768 control: Option<&crate::ExecutionControl>,
7769 ) -> Result<()> {
7770 let mut checkpoint_index = 0_usize;
7771 for event in events {
7772 for trigger in triggers {
7773 commit_prepare_checkpoint(control, checkpoint_index)?;
7774 checkpoint_index += 1;
7775 if event
7776 .op_indices
7777 .iter()
7778 .any(|idx| out.ignored_indices.contains(idx))
7779 {
7780 break;
7781 }
7782 let matches = {
7783 let cat = self.catalog.read();
7784 trigger_matches_event(trigger, event, &cat)?
7785 };
7786 if !matches {
7787 continue;
7788 }
7789 if let Some(when) = &trigger.when {
7790 if !eval_trigger_expr(when, event)? {
7791 continue;
7792 }
7793 }
7794 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
7795 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
7796 return Err(MongrelError::TriggerValidation(format!(
7797 "trigger recursion cycle detected; trigger stack: {}",
7798 Self::format_trigger_stack(&trigger_stack)
7799 )));
7800 }
7801 let outcome = match staging.as_mut() {
7802 Some(staging) => self.execute_trigger_program(
7803 trigger,
7804 event,
7805 Some(&mut **staging),
7806 out,
7807 &trigger_stack,
7808 config,
7809 read_epoch,
7810 control,
7811 )?,
7812 None => self.execute_trigger_program(
7813 trigger,
7814 event,
7815 None,
7816 out,
7817 &trigger_stack,
7818 config,
7819 read_epoch,
7820 control,
7821 )?,
7822 };
7823 if outcome == TriggerProgramOutcome::Ignore {
7824 out.ignored_indices.extend(event.op_indices.iter().copied());
7825 break;
7826 }
7827 }
7828 }
7829 Ok(())
7830 }
7831
7832 fn trigger_events_for_staging(
7833 &self,
7834 staging: &[(u64, crate::txn::Staged)],
7835 read_epoch: Epoch,
7836 trigger_stacks: Option<&[Vec<String>]>,
7837 control: Option<&crate::ExecutionControl>,
7838 ) -> Result<Vec<WriteEvent>> {
7839 use crate::txn::Staged;
7840 use std::collections::{HashMap, VecDeque};
7841
7842 let snapshot = Snapshot::at(read_epoch);
7843 let cat = self.catalog.read();
7844 let mut table_names = HashMap::new();
7845 let mut table_schemas = HashMap::new();
7846 for entry in cat
7847 .tables
7848 .iter()
7849 .filter(|entry| matches!(entry.state, TableState::Live))
7850 {
7851 table_names.insert(entry.table_id, entry.name.clone());
7852 table_schemas.insert(entry.table_id, entry.schema.clone());
7853 }
7854 drop(cat);
7855
7856 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
7857 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
7858 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
7859
7860 for (idx, (table_id, staged)) in staging.iter().enumerate() {
7861 commit_prepare_checkpoint(control, idx)?;
7862 let Some(schema) = table_schemas.get(table_id) else {
7863 continue;
7864 };
7865 let Some(pk) = schema.primary_key() else {
7866 continue;
7867 };
7868 match staged {
7869 Staged::Delete(row_id) => {
7870 let handle = self.table_by_id(*table_id)?;
7871 let Some(row) = handle.lock().get(*row_id, snapshot) else {
7872 continue;
7873 };
7874 let Some(pk_value) = row.columns.get(&pk.id) else {
7875 continue;
7876 };
7877 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
7878 delete_by_key
7879 .entry((*table_id, pk_value.encode_key()))
7880 .or_default()
7881 .push_back(idx);
7882 }
7883 Staged::Put(cells) => {
7884 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
7885 put_by_key
7886 .entry((*table_id, value.encode_key()))
7887 .or_default()
7888 .push_back(idx);
7889 }
7890 }
7891 Staged::Update { row_id, .. } => {
7892 let handle = self.table_by_id(*table_id)?;
7893 let row = handle.lock().get(*row_id, snapshot);
7894 if let Some(row) = row {
7895 old_rows.insert(idx, TriggerRowImage::from_row(row));
7896 }
7897 }
7898 Staged::Truncate => {}
7899 }
7900 }
7901
7902 let mut paired_delete = std::collections::HashSet::new();
7903 let mut paired_put = std::collections::HashSet::new();
7904 let mut events = Vec::new();
7905
7906 for (pair_index, (key, deletes)) in delete_by_key.iter_mut().enumerate() {
7907 commit_prepare_checkpoint(control, pair_index)?;
7908 let Some(puts) = put_by_key.get_mut(key) else {
7909 continue;
7910 };
7911 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
7912 paired_delete.insert(delete_idx);
7913 paired_put.insert(put_idx);
7914 let (table_id, _) = &staging[put_idx];
7915 let Some(table_name) = table_names.get(table_id).cloned() else {
7916 continue;
7917 };
7918 let old = old_rows.get(&delete_idx).cloned();
7919 let new = match &staging[put_idx].1 {
7920 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
7921 _ => None,
7922 };
7923 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
7924 events.push(WriteEvent {
7925 table: table_name,
7926 kind: TriggerEvent::Update,
7927 old,
7928 new,
7929 changed_columns,
7930 op_indices: vec![delete_idx, put_idx],
7931 put_idx: Some(put_idx),
7932 trigger_stack: Self::trigger_stack_for_indices(
7933 trigger_stacks,
7934 &[delete_idx, put_idx],
7935 ),
7936 });
7937 }
7938 }
7939
7940 for (idx, (table_id, staged)) in staging.iter().enumerate() {
7941 commit_prepare_checkpoint(control, idx)?;
7942 let Some(table_name) = table_names.get(table_id).cloned() else {
7943 continue;
7944 };
7945 match staged {
7946 Staged::Put(cells) if !paired_put.contains(&idx) => {
7947 let new = Some(TriggerRowImage::from_cells(cells));
7948 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
7949 events.push(WriteEvent {
7950 table: table_name,
7951 kind: TriggerEvent::Insert,
7952 old: None,
7953 new,
7954 changed_columns,
7955 op_indices: vec![idx],
7956 put_idx: Some(idx),
7957 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
7958 });
7959 }
7960 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
7961 let old = match old_rows.get(&idx).cloned() {
7962 Some(old) => Some(old),
7963 None => {
7964 let handle = self.table_by_id(*table_id)?;
7965 let row = handle.lock().get(*row_id, snapshot);
7966 row.map(TriggerRowImage::from_row)
7967 }
7968 };
7969 let Some(old) = old else {
7970 continue;
7971 };
7972 let changed_columns = old.columns.keys().copied().collect();
7973 events.push(WriteEvent {
7974 table: table_name,
7975 kind: TriggerEvent::Delete,
7976 old: Some(old),
7977 new: None,
7978 changed_columns,
7979 op_indices: vec![idx],
7980 put_idx: None,
7981 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
7982 });
7983 }
7984 Staged::Update { new_row: cells, .. } => {
7985 let old = old_rows.get(&idx).cloned();
7986 let new = Some(TriggerRowImage::from_cells(cells));
7987 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
7988 events.push(WriteEvent {
7989 table: table_name,
7990 kind: TriggerEvent::Update,
7991 old,
7992 new,
7993 changed_columns,
7994 op_indices: vec![idx],
7995 put_idx: Some(idx),
7996 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
7997 });
7998 }
7999 Staged::Truncate => {}
8000 _ => {}
8001 }
8002 }
8003
8004 Ok(events)
8005 }
8006
8007 #[allow(clippy::too_many_arguments)]
8008 fn execute_trigger_program(
8009 &self,
8010 trigger: &StoredTrigger,
8011 event: &WriteEvent,
8012 staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
8013 out: &mut TriggerProgramOutput<'_>,
8014 trigger_stack: &[String],
8015 config: &TriggerConfig,
8016 read_epoch: Epoch,
8017 control: Option<&crate::ExecutionControl>,
8018 ) -> Result<TriggerProgramOutcome> {
8019 let mut event = event.clone();
8020 let mut select_results: HashMap<String, Vec<TriggerRowImage>> = HashMap::new();
8021 self.execute_trigger_steps(
8022 trigger,
8023 &trigger.program.steps,
8024 &mut event,
8025 staging,
8026 out,
8027 trigger_stack,
8028 config,
8029 &mut select_results,
8030 0,
8031 None,
8032 read_epoch,
8033 control,
8034 )
8035 }
8036
8037 #[allow(clippy::too_many_arguments)]
8038 fn execute_trigger_steps(
8039 &self,
8040 trigger: &StoredTrigger,
8041 steps: &[TriggerStep],
8042 event: &mut WriteEvent,
8043 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
8044 out: &mut TriggerProgramOutput<'_>,
8045 trigger_stack: &[String],
8046 config: &TriggerConfig,
8047 select_results: &mut HashMap<String, Vec<TriggerRowImage>>,
8048 depth: u32,
8049 selected: Option<&TriggerRowImage>,
8050 read_epoch: Epoch,
8051 control: Option<&crate::ExecutionControl>,
8052 ) -> Result<TriggerProgramOutcome> {
8053 let _ = depth;
8054 for (step_index, step) in steps.iter().enumerate() {
8055 commit_prepare_checkpoint(control, step_index)?;
8056 match step {
8057 TriggerStep::SetNew { cells } => {
8058 if trigger.timing != TriggerTiming::Before {
8059 return Err(MongrelError::InvalidArgument(
8060 "SetNew trigger step is only valid in BEFORE triggers".into(),
8061 ));
8062 }
8063 let put_idx = event.put_idx.ok_or_else(|| {
8064 MongrelError::InvalidArgument(
8065 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
8066 )
8067 })?;
8068 let staging = staging.as_deref_mut().ok_or_else(|| {
8069 MongrelError::InvalidArgument(
8070 "SetNew trigger step requires mutable trigger staging".into(),
8071 )
8072 })?;
8073 let mut update_changed_columns = None;
8074 let row_cells = match staging.get_mut(put_idx).map(|(_, op)| op) {
8075 Some(crate::txn::Staged::Put(cells)) => cells,
8076 Some(crate::txn::Staged::Update {
8077 new_row,
8078 changed_columns,
8079 ..
8080 }) => {
8081 update_changed_columns = Some(changed_columns);
8082 new_row
8083 }
8084 _ => {
8085 return Err(MongrelError::InvalidArgument(
8086 "SetNew trigger step target row is not mutable".into(),
8087 ))
8088 }
8089 };
8090 for (column_id, value) in eval_trigger_cells(cells, event, selected)? {
8091 row_cells.retain(|(id, _)| *id != column_id);
8092 row_cells.push((column_id, value.clone()));
8093 if let Some(changed_columns) = &mut update_changed_columns {
8094 changed_columns.push(column_id);
8095 }
8096 if let Some(new) = &mut event.new {
8097 new.columns.insert(column_id, value);
8098 }
8099 }
8100 row_cells.sort_by_key(|(id, _)| *id);
8101 if let Some(changed_columns) = update_changed_columns {
8102 changed_columns.sort_unstable();
8103 changed_columns.dedup();
8104 }
8105 }
8106 TriggerStep::Insert { table, cells } => {
8107 let cells = eval_trigger_cells(cells, event, selected)?;
8108 if let Ok(table_id) = self.table_id(table) {
8109 out.added.push((table_id, crate::txn::Staged::Put(cells)));
8110 out.added_stacks.push(trigger_stack.to_vec());
8111 } else if self.external_table(table).is_some() {
8112 out.added_external.push(ExternalTriggerWrite::Insert {
8113 table: table.clone(),
8114 cells,
8115 });
8116 } else {
8117 return Err(MongrelError::NotFound(format!(
8118 "trigger {:?} insert target {table:?} not found",
8119 trigger.name
8120 )));
8121 }
8122 }
8123 TriggerStep::UpdateByPk { table, pk, cells } => {
8124 let pk = eval_trigger_value(pk, event, selected)?;
8125 let cells = eval_trigger_cells(cells, event, selected)?;
8126 if self.external_table(table).is_some() {
8127 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
8128 table: table.clone(),
8129 pk,
8130 cells,
8131 });
8132 } else {
8133 let row_id = self
8134 .table(table)?
8135 .lock()
8136 .lookup_pk(&pk.encode_key())
8137 .ok_or_else(|| {
8138 MongrelError::NotFound(format!(
8139 "trigger {:?} update target not found",
8140 trigger.name
8141 ))
8142 })?;
8143 let handle = self.table(table)?;
8144 let snapshot = Snapshot::at(self.epoch.visible());
8145 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
8146 MongrelError::NotFound(format!(
8147 "trigger {:?} update target not visible",
8148 trigger.name
8149 ))
8150 })?;
8151 let mut changed_columns = cells
8152 .iter()
8153 .map(|(column_id, _)| *column_id)
8154 .collect::<Vec<_>>();
8155 changed_columns.sort_unstable();
8156 changed_columns.dedup();
8157 let mut merged = old.columns;
8158 for (column_id, value) in cells {
8159 merged.insert(column_id, value);
8160 }
8161 out.added.push((
8162 self.table_id(table)?,
8163 crate::txn::Staged::Update {
8164 row_id,
8165 new_row: merged.into_iter().collect(),
8166 changed_columns,
8167 },
8168 ));
8169 out.added_stacks.push(trigger_stack.to_vec());
8170 }
8171 }
8172 TriggerStep::DeleteByPk { table, pk } => {
8173 let pk = eval_trigger_value(pk, event, selected)?;
8174 if self.external_table(table).is_some() {
8175 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
8176 table: table.clone(),
8177 pk,
8178 });
8179 } else {
8180 let row_id = self
8181 .table(table)?
8182 .lock()
8183 .lookup_pk(&pk.encode_key())
8184 .ok_or_else(|| {
8185 MongrelError::NotFound(format!(
8186 "trigger {:?} delete target not found",
8187 trigger.name
8188 ))
8189 })?;
8190 out.added
8191 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
8192 out.added_stacks.push(trigger_stack.to_vec());
8193 }
8194 }
8195 TriggerStep::Select {
8196 id,
8197 table,
8198 conditions,
8199 } => {
8200 let schema = self.table(table)?.lock().schema().clone();
8201 let snapshot = Snapshot::at(read_epoch);
8202 let handle = self.table(table)?;
8203 let rows = match control {
8204 Some(control) => {
8205 handle.lock().visible_rows_controlled(snapshot, control)?
8206 }
8207 None => handle.lock().visible_rows(snapshot)?,
8208 };
8209 let mut matched = Vec::new();
8210 for (row_index, row) in rows.into_iter().enumerate() {
8211 commit_prepare_checkpoint(control, row_index)?;
8212 let image = TriggerRowImage::from_row(row);
8213 let passes = conditions
8214 .iter()
8215 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
8216 .collect::<Result<Vec<_>>>()?
8217 .into_iter()
8218 .all(|b| b);
8219 if passes {
8220 matched.push(image);
8221 }
8222 }
8223 if let Some(pk) = schema.primary_key() {
8224 matched.sort_by(|a, b| {
8225 let av = a.columns.get(&pk.id).unwrap_or(&Value::Null);
8226 let bv = b.columns.get(&pk.id).unwrap_or(&Value::Null);
8227 value_order(av, bv).unwrap_or(std::cmp::Ordering::Equal)
8228 });
8229 }
8230 select_results.insert(id.clone(), matched);
8231 }
8232 TriggerStep::Foreach { id, steps } => {
8233 let rows = select_results.get(id).ok_or_else(|| {
8234 MongrelError::InvalidArgument(format!(
8235 "trigger {:?} foreach references unknown select id {id:?}",
8236 trigger.name
8237 ))
8238 })?;
8239 if rows.len() > config.max_loop_iterations as usize {
8240 return Err(MongrelError::InvalidArgument(format!(
8241 "trigger {:?} foreach exceeded max_loop_iterations ({})",
8242 trigger.name, config.max_loop_iterations
8243 )));
8244 }
8245 for (row_index, row) in rows.clone().into_iter().enumerate() {
8246 commit_prepare_checkpoint(control, row_index)?;
8247 let result = self.execute_trigger_steps(
8248 trigger,
8249 steps,
8250 event,
8251 staging.as_deref_mut(),
8252 out,
8253 trigger_stack,
8254 config,
8255 select_results,
8256 depth + 1,
8257 Some(&row),
8258 read_epoch,
8259 control,
8260 )?;
8261 if result == TriggerProgramOutcome::Ignore {
8262 return Ok(TriggerProgramOutcome::Ignore);
8263 }
8264 }
8265 }
8266 TriggerStep::DeleteWhere { table, conditions } => {
8267 let schema = self.table(table)?.lock().schema().clone();
8268 let snapshot = Snapshot::at(read_epoch);
8269 let handle = self.table(table)?;
8270 let rows = match control {
8271 Some(control) => {
8272 handle.lock().visible_rows_controlled(snapshot, control)?
8273 }
8274 None => handle.lock().visible_rows(snapshot)?,
8275 };
8276 let table_id = self.table_id(table)?;
8277 let mut to_delete = Vec::new();
8278 for (row_index, row) in rows.into_iter().enumerate() {
8279 commit_prepare_checkpoint(control, row_index)?;
8280 let image = TriggerRowImage::from_row(row.clone());
8281 let passes = conditions
8282 .iter()
8283 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
8284 .collect::<Result<Vec<_>>>()?
8285 .into_iter()
8286 .all(|b| b);
8287 if passes {
8288 to_delete.push((table_id, row.row_id));
8289 }
8290 }
8291 for (row_index, (table_id, row_id)) in to_delete.into_iter().enumerate() {
8292 commit_prepare_checkpoint(control, row_index)?;
8293 out.added
8294 .push((table_id, crate::txn::Staged::Delete(row_id)));
8295 out.added_stacks.push(trigger_stack.to_vec());
8296 }
8297 }
8298 TriggerStep::UpdateWhere {
8299 table,
8300 conditions,
8301 cells,
8302 } => {
8303 let schema = self.table(table)?.lock().schema().clone();
8304 let snapshot = Snapshot::at(read_epoch);
8305 let handle = self.table(table)?;
8306 let rows = match control {
8307 Some(control) => {
8308 handle.lock().visible_rows_controlled(snapshot, control)?
8309 }
8310 None => handle.lock().visible_rows(snapshot)?,
8311 };
8312 let table_id = self.table_id(table)?;
8313 let mut changed_columns =
8314 cells.iter().map(|cell| cell.column_id).collect::<Vec<_>>();
8315 changed_columns.sort_unstable();
8316 changed_columns.dedup();
8317 let mut to_update = Vec::new();
8318 for (row_index, row) in rows.into_iter().enumerate() {
8319 commit_prepare_checkpoint(control, row_index)?;
8320 let image = TriggerRowImage::from_row(row.clone());
8321 let passes = conditions
8322 .iter()
8323 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
8324 .collect::<Result<Vec<_>>>()?
8325 .into_iter()
8326 .all(|b| b);
8327 if passes {
8328 let new_cells = cells
8329 .iter()
8330 .map(|cell| {
8331 Ok((
8332 cell.column_id,
8333 eval_trigger_value(&cell.value, event, Some(&image))?,
8334 ))
8335 })
8336 .collect::<Result<Vec<_>>>()?;
8337 let mut merged = row.columns.clone();
8338 for (column_id, value) in new_cells {
8339 merged.insert(column_id, value);
8340 }
8341 to_update.push((table_id, row.row_id, merged));
8342 }
8343 }
8344 for (row_index, (table_id, row_id, merged)) in to_update.into_iter().enumerate()
8345 {
8346 commit_prepare_checkpoint(control, row_index)?;
8347 out.added.push((
8348 table_id,
8349 crate::txn::Staged::Update {
8350 row_id,
8351 new_row: merged.into_iter().collect(),
8352 changed_columns: changed_columns.clone(),
8353 },
8354 ));
8355 out.added_stacks.push(trigger_stack.to_vec());
8356 }
8357 }
8358 TriggerStep::Raise { action, message } => match action {
8359 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
8360 TriggerRaiseAction::Abort
8361 | TriggerRaiseAction::Fail
8362 | TriggerRaiseAction::Rollback => {
8363 let message = eval_trigger_value(message, event, selected)?;
8364 return Err(MongrelError::TriggerValidation(format!(
8365 "trigger {:?} raised: {}; trigger stack: {}",
8366 trigger.name,
8367 trigger_message(message),
8368 Self::format_trigger_stack(trigger_stack)
8369 )));
8370 }
8371 },
8372 }
8373 }
8374 Ok(TriggerProgramOutcome::Continue)
8375 }
8376
8377 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
8378 let Some(stacks) = stacks else {
8379 return Vec::new();
8380 };
8381 let mut out = Vec::new();
8382 for idx in indices {
8383 let Some(stack) = stacks.get(*idx) else {
8384 continue;
8385 };
8386 for name in stack {
8387 if !out.iter().any(|existing| existing == name) {
8388 out.push(name.clone());
8389 }
8390 }
8391 }
8392 out
8393 }
8394
8395 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
8396 let mut out = stack.to_vec();
8397 out.push(trigger_name.to_string());
8398 out
8399 }
8400
8401 fn format_trigger_stack(stack: &[String]) -> String {
8402 if stack.is_empty() {
8403 "<root>".into()
8404 } else {
8405 stack.join(" -> ")
8406 }
8407 }
8408
8409 fn validate_constraints(
8424 &self,
8425 staging: &mut Vec<(u64, crate::txn::Staged)>,
8426 read_epoch: Epoch,
8427 control: Option<&crate::ExecutionControl>,
8428 ) -> Result<()> {
8429 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
8430 use crate::memtable::Row;
8431 use crate::txn::Staged;
8432 use std::collections::HashSet;
8433
8434 commit_prepare_checkpoint(control, 0)?;
8435 let snapshot = Snapshot::at(read_epoch);
8436 let cat = self.catalog.read();
8437
8438 let live: Vec<(u64, &str, &crate::schema::Schema)> = cat
8440 .tables
8441 .iter()
8442 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
8443 .map(|e| (e.table_id, e.name.as_str(), &e.schema))
8444 .collect();
8445
8446 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
8448 if !any_constraints {
8449 return Ok(());
8450 }
8451
8452 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
8454 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
8455 if let Some(r) = rows_cache.get(&table_id) {
8456 return Ok(r.clone());
8457 }
8458 let handle = self.table_by_id(table_id)?;
8459 let rows = match control {
8460 Some(control) => handle.lock().visible_rows_controlled(snapshot, control)?,
8461 None => handle.lock().visible_rows(snapshot)?,
8462 };
8463 rows_cache.insert(table_id, rows.clone());
8464 Ok(rows)
8465 };
8466
8467 let mut processed_updates = HashSet::new();
8472 type PendingUpdate = (usize, u64, crate::rowid::RowId, Vec<(u16, Value)>);
8473 let mut update_pass = 0_usize;
8474 loop {
8475 commit_prepare_checkpoint(control, update_pass)?;
8476 update_pass += 1;
8477 let updates: Vec<PendingUpdate> = staging
8478 .iter()
8479 .enumerate()
8480 .filter_map(|(index, (table_id, op))| match op {
8481 Staged::Update {
8482 row_id,
8483 new_row: cells,
8484 ..
8485 } if !processed_updates.contains(&index) => {
8486 Some((index, *table_id, *row_id, cells.clone()))
8487 }
8488 _ => None,
8489 })
8490 .collect();
8491 if updates.is_empty() {
8492 break;
8493 }
8494 let mut new_ops = Vec::new();
8495 for (update_index, (index, table_id, row_id, new_cells)) in
8496 updates.into_iter().enumerate()
8497 {
8498 commit_prepare_checkpoint(control, update_index)?;
8499 processed_updates.insert(index);
8500 let Some(tname) = live
8501 .iter()
8502 .find(|(id, _, _)| *id == table_id)
8503 .map(|(_, name, _)| *name)
8504 else {
8505 continue;
8506 };
8507 let Some(old_row) = self.table_by_id(table_id)?.lock().get(row_id, snapshot) else {
8508 continue;
8509 };
8510 let new_map: HashMap<u16, Value> = new_cells.iter().cloned().collect();
8511 for (child_id, _child_name, child_schema) in &live {
8512 for fk in &child_schema.constraints.foreign_keys {
8513 if fk.ref_table != tname {
8514 continue;
8515 }
8516 let Some(old_key) = encode_composite_key(&fk.ref_columns, &old_row.columns)
8517 else {
8518 continue;
8519 };
8520 if encode_composite_key(&fk.ref_columns, &new_map).as_deref()
8521 == Some(old_key.as_slice())
8522 {
8523 continue;
8524 }
8525 if fk.on_update == FkAction::Restrict {
8526 continue;
8527 }
8528 let child_rows = load_rows(*child_id)?;
8529 for (child_index, child) in child_rows.into_iter().enumerate() {
8530 commit_prepare_checkpoint(control, child_index)?;
8531 if encode_composite_key(&fk.columns, &child.columns).as_deref()
8532 != Some(old_key.as_slice())
8533 {
8534 continue;
8535 }
8536 if staging.iter().any(|(id, op)| {
8537 *id == *child_id
8538 && matches!(op, Staged::Delete(id) if *id == child.row_id)
8539 }) {
8540 continue;
8541 }
8542 let mut cells: Vec<(u16, Value)> = child
8543 .columns
8544 .iter()
8545 .map(|(column_id, value)| (*column_id, value.clone()))
8546 .collect();
8547 for (child_column, parent_column) in
8548 fk.columns.iter().zip(&fk.ref_columns)
8549 {
8550 cells.retain(|(column_id, _)| column_id != child_column);
8551 let value = match fk.on_update {
8552 FkAction::Cascade => {
8553 new_map.get(parent_column).cloned().unwrap_or(Value::Null)
8554 }
8555 FkAction::SetNull => Value::Null,
8556 FkAction::Restrict => {
8557 return Err(MongrelError::Other(
8558 "restricted foreign-key update reached cascade preparation"
8559 .into(),
8560 ));
8561 }
8562 };
8563 cells.push((*child_column, value));
8564 }
8565 cells.sort_by_key(|(column_id, _)| *column_id);
8566 if let Some(existing_index) = staging.iter().position(|(id, op)| {
8567 *id == *child_id
8568 && matches!(op, Staged::Update { row_id, .. } if *row_id == child.row_id)
8569 }) {
8570 if let Staged::Update {
8571 new_row: existing,
8572 changed_columns,
8573 ..
8574 } = &mut staging[existing_index].1 {
8575 changed_columns.extend(fk.columns.iter().copied());
8576 changed_columns.sort_unstable();
8577 changed_columns.dedup();
8578 if *existing != cells {
8579 *existing = cells;
8580 processed_updates.remove(&existing_index);
8581 }
8582 }
8583 } else {
8584 new_ops.push((
8585 *child_id,
8586 Staged::Update {
8587 row_id: child.row_id,
8588 new_row: cells,
8589 changed_columns: fk.columns.clone(),
8590 },
8591 ));
8592 }
8593 }
8594 }
8595 }
8596 }
8597 staging.extend(new_ops);
8598 }
8599
8600 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
8605 let mut cascade_pass = 0_usize;
8606 loop {
8607 commit_prepare_checkpoint(control, cascade_pass)?;
8608 cascade_pass += 1;
8609 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
8610 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
8611 .iter()
8612 .filter_map(|(t, op)| match op {
8613 Staged::Delete(rid) => Some((*t, *rid)),
8614 _ => None,
8615 })
8616 .collect();
8617 for (delete_index, (table_id, rid)) in deletes.into_iter().enumerate() {
8618 commit_prepare_checkpoint(control, delete_index)?;
8619 if !cascaded.insert((table_id, rid.0)) {
8620 continue;
8621 }
8622 let Some(tname) = live
8623 .iter()
8624 .find(|(t, _, _)| *t == table_id)
8625 .map(|(_, n, _)| *n)
8626 else {
8627 continue;
8628 };
8629 let parent_handle = self.table_by_id(table_id)?;
8630 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
8631 continue;
8632 };
8633 for (child_id, _child_name, child_schema) in &live {
8634 for fk in &child_schema.constraints.foreign_keys {
8635 if fk.ref_table != tname {
8636 continue;
8637 }
8638 let Some(parent_key) =
8639 encode_composite_key(&fk.ref_columns, &parent_row.columns)
8640 else {
8641 continue;
8642 };
8643 let key_preserved = staging.iter().any(|(t, op)| {
8652 if *t != table_id {
8653 return false;
8654 }
8655 let Staged::Put(cells) = op else {
8656 return false;
8657 };
8658 let map: HashMap<u16, crate::memtable::Value> =
8659 cells.iter().cloned().collect();
8660 encode_composite_key(&fk.ref_columns, &map).as_deref()
8661 == Some(parent_key.as_slice())
8662 });
8663 if key_preserved {
8664 continue;
8665 }
8666 match fk.on_delete {
8667 FkAction::Restrict => continue,
8668 FkAction::Cascade => {
8669 let child_rows = load_rows(*child_id)?;
8670 for (child_index, cr) in child_rows.iter().enumerate() {
8671 commit_prepare_checkpoint(control, child_index)?;
8672 if !cascaded.contains(&(*child_id, cr.row_id.0))
8673 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
8674 == Some(parent_key.as_slice())
8675 {
8676 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
8677 }
8678 }
8679 }
8680 FkAction::SetNull => {
8681 let child_rows = load_rows(*child_id)?;
8682 for (child_index, cr) in child_rows.iter().enumerate() {
8683 commit_prepare_checkpoint(control, child_index)?;
8684 if !cascaded.contains(&(*child_id, cr.row_id.0))
8685 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
8686 == Some(parent_key.as_slice())
8687 {
8688 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
8691 .columns
8692 .iter()
8693 .map(|(k, v)| (*k, v.clone()))
8694 .collect();
8695 for cid in &fk.columns {
8696 cells.retain(|(k, _)| k != cid);
8697 cells.push((*cid, crate::memtable::Value::Null));
8698 }
8699 new_ops.push((
8700 *child_id,
8701 Staged::Update {
8702 row_id: cr.row_id,
8703 new_row: cells,
8704 changed_columns: fk.columns.clone(),
8705 },
8706 ));
8707 }
8708 }
8709 }
8710 }
8711 }
8712 }
8713 }
8714 if new_ops.is_empty() {
8715 break;
8716 }
8717 staging.extend(new_ops);
8718 }
8719
8720 let staged_deletes: HashSet<(u64, u64)> = staging
8724 .iter()
8725 .filter_map(|(t, op)| match op {
8726 Staged::Delete(rid) | Staged::Update { row_id: rid, .. } => Some((*t, rid.0)),
8727 _ => None,
8728 })
8729 .collect();
8730
8731 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
8733
8734 for (operation_index, (table_id, op)) in staging.iter().enumerate() {
8736 commit_prepare_checkpoint(control, operation_index)?;
8737 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id).copied()
8738 else {
8739 continue;
8740 };
8741 let cells_map: HashMap<u16, crate::memtable::Value>;
8742 match op {
8743 Staged::Put(cells) | Staged::Update { new_row: cells, .. } => {
8744 cells_map = cells.iter().cloned().collect();
8745
8746 if !schema.constraints.checks.is_empty() {
8748 validate_checks(&schema.constraints.checks, &cells_map)?;
8749 }
8750
8751 for uc in &schema.constraints.uniques {
8753 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
8754 continue; };
8756 let marker = (*table_id, uc.id, key.clone());
8757 if !seen_unique.insert(marker) {
8758 return Err(MongrelError::Conflict(format!(
8759 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
8760 uc.name
8761 )));
8762 }
8763 let rows = load_rows(*table_id)?;
8764 for (row_index, r) in rows.iter().enumerate() {
8765 commit_prepare_checkpoint(control, row_index)?;
8766 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
8769 continue;
8770 }
8771 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
8772 if theirs == key {
8773 return Err(MongrelError::Conflict(format!(
8774 "UNIQUE constraint '{}' on table '{tname}' violated",
8775 uc.name
8776 )));
8777 }
8778 }
8779 }
8780 }
8781
8782 for fk in &schema.constraints.foreign_keys {
8784 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
8785 continue; };
8787 let Some(parent_id) = cat
8788 .tables
8789 .iter()
8790 .find(|t| t.name == fk.ref_table)
8791 .map(|t| t.table_id)
8792 else {
8793 return Err(MongrelError::InvalidArgument(format!(
8794 "FOREIGN KEY '{}' references unknown table '{}'",
8795 fk.name, fk.ref_table
8796 )));
8797 };
8798 let parent_rows = load_rows(parent_id)?;
8799 let mut found = false;
8800 for (row_index, r) in parent_rows.iter().enumerate() {
8801 commit_prepare_checkpoint(control, row_index)?;
8802 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
8803 continue;
8804 }
8805 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
8806 if pkey == child_key {
8807 found = true;
8808 break;
8809 }
8810 }
8811 }
8812 if !found {
8819 for (staged_index, (st_table, st_op)) in staging.iter().enumerate() {
8820 commit_prepare_checkpoint(control, staged_index)?;
8821 if *st_table != parent_id {
8822 continue;
8823 }
8824 if let Staged::Put(pcells)
8825 | Staged::Update {
8826 new_row: pcells, ..
8827 } = st_op
8828 {
8829 let pmap: HashMap<u16, crate::memtable::Value> =
8830 pcells.iter().cloned().collect();
8831 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &pmap)
8832 {
8833 if pkey == child_key {
8834 found = true;
8835 break;
8836 }
8837 }
8838 }
8839 }
8840 }
8841 if !found {
8842 return Err(MongrelError::Conflict(format!(
8843 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
8844 fk.name, fk.ref_table
8845 )));
8846 }
8847 }
8848
8849 if let Staged::Update { row_id, .. } = op {
8854 let parent_handle = self.table_by_id(*table_id)?;
8855 let Some(old_parent) = parent_handle.lock().get(*row_id, snapshot) else {
8856 continue;
8857 };
8858 for (child_id, child_name, child_schema) in &live {
8859 for fk in &child_schema.constraints.foreign_keys {
8860 if fk.ref_table != tname || fk.on_update != FkAction::Restrict {
8861 continue;
8862 }
8863 let Some(old_key) =
8864 encode_composite_key(&fk.ref_columns, &old_parent.columns)
8865 else {
8866 continue;
8867 };
8868 if encode_composite_key(&fk.ref_columns, &cells_map).as_deref()
8869 == Some(old_key.as_slice())
8870 {
8871 continue;
8872 }
8873 for (child_index, child) in
8874 load_rows(*child_id)?.into_iter().enumerate()
8875 {
8876 commit_prepare_checkpoint(control, child_index)?;
8877 if encode_composite_key(&fk.columns, &child.columns).as_deref()
8878 != Some(old_key.as_slice())
8879 {
8880 continue;
8881 }
8882 let replacement = staging.iter().find_map(|(id, op)| {
8883 if *id != *child_id {
8884 return None;
8885 }
8886 match op {
8887 Staged::Delete(id) if *id == child.row_id => Some(None),
8888 Staged::Update {
8889 row_id,
8890 new_row: cells,
8891 ..
8892 } if *row_id == child.row_id => {
8893 let map: HashMap<u16, Value> =
8894 cells.iter().cloned().collect();
8895 Some(encode_composite_key(&fk.columns, &map))
8896 }
8897 _ => None,
8898 }
8899 });
8900 if replacement.is_some_and(|key| {
8901 key.as_deref() != Some(old_key.as_slice())
8902 }) {
8903 continue;
8904 }
8905 return Err(MongrelError::Conflict(format!(
8906 "FOREIGN KEY '{}' on table '{child_name}' restricts update (parent key referenced)",
8907 fk.name
8908 )));
8909 }
8910 }
8911 }
8912 }
8913 }
8914 Staged::Delete(rid) => {
8915 let parent_handle = self.table_by_id(*table_id)?;
8919 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
8920 continue;
8921 };
8922 for (child_id, child_name, child_schema) in &live {
8923 for fk in &child_schema.constraints.foreign_keys {
8924 if fk.ref_table != tname || fk.on_delete != FkAction::Restrict {
8925 continue;
8926 }
8927 let Some(parent_key) =
8928 encode_composite_key(&fk.ref_columns, &parent_row.columns)
8929 else {
8930 continue;
8931 };
8932 let child_rows = load_rows(*child_id)?;
8933 for (row_index, r) in child_rows.iter().enumerate() {
8934 commit_prepare_checkpoint(control, row_index)?;
8935 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
8938 continue;
8939 }
8940 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
8941 if ck == parent_key {
8942 return Err(MongrelError::Conflict(format!(
8943 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
8944 fk.name
8945 )));
8946 }
8947 }
8948 }
8949 }
8950 }
8951 }
8952 Staged::Truncate => {
8953 for (child_id, child_name, child_schema) in &live {
8957 for fk in &child_schema.constraints.foreign_keys {
8958 if fk.ref_table != tname {
8959 continue;
8960 }
8961 let child_rows = load_rows(*child_id)?;
8962 if child_rows
8963 .iter()
8964 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
8965 {
8966 return Err(MongrelError::Conflict(format!(
8967 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
8968 fk.name
8969 )));
8970 }
8971 }
8972 }
8973 }
8974 }
8975 }
8976 Ok(())
8977 }
8978
8979 fn validate_write_permissions(
8980 &self,
8981 staging: &[(u64, crate::txn::Staged)],
8982 principal: Option<&crate::auth::Principal>,
8983 control: Option<&crate::ExecutionControl>,
8984 ) -> Result<()> {
8985 commit_prepare_checkpoint(control, 0)?;
8986 if principal.is_none() && !self.auth_state.require_auth() {
8987 return Ok(());
8988 }
8989 let principal = principal.ok_or(MongrelError::AuthRequired)?;
8990 let needs = summarize_write_permissions(staging);
8991 let catalog = self.catalog.read();
8992
8993 if needs.values().any(|need| need.truncate) {
8994 self.require_for(Some(principal), &crate::auth::Permission::Admin)?;
8995 }
8996 for (need_index, (table_id, need)) in needs.into_iter().enumerate() {
8997 commit_prepare_checkpoint(control, need_index)?;
8998 let entry = catalog
8999 .tables
9000 .iter()
9001 .find(|entry| {
9002 entry.table_id == table_id
9003 && matches!(entry.state, TableState::Live | TableState::Building { .. })
9004 })
9005 .ok_or_else(|| {
9006 MongrelError::NotFound(format!(
9007 "live table {table_id} not found during write validation"
9008 ))
9009 })?;
9010 if matches!(entry.state, TableState::Building { .. }) {
9011 self.require_for(Some(principal), &crate::auth::Permission::Ddl)?;
9012 continue;
9013 }
9014 if need.insert {
9015 Self::require_columns_for_principal(
9016 &entry.name,
9017 &entry.schema,
9018 crate::auth::ColumnOperation::Insert,
9019 &need.insert_columns,
9020 principal,
9021 )?;
9022 }
9023 if need.update {
9024 Self::require_columns_for_principal(
9025 &entry.name,
9026 &entry.schema,
9027 crate::auth::ColumnOperation::Update,
9028 &need.update_columns,
9029 principal,
9030 )?;
9031 }
9032 if need.delete {
9033 self.require_for(
9034 Some(principal),
9035 &crate::auth::Permission::Delete {
9036 table: entry.name.clone(),
9037 },
9038 )?;
9039 }
9040 }
9041 Ok(())
9042 }
9043
9044 fn validate_security_writes(
9045 &self,
9046 staging: &[(u64, crate::txn::Staged)],
9047 read_epoch: Epoch,
9048 explicit_principal: Option<&crate::auth::Principal>,
9049 control: Option<&crate::ExecutionControl>,
9050 ) -> Result<()> {
9051 commit_prepare_checkpoint(control, 0)?;
9052 use crate::security::PolicyCommand;
9053 use crate::txn::Staged;
9054
9055 let catalog = self.catalog.read();
9056 if catalog.security.rls_tables.is_empty() {
9057 return Ok(());
9058 }
9059 let security = catalog.security.clone();
9060 let table_names = catalog
9061 .tables
9062 .iter()
9063 .filter(|entry| matches!(entry.state, TableState::Live))
9064 .map(|entry| (entry.table_id, entry.name.clone()))
9065 .collect::<HashMap<_, _>>();
9066 drop(catalog);
9067 if !staging.iter().any(|(table_id, _)| {
9068 table_names
9069 .get(table_id)
9070 .is_some_and(|table| security.rls_enabled(table))
9071 }) {
9072 return Ok(());
9073 }
9074 let principal = explicit_principal.ok_or(MongrelError::AuthRequired)?;
9075
9076 for (operation_index, (table_id, operation)) in staging.iter().enumerate() {
9077 commit_prepare_checkpoint(control, operation_index)?;
9078 let Some(table) = table_names.get(table_id) else {
9079 continue;
9080 };
9081 if !security.rls_enabled(table) || principal.is_admin {
9082 continue;
9083 }
9084 let denied = |command| MongrelError::PermissionDenied {
9085 required: match command {
9086 PolicyCommand::Insert => crate::auth::Permission::Insert {
9087 table: table.clone(),
9088 },
9089 PolicyCommand::Update => crate::auth::Permission::Update {
9090 table: table.clone(),
9091 },
9092 PolicyCommand::Delete | PolicyCommand::All | PolicyCommand::Select => {
9093 crate::auth::Permission::Delete {
9094 table: table.clone(),
9095 }
9096 }
9097 },
9098 principal: principal.username.clone(),
9099 };
9100 match operation {
9101 Staged::Put(cells) => {
9102 let mut row = crate::memtable::Row::new(RowId(0), Epoch(read_epoch.0));
9103 row.columns.extend(cells.iter().cloned());
9104 if !security.row_allowed(table, PolicyCommand::Insert, &row, principal, true) {
9105 return Err(denied(PolicyCommand::Insert));
9106 }
9107 }
9108 Staged::Update {
9109 row_id,
9110 new_row: cells,
9111 ..
9112 } => {
9113 let old = self
9114 .table_by_id(*table_id)?
9115 .lock()
9116 .get(*row_id, Snapshot::at(read_epoch))
9117 .ok_or_else(|| {
9118 MongrelError::NotFound(format!("row {} not found", row_id.0))
9119 })?;
9120 if !security.row_allowed(table, PolicyCommand::Update, &old, principal, false) {
9121 return Err(denied(PolicyCommand::Update));
9122 }
9123 let mut new = crate::memtable::Row::new(*row_id, Epoch(read_epoch.0));
9124 new.columns.extend(cells.iter().cloned());
9125 if !security.row_allowed(table, PolicyCommand::Update, &new, principal, true) {
9126 return Err(denied(PolicyCommand::Update));
9127 }
9128 }
9129 Staged::Delete(row_id) => {
9130 let old = self
9131 .table_by_id(*table_id)?
9132 .lock()
9133 .get(*row_id, Snapshot::at(read_epoch))
9134 .ok_or_else(|| {
9135 MongrelError::NotFound(format!("row {} not found", row_id.0))
9136 })?;
9137 if !security.row_allowed(table, PolicyCommand::Delete, &old, principal, false) {
9138 return Err(denied(PolicyCommand::Delete));
9139 }
9140 }
9141 Staged::Truncate => return Err(denied(PolicyCommand::Delete)),
9142 }
9143 }
9144 Ok(())
9145 }
9146
9147 #[allow(clippy::too_many_arguments)]
9154 pub(crate) fn commit_transaction_with_external_states(
9155 &self,
9156 txn_id: u64,
9157 read_epoch: Epoch,
9158 staging: Vec<(u64, crate::txn::Staged)>,
9159 external_states: Vec<(String, Vec<u8>)>,
9160 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
9161 security_principal: Option<crate::auth::Principal>,
9162 principal_catalog_bound: bool,
9163 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9164 ) -> Result<(Epoch, Vec<RowId>)> {
9165 self.commit_transaction_with_external_states_inner(
9166 txn_id,
9167 read_epoch,
9168 staging,
9169 external_states,
9170 materialized_view_updates,
9171 security_principal,
9172 principal_catalog_bound,
9173 external_trigger_bridge,
9174 None,
9175 None,
9176 )
9177 }
9178
9179 #[allow(clippy::too_many_arguments)]
9180 pub(crate) fn commit_transaction_with_external_states_controlled(
9181 &self,
9182 txn_id: u64,
9183 read_epoch: Epoch,
9184 staging: Vec<(u64, crate::txn::Staged)>,
9185 external_states: Vec<(String, Vec<u8>)>,
9186 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
9187 security_principal: Option<crate::auth::Principal>,
9188 principal_catalog_bound: bool,
9189 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9190 control: &crate::ExecutionControl,
9191 before_commit: &mut dyn FnMut() -> Result<()>,
9192 ) -> Result<(Epoch, Vec<RowId>)> {
9193 self.commit_transaction_with_external_states_inner(
9194 txn_id,
9195 read_epoch,
9196 staging,
9197 external_states,
9198 materialized_view_updates,
9199 security_principal,
9200 principal_catalog_bound,
9201 external_trigger_bridge,
9202 Some(control),
9203 Some(before_commit),
9204 )
9205 }
9206
9207 #[allow(clippy::too_many_arguments)]
9208 fn commit_transaction_with_external_states_inner(
9209 &self,
9210 txn_id: u64,
9211 read_epoch: Epoch,
9212 mut staging: Vec<(u64, crate::txn::Staged)>,
9213 external_states: Vec<(String, Vec<u8>)>,
9214 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
9215 mut security_principal: Option<crate::auth::Principal>,
9216 principal_catalog_bound: bool,
9217 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9218 control: Option<&crate::ExecutionControl>,
9219 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
9220 ) -> Result<(Epoch, Vec<RowId>)> {
9221 use crate::memtable::Row;
9222 use crate::txn::{Staged, StagedOp, WriteKey};
9223 use crate::wal::Op;
9224 use std::collections::hash_map::DefaultHasher;
9225 use std::hash::{Hash, Hasher};
9226 use std::sync::atomic::Ordering;
9227
9228 if txn_id == crate::wal::SYSTEM_TXN_ID {
9229 return Err(MongrelError::Full(
9230 "per-open transaction id namespace exhausted; reopen the database".into(),
9231 ));
9232 }
9233 if self.read_only {
9234 return Err(MongrelError::ReadOnlyReplica);
9235 }
9236 commit_prepare_checkpoint(control, 0)?;
9237 let observed_security_version = self.security_coordinator.version.load(Ordering::Acquire);
9238 self.refresh_security_catalog_if_stale(observed_security_version)?;
9239 let trigger_binding = trigger_catalog_binding(&self.catalog.read());
9240 if self.auth_state.require_auth() && security_principal.is_none() {
9241 return Err(MongrelError::AuthRequired);
9242 }
9243 {
9244 let catalog = self.catalog.read();
9245 if catalog.require_auth
9246 || principal_catalog_bound
9247 || security_principal
9248 .as_ref()
9249 .is_some_and(|principal| principal.user_id != 0)
9250 {
9251 let principal = security_principal
9252 .as_ref()
9253 .ok_or(MongrelError::AuthRequired)?;
9254 security_principal =
9255 Self::resolve_bound_principal_from_catalog(&catalog, principal);
9256 if security_principal.is_none() {
9257 return Err(MongrelError::AuthRequired);
9258 }
9259 }
9260 }
9261 let _replication_guard = self.replication_barrier.read();
9262 if self.poisoned.load(Ordering::Relaxed) {
9263 return Err(MongrelError::Other(
9264 "database poisoned by fsync error".into(),
9265 ));
9266 }
9267 let mut external_states = dedup_external_states(external_states);
9268 if !external_states.is_empty() {
9269 let cat = self.catalog.read();
9270 for (name, _) in &external_states {
9271 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
9272 return Err(MongrelError::NotFound(format!(
9273 "external table {name:?} not found"
9274 )));
9275 }
9276 }
9277 }
9278 let prepared_materialized_views = {
9279 let mut deduplicated = HashMap::new();
9280 for (definition_index, definition) in materialized_view_updates.into_iter().enumerate()
9281 {
9282 commit_prepare_checkpoint(control, definition_index)?;
9283 if definition.name.is_empty() || definition.query.trim().is_empty() {
9284 return Err(MongrelError::InvalidArgument(
9285 "materialized view name and query must not be empty".into(),
9286 ));
9287 }
9288 deduplicated.insert(definition.name.clone(), definition);
9289 }
9290 let catalog = self.catalog.read();
9291 let mut prepared = Vec::with_capacity(deduplicated.len());
9292 for (definition_index, definition) in deduplicated.into_values().enumerate() {
9293 commit_prepare_checkpoint(control, definition_index)?;
9294 let table_id = catalog
9295 .live(&definition.name)
9296 .ok_or_else(|| {
9297 MongrelError::NotFound(format!(
9298 "materialized view table {:?} not found",
9299 definition.name
9300 ))
9301 })?
9302 .table_id;
9303 prepared.push((table_id, definition));
9304 }
9305 prepared.sort_by(|left, right| left.1.name.cmp(&right.1.name));
9306 prepared
9307 };
9308
9309 self.fill_auto_increment_for_staging(&mut staging, control)?;
9312 self.expand_table_triggers(
9313 &mut staging,
9314 read_epoch,
9315 external_trigger_bridge,
9316 &mut external_states,
9317 control,
9318 )?;
9319 self.fill_auto_increment_for_staging(&mut staging, control)?;
9320 external_states = dedup_external_states(external_states);
9321 let expected_external_generations = {
9322 let catalog = self.catalog.read();
9323 let mut generations = HashMap::with_capacity(external_states.len());
9324 for (name, _) in &external_states {
9325 let entry = catalog
9326 .external_tables
9327 .iter()
9328 .find(|entry| entry.name == *name)
9329 .ok_or_else(|| {
9330 MongrelError::NotFound(format!("external table {name:?} not found"))
9331 })?;
9332 generations.insert(name.clone(), entry.created_epoch);
9333 }
9334 generations
9335 };
9336
9337 self.validate_constraints(&mut staging, read_epoch, control)?;
9342 self.validate_write_permissions(&staging, security_principal.as_ref(), control)?;
9343 self.validate_security_writes(&staging, read_epoch, security_principal.as_ref(), control)?;
9344 let mut normalized = Vec::with_capacity(staging.len() * 2);
9345 for (staged_index, (table_id, op)) in staging.into_iter().enumerate() {
9346 commit_prepare_checkpoint(control, staged_index)?;
9347 match op {
9348 crate::txn::Staged::Update {
9349 row_id,
9350 new_row: cells,
9351 ..
9352 } => {
9353 normalized.push((table_id, crate::txn::Staged::Delete(row_id)));
9354 normalized.push((table_id, crate::txn::Staged::Put(cells)));
9355 }
9356 op => normalized.push((table_id, op)),
9357 }
9358 }
9359 staging = normalized;
9360 let has_changes = !staging.is_empty()
9361 || !external_states.is_empty()
9362 || !prepared_materialized_views.is_empty();
9363 let truncated_tables: HashSet<u64> = staging
9364 .iter()
9365 .filter_map(|(table_id, op)| matches!(op, Staged::Truncate).then_some(*table_id))
9366 .collect();
9367
9368 let write_keys = {
9369 let cat = self.catalog.read();
9370 let mut keys: Vec<WriteKey> = Vec::new();
9371 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
9372 commit_prepare_checkpoint(control, staged_index)?;
9373 match staged {
9374 Staged::Put(cells) => {
9375 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
9376 for col in &entry.schema.columns {
9377 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
9378 if let Some((_, val)) =
9379 cells.iter().find(|(id, _)| *id == col.id)
9380 {
9381 let mut h = DefaultHasher::new();
9382 val.encode_key().hash(&mut h);
9383 keys.push(WriteKey::Unique {
9384 table_id: *table_id,
9385 index_id: 0,
9386 key_hash: h.finish(),
9387 });
9388 }
9389 }
9390 }
9391 for uc in &entry.schema.constraints.uniques {
9398 if let Some(key_bytes) = crate::constraint::encode_composite_key(
9399 &uc.columns,
9400 &cells.iter().cloned().collect(),
9401 ) {
9402 let mut h = DefaultHasher::new();
9403 key_bytes.hash(&mut h);
9404 keys.push(WriteKey::Unique {
9405 table_id: *table_id,
9406 index_id: uc.id | 0x8000,
9407 key_hash: h.finish(),
9408 });
9409 }
9410 }
9411 }
9412 }
9413 Staged::Delete(rid) => keys.push(WriteKey::Row {
9414 table_id: *table_id,
9415 row_id: rid.0,
9416 }),
9417 Staged::Truncate => keys.push(WriteKey::Table {
9418 table_id: *table_id,
9419 }),
9420 Staged::Update { .. } => {
9421 return Err(MongrelError::Other(
9422 "transaction contains an unnormalized update during preparation".into(),
9423 ));
9424 }
9425 }
9426 }
9427 for (external_index, (name, _)) in external_states.iter().enumerate() {
9428 commit_prepare_checkpoint(control, external_index)?;
9429 let mut h = DefaultHasher::new();
9430 name.hash(&mut h);
9431 keys.push(WriteKey::Unique {
9432 table_id: EXTERNAL_TABLE_ID,
9433 index_id: 0,
9434 key_hash: h.finish(),
9435 });
9436 }
9437 keys
9438 };
9439
9440 let min_active = self.active_txns.min_read_epoch();
9442 if min_active < u64::MAX {
9443 self.conflicts.prune_below(Epoch(min_active));
9444 }
9445
9446 if self.conflicts.conflicts(&write_keys, read_epoch) {
9450 return Err(MongrelError::Conflict(
9451 "write-write conflict (pre-validate, first-committer-wins)".into(),
9452 ));
9453 }
9454 let pre_validate_version = self.conflicts.version();
9455
9456 let mut spilled: Vec<SpilledRun> = Vec::new();
9460 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
9461 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
9465 {
9466 let mut table_bytes: HashMap<u64, u64> = HashMap::new();
9467 let mut put_indexes: HashMap<u64, Vec<usize>> = HashMap::new();
9468 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
9469 commit_prepare_checkpoint(control, staged_index)?;
9470 if let Staged::Put(cells) = staged {
9471 let bytes = cells.iter().fold(32_u64, |bytes, (_, value)| {
9472 bytes.saturating_add(value.estimated_bytes())
9473 });
9474 let table_bytes = table_bytes.entry(*table_id).or_default();
9475 *table_bytes = table_bytes.saturating_add(bytes);
9476 put_indexes.entry(*table_id).or_default().push(staged_index);
9477 }
9478 }
9479 let tables = self.tables.read();
9480 for (table_index, (&table_id, &bytes)) in table_bytes.iter().enumerate() {
9481 commit_prepare_checkpoint(control, table_index)?;
9482 if bytes
9483 <= self
9484 .spill_threshold
9485 .load(std::sync::atomic::Ordering::Relaxed)
9486 {
9487 continue;
9488 }
9489 let Some(handle) = tables.get(&table_id) else {
9490 continue;
9491 };
9492 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
9493 let mut t = handle.lock();
9494 let tdir = t.table_dir().to_path_buf();
9495 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
9496 std::fs::create_dir_all(&txn_dir)?;
9497 let run_id = t.alloc_run_id()? as u128;
9498 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
9499 let final_path = t.run_path(run_id as u64);
9500
9501 let mut rows: Vec<Row> = Vec::new();
9502 for (put_index, staged_index) in put_indexes[&table_id].iter().enumerate() {
9503 commit_prepare_checkpoint(control, put_index)?;
9504 let Staged::Put(cells) = &mut staging[*staged_index].1 else {
9505 return Err(MongrelError::Other(
9506 "transaction put index no longer references a put".into(),
9507 ));
9508 };
9509 t.validate_cells_not_null(cells)?;
9510 let row_id = t.alloc_row_id()?;
9511 let mut row = Row::new(row_id, Epoch(0));
9512 row.columns.extend(std::mem::take(cells));
9513 rows.push(row);
9514 }
9515 let schema = t.schema_ref().clone();
9516 let kek = t.kek_ref().cloned();
9517 let specs = t.indexable_column_specs();
9518 drop(t);
9519
9520 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
9521 .uniform_epoch(true);
9522 if let Some(ref kek) = kek {
9523 writer = writer.with_encryption(kek.as_ref(), specs);
9524 }
9525 commit_prepare_checkpoint(control, 0)?;
9526 let header = writer.write(&pending_path, &rows)?;
9527 commit_prepare_checkpoint(control, 0)?;
9528 let row_count = header.row_count;
9529 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
9530 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
9531
9532 spilled.push(SpilledRun {
9533 table_id,
9534 run_id,
9535 pending_path,
9536 final_path,
9537 rows,
9538 row_count,
9539 min_rid,
9540 max_rid,
9541 content_hash: header.content_hash,
9542 });
9543 spilled_tables.insert(table_id);
9544 }
9545 }
9546
9547 if spill_guard.is_some() {
9549 if let Some(hook) = self.spill_hook.lock().as_ref() {
9550 hook();
9551 }
9552 }
9553
9554 let mut prebuilt: Vec<Option<Row>> = std::iter::repeat_with(|| None)
9565 .take(staging.len())
9566 .collect();
9567 let mut delete_images: Vec<Option<Row>> = std::iter::repeat_with(|| None)
9568 .take(staging.len())
9569 .collect();
9570 {
9571 let mut indexes_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
9572 for (index, (table_id, staged)) in staging.iter().enumerate() {
9573 commit_prepare_checkpoint(control, index)?;
9574 if matches!(staged, Staged::Delete(_))
9575 || matches!(staged, Staged::Put(_) if !spilled_tables.contains(table_id))
9576 {
9577 indexes_by_table.entry(*table_id).or_default().push(index);
9578 }
9579 }
9580 let tables = self.tables.read();
9581 for (table_index, (table_id, indexes)) in indexes_by_table.into_iter().enumerate() {
9582 commit_prepare_checkpoint(control, table_index)?;
9583 let handle = tables.get(&table_id).ok_or_else(|| {
9584 MongrelError::NotFound(format!("table {table_id} not mounted"))
9585 })?;
9586 #[cfg(test)]
9587 PREBUILD_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
9588 let mut t = handle.lock();
9589 for (prepare_index, index) in indexes.into_iter().enumerate() {
9590 commit_prepare_checkpoint(control, prepare_index)?;
9591 match &staging[index].1 {
9592 Staged::Put(cells) if !spilled_tables.contains(&table_id) => {
9593 t.validate_cells_not_null(cells)?;
9594 let mut row = Row::new(t.alloc_row_id()?, Epoch(0));
9595 for (column, value) in cells {
9596 row.columns.insert(*column, value.clone());
9597 }
9598 prebuilt[index] = Some(row);
9599 }
9600 Staged::Delete(row_id) => {
9601 delete_images[index] = t.get(*row_id, Snapshot::at(read_epoch));
9602 }
9603 Staged::Put(_) | Staged::Truncate => {}
9604 Staged::Update { .. } => {
9605 return Err(MongrelError::Other(
9606 "transaction contains an unnormalized update during row preparation"
9607 .into(),
9608 ));
9609 }
9610 }
9611 }
9612 }
9613 }
9614
9615 let prepared_table_handles = {
9619 let table_ids: HashSet<u64> = staging.iter().map(|(table_id, _)| *table_id).collect();
9620 let put_table_ids: HashSet<u64> = staging
9621 .iter()
9622 .filter_map(|(table_id, staged)| {
9623 matches!(staged, Staged::Put(_)).then_some(*table_id)
9624 })
9625 .collect();
9626 let tables = self.tables.read();
9627 let mut handles = HashMap::with_capacity(table_ids.len());
9628 for (table_index, table_id) in table_ids.into_iter().enumerate() {
9629 commit_prepare_checkpoint(control, table_index)?;
9630 let handle = tables.get(&table_id).ok_or_else(|| {
9631 MongrelError::NotFound(format!("table {table_id} not mounted"))
9632 })?;
9633 if put_table_ids.contains(&table_id) {
9634 match control {
9635 Some(control) => {
9636 handle.lock().prepare_durable_publish_controlled(control)?
9637 }
9638 None => handle.lock().prepare_durable_publish()?,
9639 }
9640 }
9641 handles.insert(table_id, handle.clone());
9642 }
9643 handles
9644 };
9645
9646 let mut prepared_run_links = PreparedRunLinks::prepare(&spilled)?;
9650
9651 let mut spilled_row_ids: HashMap<u64, VecDeque<RowId>> = spilled
9652 .iter()
9653 .map(|run| {
9654 (
9655 run.table_id,
9656 run.rows.iter().map(|row| row.row_id).collect(),
9657 )
9658 })
9659 .collect();
9660 let committed_row_ids = staging
9661 .iter()
9662 .enumerate()
9663 .filter_map(|(index, (table_id, staged))| {
9664 if !matches!(staged, Staged::Put(_)) {
9665 return None;
9666 }
9667 prebuilt[index].as_ref().map(|row| row.row_id).or_else(|| {
9668 spilled_row_ids
9669 .get_mut(table_id)
9670 .and_then(VecDeque::pop_front)
9671 })
9672 })
9673 .collect();
9674
9675 let mut prepared_external = Vec::with_capacity(external_states.len());
9676 for (external_index, (name, state)) in external_states.iter().enumerate() {
9677 commit_prepare_checkpoint(control, external_index)?;
9678 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
9679 prepared_external.push((name.clone(), state.clone(), pending));
9680 }
9681
9682 let added_runs: Vec<crate::wal::AddedRun> = spilled
9684 .iter()
9685 .map(|s| crate::wal::AddedRun {
9686 table_id: s.table_id,
9687 run_id: s.run_id,
9688 row_count: s.row_count,
9689 level: 0,
9690 min_row_id: s.min_rid,
9691 max_row_id: s.max_rid,
9692 content_hash: s.content_hash,
9693 })
9694 .collect();
9695 if let Some(hook) = self.catalog_commit_hook.lock().as_ref() {
9696 hook();
9697 }
9698 let security_guard = self.security_coordinator.gate.read();
9702 if self.security_coordinator.version.load(Ordering::Acquire) != observed_security_version {
9703 return Err(MongrelError::Conflict(
9704 "security policy changed during write".into(),
9705 ));
9706 }
9707 if spill_guard.is_some() {
9708 if let Some(hook) = self.security_commit_hook.lock().as_ref() {
9709 hook();
9710 }
9711 }
9712 let commit_guard = self.commit_lock.lock();
9713 let catalog_generation_result = (|| {
9714 {
9715 let catalog = self.catalog.read();
9716 for table_id in prepared_table_handles.keys() {
9717 let is_current = catalog.tables.iter().any(|entry| {
9718 entry.table_id == *table_id
9719 && matches!(entry.state, TableState::Live | TableState::Building { .. })
9720 });
9721 if !is_current {
9722 return Err(MongrelError::Conflict(format!(
9723 "table {table_id} changed during transaction preparation"
9724 )));
9725 }
9726 }
9727 for (name, created_epoch) in &expected_external_generations {
9728 let current = catalog
9729 .external_tables
9730 .iter()
9731 .find(|entry| entry.name == *name)
9732 .map(|entry| entry.created_epoch);
9733 if current != Some(*created_epoch) {
9734 return Err(MongrelError::Conflict(format!(
9735 "external table {name:?} changed during transaction preparation"
9736 )));
9737 }
9738 }
9739 for (table_id, definition) in &prepared_materialized_views {
9740 let current = catalog.live(&definition.name).map(|entry| entry.table_id);
9741 if current != Some(*table_id) {
9742 return Err(MongrelError::Conflict(format!(
9743 "materialized view {:?} changed during transaction preparation",
9744 definition.name
9745 )));
9746 }
9747 }
9748 if trigger_catalog_binding(&catalog) != trigger_binding {
9749 return Err(MongrelError::Conflict(
9750 "trigger or referenced table generation changed during transaction preparation"
9751 .into(),
9752 ));
9753 }
9754 }
9755 let tables = self.tables.read();
9756 for (table_id, prepared) in &prepared_table_handles {
9757 if !tables
9758 .get(table_id)
9759 .is_some_and(|current| current.ptr_eq(prepared))
9760 {
9761 return Err(MongrelError::Conflict(format!(
9762 "table {table_id} mount changed during transaction preparation"
9763 )));
9764 }
9765 }
9766 Ok(())
9767 })();
9768 if let Err(error) = catalog_generation_result {
9769 drop(commit_guard);
9770 for (_, _, pending) in &prepared_external {
9771 let _ = std::fs::remove_file(pending);
9772 }
9773 return Err(error);
9774 }
9775 let new_epoch = self.epoch.assigned().next();
9779 let mut spilled_wal_bytes = 0;
9780 let mut spilled_wal_records = Vec::<(u64, Op)>::new();
9781 let spill_prepare = (|| {
9782 for run in &mut spilled {
9783 for row in &mut run.rows {
9784 row.committed_epoch = new_epoch;
9785 }
9786 for rows in encode_spilled_row_chunks(
9787 &run.rows,
9788 &mut spilled_wal_bytes,
9789 SPILLED_WAL_TOTAL_MAX_BYTES,
9790 control,
9791 )? {
9792 spilled_wal_records.push((
9793 run.table_id,
9794 Op::SpilledRows {
9795 table_id: run.table_id,
9796 rows,
9797 },
9798 ));
9799 }
9800 }
9801 Result::<()>::Ok(())
9802 })();
9803 if let Err(error) = spill_prepare {
9804 for (_, _, pending) in &prepared_external {
9805 let _ = std::fs::remove_file(pending);
9806 }
9807 return Err(error);
9808 }
9809 let (new_epoch, mut _epoch_guard, applies, committed_materialized_views, commit_seq) = {
9810 let mut wal = self.shared_wal.lock();
9811
9812 if self.conflicts.version() != pre_validate_version
9817 && self.conflicts.conflicts(&write_keys, read_epoch)
9818 {
9819 drop(wal);
9822 for (_, _, pending) in &prepared_external {
9823 let _ = std::fs::remove_file(pending);
9824 }
9825 return Err(MongrelError::Conflict(
9826 "write-write conflict (sequencer delta re-check)".into(),
9827 ));
9828 }
9829
9830 if let Some(control) = control {
9831 if let Err(error) = control.checkpoint() {
9832 drop(wal);
9833 for (_, _, pending) in &prepared_external {
9834 let _ = std::fs::remove_file(pending);
9835 }
9836 return Err(error);
9837 }
9838 }
9839 let mut applies = Vec::<TableApplyBatch>::new();
9840 let mut apply_indexes = HashMap::<u64, usize>::new();
9841 let mut committed_materialized_views = Vec::new();
9842 let mut wal_records = spilled_wal_records;
9843
9844 let mut index = 0;
9845 while index < staging.len() {
9846 let table_id = staging[index].0;
9847 let handle = prepared_table_handles
9848 .get(&table_id)
9849 .cloned()
9850 .ok_or_else(|| {
9851 MongrelError::NotFound(format!("table {table_id} not prepared"))
9852 })?;
9853 let batch_index = *apply_indexes.entry(table_id).or_insert_with(|| {
9854 let index = applies.len();
9855 applies.push(TableApplyBatch {
9856 table_id,
9857 handle,
9858 ops: Vec::new(),
9859 });
9860 index
9861 });
9862
9863 if spilled_tables.contains(&table_id) && matches!(&staging[index].1, Staged::Put(_))
9866 {
9867 index += 1;
9868 continue;
9869 }
9870
9871 match &staging[index].1 {
9872 Staged::Put(_) => {
9873 let mut rows = Vec::new();
9874 while index < staging.len()
9875 && staging[index].0 == table_id
9876 && matches!(&staging[index].1, Staged::Put(_))
9877 {
9878 let mut row = prebuilt[index].take().ok_or_else(|| {
9879 MongrelError::Other(
9880 "transaction prepare lost a prebuilt put row".into(),
9881 )
9882 })?;
9883 row.committed_epoch = new_epoch;
9884 rows.push(row);
9885 index += 1;
9886 }
9887 let payload = bincode::serialize(&rows)
9888 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
9889 wal_records.push((
9890 table_id,
9891 Op::Put {
9892 table_id,
9893 rows: payload,
9894 },
9895 ));
9896 applies[batch_index].ops.push(StagedOp::Put(rows));
9897 }
9898 Staged::Delete(_) => {
9899 let mut row_ids = Vec::new();
9900 while index < staging.len()
9901 && staging[index].0 == table_id
9902 && matches!(&staging[index].1, Staged::Delete(_))
9903 {
9904 let Staged::Delete(row_id) = &staging[index].1 else {
9905 return Err(MongrelError::Other(
9906 "transaction delete batch changed during WAL preparation"
9907 .into(),
9908 ));
9909 };
9910 if let Some(before) = &delete_images[index] {
9911 wal_records.push((
9912 table_id,
9913 Op::BeforeImage {
9914 table_id,
9915 row_id: *row_id,
9916 row: bincode::serialize(before).map_err(|error| {
9917 MongrelError::Other(format!(
9918 "before-image serialize: {error}"
9919 ))
9920 })?,
9921 },
9922 ));
9923 }
9924 row_ids.push(*row_id);
9925 index += 1;
9926 }
9927 wal_records.push((
9928 table_id,
9929 Op::Delete {
9930 table_id,
9931 row_ids: row_ids.clone(),
9932 },
9933 ));
9934 applies[batch_index].ops.push(StagedOp::Delete(row_ids));
9935 }
9936 Staged::Truncate => {
9937 wal_records.push((table_id, Op::TruncateTable { table_id }));
9938 applies[batch_index].ops.push(StagedOp::Truncate);
9939 index += 1;
9940 }
9941 Staged::Update { .. } => {
9942 return Err(MongrelError::Other(
9943 "transaction contains an unnormalized update at the sequencer".into(),
9944 ));
9945 }
9946 }
9947 }
9948
9949 for (name, state, _) in &prepared_external {
9950 wal_records.push((
9951 EXTERNAL_TABLE_ID,
9952 Op::ExternalTableState {
9953 name: name.clone(),
9954 state: state.clone(),
9955 },
9956 ));
9957 }
9958
9959 for (table_id, definition) in &prepared_materialized_views {
9960 let mut definition = definition.clone();
9961 definition.last_refresh_epoch = new_epoch.0;
9962 wal_records.push((
9963 *table_id,
9964 Op::Ddl(crate::wal::DdlOp::SetMaterializedView {
9965 name: definition.name.clone(),
9966 definition_json: crate::wal::DdlOp::encode_materialized_view(&definition)?,
9967 }),
9968 ));
9969 committed_materialized_views.push(definition);
9970 }
9971 if !committed_materialized_views.is_empty() {
9972 let mut next_catalog = self.catalog.read().clone();
9973 for definition in &committed_materialized_views {
9974 if let Some(existing) = next_catalog
9975 .materialized_views
9976 .iter_mut()
9977 .find(|existing| existing.name == definition.name)
9978 {
9979 *existing = definition.clone();
9980 } else {
9981 next_catalog.materialized_views.push(definition.clone());
9982 }
9983 }
9984 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
9985 wal_records.push((
9986 WAL_TABLE_ID,
9987 Op::Ddl(crate::wal::DdlOp::CatalogSnapshot {
9988 catalog_json: crate::wal::DdlOp::encode_catalog(&next_catalog)?,
9989 }),
9990 ));
9991 }
9992
9993 if let Some(control) = control {
9994 if let Err(error) = control.checkpoint() {
9995 drop(wal);
9996 for (_, _, pending) in &prepared_external {
9997 let _ = std::fs::remove_file(pending);
9998 }
9999 return Err(error);
10000 }
10001 }
10002 if let Some(before_commit) = before_commit.as_mut() {
10003 if let Err(error) = before_commit() {
10004 drop(wal);
10005 for (_, _, pending) in &prepared_external {
10006 let _ = std::fs::remove_file(pending);
10007 }
10008 return Err(error);
10009 }
10010 }
10011
10012 let assigned_epoch = self.epoch.bump_assigned();
10013 let _epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
10014 if assigned_epoch != new_epoch {
10015 for (_, _, pending) in &prepared_external {
10016 let _ = std::fs::remove_file(pending);
10017 }
10018 return Err(MongrelError::Conflict(
10019 "commit epoch changed while sequencer lock was held".into(),
10020 ));
10021 }
10022
10023 prepared_run_links.disarm();
10027
10028 let append: Result<u64> = (|| {
10029 for (table_id, op) in wal_records {
10030 wal.append(txn_id, table_id, op)?;
10031 }
10032 wal.append_commit(txn_id, new_epoch, &added_runs)
10033 })();
10034 let commit_seq =
10035 append.map_err(|error| self.commit_outcome_unknown(new_epoch, error))?;
10036
10037 self.conflicts.record(&write_keys, new_epoch);
10042 (
10043 new_epoch,
10044 _epoch_guard,
10045 applies,
10046 committed_materialized_views,
10047 commit_seq,
10048 )
10049 };
10050 drop(commit_guard);
10051
10052 self.await_durable_commit(commit_seq, new_epoch)?;
10054 drop(security_guard);
10055
10056 let publish_result: Result<()> = {
10058 let mut first_error = None;
10059 let mut spilled_by_table: HashMap<u64, Vec<&SpilledRun>> = HashMap::new();
10060 for run in &spilled {
10061 spilled_by_table.entry(run.table_id).or_default().push(run);
10062 }
10063 let mut modified_tables = Vec::with_capacity(applies.len());
10064 for batch in applies {
10067 #[cfg(test)]
10068 PUBLISH_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
10069 let mut t = batch.handle.lock();
10070 for op in batch.ops {
10071 match op {
10072 StagedOp::Put(rows) => t.apply_put_rows_prepared(rows),
10073 StagedOp::Delete(row_ids) => {
10074 for row_id in row_ids {
10075 t.apply_delete(row_id, new_epoch);
10076 }
10077 }
10078 StagedOp::Truncate => t.apply_truncate(new_epoch),
10079 }
10080 }
10081 if let Some(runs) = spilled_by_table.remove(&batch.table_id) {
10082 for run in runs {
10083 t.link_run(crate::manifest::RunRef {
10084 run_id: run.run_id,
10085 level: 0,
10086 epoch_created: new_epoch.0,
10087 row_count: run.row_count,
10088 });
10089 t.apply_run_metadata_prepared(&run.rows)?;
10090 if truncated_tables.contains(&batch.table_id) {
10091 t.set_flushed_epoch(new_epoch);
10096 }
10097 }
10098 }
10099 t.invalidate_pending_cache();
10100 drop(t);
10101 modified_tables.push(batch.handle);
10102 }
10103
10104 for handle in modified_tables {
10108 #[cfg(test)]
10109 COMMIT_MANIFEST_WRITES.with(|count| count.set(count.get() + 1));
10110 if let Err(error) = handle.lock().persist_manifest(new_epoch) {
10111 first_error.get_or_insert(error);
10112 }
10113 }
10114 for (name, _, pending) in &prepared_external {
10115 if let Err(error) = publish_external_state_file(&self.root, name, pending) {
10116 first_error.get_or_insert(error);
10117 }
10118 }
10119 if !committed_materialized_views.is_empty() {
10120 let mut next_catalog = self.catalog.read().clone();
10121 for definition in committed_materialized_views {
10122 if let Some(existing) = next_catalog
10123 .materialized_views
10124 .iter_mut()
10125 .find(|existing| existing.name == definition.name)
10126 {
10127 *existing = definition;
10128 } else {
10129 next_catalog.materialized_views.push(definition);
10130 }
10131 }
10132 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
10133 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
10134 first_error.get_or_insert(error);
10135 }
10136 }
10137 match first_error {
10138 Some(error) => Err(error),
10139 None => Ok(()),
10140 }
10141 };
10142
10143 if has_changes {
10144 let _ = self.change_wake.send(());
10145 }
10146 self.finish_durable_publish(new_epoch, &mut _epoch_guard, publish_result)?;
10147 Ok((new_epoch, committed_row_ids))
10148 }
10149
10150 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
10153 let e = self.epoch.visible();
10154 let g = self.snapshots.register(e);
10155 (Snapshot::at(e), g)
10156 }
10157
10158 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
10161 let e = self.epoch.visible();
10162 let g = self.snapshots.register_owned(e);
10163 (Snapshot::at(e), g)
10164 }
10165
10166 pub fn set_history_retention_epochs(&self, epochs: u64) -> Result<()> {
10171 let _guard = self.ddl_lock.lock();
10172 let current = self.epoch.visible();
10173 let (old_epochs, old_start) = self.snapshots.history_config();
10174 let earliest_already_guaranteed = if old_epochs == 0 {
10175 current
10176 } else {
10177 Epoch(old_start.0.max(current.0.saturating_sub(old_epochs)))
10178 };
10179 let start = if epochs == 0 {
10180 current
10181 } else {
10182 earliest_already_guaranteed
10183 };
10184 let published = std::cell::Cell::new(false);
10185 let result = write_history_retention(&self.root, epochs, start, || {
10186 self.snapshots.configure_history(epochs, start);
10187 published.set(true);
10188 });
10189 match result {
10190 Err(error) if published.get() => Err(MongrelError::CommitOutcomeUnknown {
10191 epoch: current.0,
10192 message: format!("history-retention publication was not durable: {error}"),
10193 }),
10194 result => result,
10195 }
10196 }
10197
10198 pub fn history_retention_epochs(&self) -> u64 {
10199 self.snapshots.history_config().0
10200 }
10201
10202 pub fn earliest_retained_epoch(&self) -> Epoch {
10203 let current = self.epoch.visible();
10204 self.snapshots.history_floor(current).unwrap_or(current)
10205 }
10206
10207 pub fn snapshot_at_owned(&self, epoch: Epoch) -> Result<(Snapshot, OwnedSnapshotGuard)> {
10210 let current = self.epoch.visible();
10211 if epoch > current {
10212 return Err(MongrelError::InvalidArgument(format!(
10213 "epoch {} is in the future; current epoch is {}",
10214 epoch.0, current.0
10215 )));
10216 }
10217 let earliest = self.earliest_retained_epoch();
10218 if epoch < earliest {
10219 return Err(MongrelError::InvalidArgument(format!(
10220 "epoch {} is no longer retained; earliest available epoch is {}",
10221 epoch.0, earliest.0
10222 )));
10223 }
10224 let guard = self.snapshots.register_owned(epoch);
10225 Ok((Snapshot::at(epoch), guard))
10226 }
10227
10228 pub fn table_names(&self) -> Vec<String> {
10230 self.catalog
10231 .read()
10232 .tables
10233 .iter()
10234 .filter(|t| matches!(t.state, TableState::Live))
10235 .map(|t| t.name.clone())
10236 .collect()
10237 }
10238
10239 pub fn close(&self) -> Result<()> {
10245 for name in self.table_names() {
10246 if let Ok(handle) = self.table(&name) {
10247 if let Err(e) = handle.lock().close() {
10248 eprintln!("[close] flush failed for {name}: {e}");
10249 }
10250 }
10251 }
10252 Ok(())
10253 }
10254
10255 pub fn compact(&self) -> Result<(usize, usize)> {
10262 self.require(&crate::auth::Permission::Ddl)?;
10263 let mut compacted = 0;
10264 let mut skipped = 0;
10265 for name in self.table_names() {
10266 let Ok(handle) = self.table(&name) else {
10267 continue;
10268 };
10269 {
10270 let mut t = handle.lock();
10271 let before = t.run_count();
10272 if before < 2 && !t.should_compact() {
10273 skipped += 1;
10274 continue;
10275 }
10276 match t.compact() {
10277 Ok(()) => {
10278 let after = t.run_count();
10279 compacted += 1;
10280 eprintln!("[compact] {name}: {before} -> {after} runs");
10281 }
10282 Err(e) => {
10283 eprintln!("[compact] {name}: compaction failed: {e}");
10284 skipped += 1;
10285 }
10286 }
10287 }
10288 }
10289 Ok((compacted, skipped))
10290 }
10291
10292 pub fn compact_table(&self, name: &str) -> Result<bool> {
10295 self.require(&crate::auth::Permission::Ddl)?;
10296 let handle = self.table(name)?;
10297 let mut t = handle.lock();
10298 let before = t.run_count();
10299 if before < 2 {
10300 return Ok(false);
10301 }
10302 t.compact()?;
10303 Ok(t.run_count() < before)
10304 }
10305
10306 pub fn table(&self, name: &str) -> Result<TableHandle> {
10308 let cat = self.catalog.read();
10309 let entry = cat
10310 .live(name)
10311 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
10312 let id = entry.table_id;
10313 drop(cat);
10314 self.tables
10315 .read()
10316 .get(&id)
10317 .cloned()
10318 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
10319 }
10320
10321 pub fn has_ttl_tables(&self) -> bool {
10324 self.tables
10325 .read()
10326 .values()
10327 .any(|table| table.lock().ttl().is_some())
10328 }
10329
10330 pub(crate) fn table_by_id(&self, id: u64) -> Result<TableHandle> {
10333 self.tables
10334 .read()
10335 .get(&id)
10336 .cloned()
10337 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
10338 }
10339
10340 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
10346 if name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
10347 return Err(MongrelError::InvalidArgument(format!(
10348 "table names beginning with {CTAS_BUILD_TABLE_PREFIX:?} are reserved"
10349 )));
10350 }
10351 self.create_table_with_state(name, schema, TableState::Live)
10352 }
10353
10354 #[doc(hidden)]
10356 pub fn create_building_table(
10357 &self,
10358 build_name: &str,
10359 intended_name: &str,
10360 query_id: &str,
10361 schema: Schema,
10362 ) -> Result<u64> {
10363 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10364 || intended_name.is_empty()
10365 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10366 || query_id.is_empty()
10367 {
10368 return Err(MongrelError::InvalidArgument(
10369 "invalid CTAS building-table identity".into(),
10370 ));
10371 }
10372 self.create_table_with_state(
10373 build_name,
10374 schema,
10375 TableState::Building {
10376 intended_name: intended_name.to_string(),
10377 query_id: query_id.to_string(),
10378 created_at_unix_nanos: current_unix_nanos(),
10379 replaces_table_id: None,
10380 },
10381 )
10382 }
10383
10384 #[doc(hidden)]
10387 pub fn create_rebuilding_table(
10388 &self,
10389 build_name: &str,
10390 intended_name: &str,
10391 query_id: &str,
10392 schema: Schema,
10393 ) -> Result<u64> {
10394 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10395 || intended_name.is_empty()
10396 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10397 || query_id.is_empty()
10398 {
10399 return Err(MongrelError::InvalidArgument(
10400 "invalid rebuilding-table identity".into(),
10401 ));
10402 }
10403 let replaces_table_id = self
10404 .catalog
10405 .read()
10406 .live(intended_name)
10407 .ok_or_else(|| MongrelError::NotFound(format!("table {intended_name:?} not found")))?
10408 .table_id;
10409 self.create_table_with_state(
10410 build_name,
10411 schema,
10412 TableState::Building {
10413 intended_name: intended_name.to_string(),
10414 query_id: query_id.to_string(),
10415 created_at_unix_nanos: current_unix_nanos(),
10416 replaces_table_id: Some(replaces_table_id),
10417 },
10418 )
10419 }
10420
10421 fn create_table_with_state(
10422 &self,
10423 name: &str,
10424 schema: Schema,
10425 state: TableState,
10426 ) -> Result<u64> {
10427 use crate::wal::DdlOp;
10428 use std::sync::atomic::Ordering;
10429
10430 self.require(&crate::auth::Permission::Ddl)?;
10431 if self.poisoned.load(Ordering::Relaxed) {
10432 return Err(MongrelError::Other(
10433 "database poisoned by fsync error".into(),
10434 ));
10435 }
10436
10437 let _g = self.ddl_lock.lock();
10438 let _security_write = self.security_write()?;
10439 self.require(&crate::auth::Permission::Ddl)?;
10440 {
10441 let cat = self.catalog.read();
10442 match &state {
10443 TableState::Live => {
10444 if cat.live(name).is_some() || cat.building_for(name).is_some() {
10445 return Err(MongrelError::InvalidArgument(format!(
10446 "table {name:?} already exists or is being built"
10447 )));
10448 }
10449 }
10450 TableState::Building {
10451 intended_name,
10452 replaces_table_id,
10453 ..
10454 } => {
10455 let target_matches = match replaces_table_id {
10456 Some(table_id) => cat
10457 .live(intended_name)
10458 .is_some_and(|entry| entry.table_id == *table_id),
10459 None => cat.live(intended_name).is_none(),
10460 };
10461 if !target_matches || cat.building_for(intended_name).is_some() {
10462 return Err(MongrelError::InvalidArgument(format!(
10463 "table {intended_name:?} changed or is already being built"
10464 )));
10465 }
10466 if cat.building(name).is_some() {
10467 return Err(MongrelError::InvalidArgument(format!(
10468 "building table {name:?} already exists"
10469 )));
10470 }
10471 }
10472 TableState::Dropped { .. } => {
10473 return Err(MongrelError::InvalidArgument(
10474 "cannot create a dropped table".into(),
10475 ));
10476 }
10477 }
10478 }
10479
10480 let commit_lock = Arc::clone(&self.commit_lock);
10483 let _c = commit_lock.lock();
10484 let table_id = {
10485 let mut cat = self.catalog.write();
10486 let id = cat.next_table_id;
10487 cat.next_table_id = id
10488 .checked_add(1)
10489 .ok_or_else(|| MongrelError::InvalidArgument("table id space exhausted".into()))?;
10490 Result::<u64>::Ok(id)
10491 }?;
10492 let epoch = self.epoch.bump_assigned();
10493 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
10494 let txn_id = self.alloc_txn_id()?;
10495
10496 let mut schema = schema;
10500 schema.schema_id = table_id;
10501 schema.validate_auto_increment()?;
10508 schema.validate_defaults()?;
10509 schema.validate_ai()?;
10510 for index in &schema.indexes {
10511 index.validate_options()?;
10512 }
10513 for constraint in &schema.constraints.checks {
10514 constraint.expr.validate()?;
10515 }
10516
10517 let table_relative = Path::new(TABLES_DIR).join(table_id.to_string());
10520 let canonical_tdir = self.root.join(&table_relative);
10521 let table_root = Arc::new(
10522 self.durable_root
10523 .create_directory_all_pinned(&table_relative)?,
10524 );
10525 let tdir = table_root.io_path()?;
10526 let mut pending_table_dir = PendingTableDir::new(canonical_tdir);
10527 let ctx = SharedCtx {
10528 root_guard: Some(table_root),
10529 epoch: Arc::clone(&self.epoch),
10530 page_cache: Arc::clone(&self.page_cache),
10531 decoded_cache: Arc::clone(&self.decoded_cache),
10532 snapshots: Arc::clone(&self.snapshots),
10533 kek: self.kek.clone(),
10534 commit_lock: Arc::clone(&self.commit_lock),
10535 shared: Some(crate::engine::SharedWalCtx {
10536 wal: Arc::clone(&self.shared_wal),
10537 group: Arc::clone(&self.group),
10538 poisoned: Arc::clone(&self.poisoned),
10539 txn_ids: Arc::clone(&self.next_txn_id),
10540 change_wake: self.change_wake.clone(),
10541 }),
10542 table_name: Some(name.to_string()),
10543 auth: self.table_auth_checker(),
10544 read_only: self.read_only,
10545 };
10546 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
10547
10548 let schema_json = DdlOp::encode_schema(&schema)?;
10551 let ddl = match &state {
10552 TableState::Live => DdlOp::CreateTable {
10553 table_id,
10554 name: name.to_string(),
10555 schema_json,
10556 },
10557 TableState::Building {
10558 intended_name,
10559 query_id,
10560 created_at_unix_nanos,
10561 replaces_table_id,
10562 } => match replaces_table_id {
10563 Some(replaces_table_id) => DdlOp::CreateRebuildingTable {
10564 table_id,
10565 build_name: name.to_string(),
10566 intended_name: intended_name.clone(),
10567 query_id: query_id.clone(),
10568 created_at_unix_nanos: *created_at_unix_nanos,
10569 replaces_table_id: *replaces_table_id,
10570 schema_json,
10571 },
10572 None => DdlOp::CreateBuildingTable {
10573 table_id,
10574 build_name: name.to_string(),
10575 intended_name: intended_name.clone(),
10576 query_id: query_id.clone(),
10577 created_at_unix_nanos: *created_at_unix_nanos,
10578 schema_json,
10579 },
10580 },
10581 TableState::Dropped { .. } => {
10582 return Err(MongrelError::InvalidArgument(
10583 "cannot create a table in dropped state".into(),
10584 ));
10585 }
10586 };
10587 let mut next_catalog = self.catalog.read().clone();
10588 next_catalog.tables.push(CatalogEntry {
10589 table_id,
10590 name: name.to_string(),
10591 schema: schema.clone(),
10592 state: state.clone(),
10593 created_epoch: epoch.0,
10594 });
10595 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
10596 let commit_seq = {
10597 let mut wal = self.shared_wal.lock();
10598 let append: Result<u64> = (|| {
10599 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
10600 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
10601 wal.append_commit(txn_id, epoch, &[])
10602 })();
10603 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
10604 };
10605 self.await_durable_commit(commit_seq, epoch)?;
10606 pending_table_dir.disarm();
10607
10608 self.tables
10611 .write()
10612 .insert(table_id, TableHandle::new(table));
10613 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
10614 self.finish_durable_publish(epoch, &mut _epoch_guard, checkpoint)?;
10615 Ok(table_id)
10616 }
10617
10618 pub fn drop_table(&self, name: &str) -> Result<()> {
10620 self.drop_table_with_epoch(name).map(|_| ())
10621 }
10622
10623 pub fn drop_table_with_epoch(&self, name: &str) -> Result<Epoch> {
10625 self.drop_table_with_state(name, false, None)
10626 }
10627
10628 pub fn drop_table_with_epoch_controlled<F>(
10629 &self,
10630 name: &str,
10631 mut before_commit: F,
10632 ) -> Result<Epoch>
10633 where
10634 F: FnMut() -> Result<()>,
10635 {
10636 self.drop_table_with_state(name, false, Some(&mut before_commit))
10637 }
10638
10639 #[doc(hidden)]
10641 pub fn discard_building_table(&self, name: &str) -> Result<()> {
10642 if !name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
10643 return Err(MongrelError::InvalidArgument(
10644 "not a CTAS building table".into(),
10645 ));
10646 }
10647 self.drop_table_with_state(name, true, None).map(|_| ())
10648 }
10649
10650 fn drop_table_with_state(
10651 &self,
10652 name: &str,
10653 building: bool,
10654 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10655 ) -> Result<Epoch> {
10656 use crate::wal::DdlOp;
10657 use std::sync::atomic::Ordering;
10658
10659 self.require(&crate::auth::Permission::Ddl)?;
10660 if self.poisoned.load(Ordering::Relaxed) {
10661 return Err(MongrelError::Other(
10662 "database poisoned by fsync error".into(),
10663 ));
10664 }
10665
10666 let _g = self.ddl_lock.lock();
10667 let _security_write = self.security_write()?;
10668 self.require(&crate::auth::Permission::Ddl)?;
10669 let table_id = {
10670 let cat = self.catalog.read();
10671 if building {
10672 cat.building(name)
10673 } else {
10674 cat.live(name)
10675 }
10676 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
10677 .table_id
10678 };
10679
10680 let commit_lock = Arc::clone(&self.commit_lock);
10681 let _c = commit_lock.lock();
10682 let epoch = self.epoch.bump_assigned();
10683 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
10684 let txn_id = self.alloc_txn_id()?;
10685 let mut next_catalog = self.catalog.read().clone();
10686 let entry = next_catalog
10687 .tables
10688 .iter_mut()
10689 .find(|t| t.table_id == table_id)
10690 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
10691 entry.state = TableState::Dropped { at_epoch: epoch.0 };
10692 next_catalog.triggers.retain(|trigger| {
10693 !matches!(
10694 &trigger.trigger.target,
10695 TriggerTarget::Table(target) if target == name
10696 )
10697 });
10698 next_catalog
10699 .materialized_views
10700 .retain(|definition| definition.name != name);
10701 next_catalog
10702 .security
10703 .rls_tables
10704 .retain(|table| table != name);
10705 next_catalog
10706 .security
10707 .policies
10708 .retain(|policy| policy.table != name);
10709 next_catalog
10710 .security
10711 .masks
10712 .retain(|mask| mask.table != name);
10713 for role in &mut next_catalog.roles {
10714 role.permissions
10715 .retain(|permission| permission_table(permission) != Some(name));
10716 }
10717 advance_security_version(&mut next_catalog)?;
10718 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
10719 let commit_seq = {
10720 let mut wal = self.shared_wal.lock();
10721 if let Some(before_commit) = before_commit {
10722 before_commit()?;
10723 }
10724 let append: Result<u64> = (|| {
10725 wal.append(
10726 txn_id,
10727 table_id,
10728 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
10729 )?;
10730 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
10731 wal.append_commit(txn_id, epoch, &[])
10732 })();
10733 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
10734 };
10735 self.await_durable_commit(commit_seq, epoch)?;
10736
10737 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
10738 self.tables.write().remove(&table_id);
10739 self.finish_durable_publish(epoch, &mut _epoch_guard, checkpoint)?;
10740 Ok(epoch)
10741 }
10742
10743 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
10752 self.rename_table_with_epoch(name, new_name).map(|_| ())
10753 }
10754
10755 pub fn rename_table_with_epoch(&self, name: &str, new_name: &str) -> Result<Epoch> {
10757 self.rename_table_with_epoch_inner(name, new_name, None)
10758 }
10759
10760 pub fn rename_table_with_epoch_controlled<F>(
10761 &self,
10762 name: &str,
10763 new_name: &str,
10764 mut before_commit: F,
10765 ) -> Result<Epoch>
10766 where
10767 F: FnMut() -> Result<()>,
10768 {
10769 self.rename_table_with_epoch_inner(name, new_name, Some(&mut before_commit))
10770 }
10771
10772 fn rename_table_with_epoch_inner(
10773 &self,
10774 name: &str,
10775 new_name: &str,
10776 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10777 ) -> Result<Epoch> {
10778 if name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10779 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10780 {
10781 return Err(MongrelError::InvalidArgument(
10782 "the CTAS building-table namespace is reserved".into(),
10783 ));
10784 }
10785 self.rename_table_with_state(name, new_name, false, None, before_commit)
10786 }
10787
10788 #[doc(hidden)]
10790 pub fn publish_building_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
10791 self.publish_building_table_inner(build_name, new_name, None)
10792 }
10793
10794 #[doc(hidden)]
10795 pub fn publish_building_table_controlled<F>(
10796 &self,
10797 build_name: &str,
10798 new_name: &str,
10799 mut before_commit: F,
10800 ) -> Result<Epoch>
10801 where
10802 F: FnMut() -> Result<()>,
10803 {
10804 self.publish_building_table_inner(build_name, new_name, Some(&mut before_commit))
10805 }
10806
10807 fn publish_building_table_inner(
10808 &self,
10809 build_name: &str,
10810 new_name: &str,
10811 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10812 ) -> Result<Epoch> {
10813 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10814 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10815 {
10816 return Err(MongrelError::InvalidArgument(
10817 "invalid CTAS publish identity".into(),
10818 ));
10819 }
10820 self.rename_table_with_state(build_name, new_name, true, None, before_commit)
10821 }
10822
10823 #[doc(hidden)]
10825 pub fn publish_materialized_building_table(
10826 &self,
10827 build_name: &str,
10828 new_name: &str,
10829 definition: crate::catalog::MaterializedViewEntry,
10830 ) -> Result<Epoch> {
10831 self.publish_materialized_building_table_inner(build_name, new_name, definition, None)
10832 }
10833
10834 #[doc(hidden)]
10835 pub fn publish_materialized_building_table_controlled<F>(
10836 &self,
10837 build_name: &str,
10838 new_name: &str,
10839 definition: crate::catalog::MaterializedViewEntry,
10840 mut before_commit: F,
10841 ) -> Result<Epoch>
10842 where
10843 F: FnMut() -> Result<()>,
10844 {
10845 self.publish_materialized_building_table_inner(
10846 build_name,
10847 new_name,
10848 definition,
10849 Some(&mut before_commit),
10850 )
10851 }
10852
10853 fn publish_materialized_building_table_inner(
10854 &self,
10855 build_name: &str,
10856 new_name: &str,
10857 definition: crate::catalog::MaterializedViewEntry,
10858 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10859 ) -> Result<Epoch> {
10860 if definition.name != new_name || definition.query.trim().is_empty() {
10861 return Err(MongrelError::InvalidArgument(
10862 "invalid materialized-view publication".into(),
10863 ));
10864 }
10865 self.rename_table_with_state(build_name, new_name, true, Some(definition), before_commit)
10866 }
10867
10868 #[doc(hidden)]
10870 pub fn publish_rebuilding_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
10871 self.publish_rebuilding_table_inner(build_name, new_name, None, None)
10872 }
10873
10874 #[doc(hidden)]
10875 pub fn publish_rebuilding_table_controlled<F>(
10876 &self,
10877 build_name: &str,
10878 new_name: &str,
10879 mut before_commit: F,
10880 ) -> Result<Epoch>
10881 where
10882 F: FnMut() -> Result<()>,
10883 {
10884 self.publish_rebuilding_table_inner(build_name, new_name, None, Some(&mut before_commit))
10885 }
10886
10887 #[doc(hidden)]
10889 pub fn publish_materialized_rebuilding_table_controlled<F>(
10890 &self,
10891 build_name: &str,
10892 new_name: &str,
10893 definition: crate::catalog::MaterializedViewEntry,
10894 mut before_commit: F,
10895 ) -> Result<Epoch>
10896 where
10897 F: FnMut() -> Result<()>,
10898 {
10899 self.publish_rebuilding_table_inner(
10900 build_name,
10901 new_name,
10902 Some(definition),
10903 Some(&mut before_commit),
10904 )
10905 }
10906
10907 fn publish_rebuilding_table_inner(
10908 &self,
10909 build_name: &str,
10910 new_name: &str,
10911 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
10912 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10913 ) -> Result<Epoch> {
10914 use crate::wal::DdlOp;
10915
10916 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10917 || new_name.is_empty()
10918 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10919 {
10920 return Err(MongrelError::InvalidArgument(
10921 "invalid rebuilding-table publish identity".into(),
10922 ));
10923 }
10924 if materialized_view.as_ref().is_some_and(|definition| {
10925 definition.name != new_name || definition.query.trim().is_empty()
10926 }) {
10927 return Err(MongrelError::InvalidArgument(
10928 "invalid materialized-view replacement".into(),
10929 ));
10930 }
10931 self.require(&crate::auth::Permission::Ddl)?;
10932 if self.poisoned.load(Ordering::Relaxed) {
10933 return Err(MongrelError::Other(
10934 "database poisoned by fsync error".into(),
10935 ));
10936 }
10937
10938 let _ddl = self.ddl_lock.lock();
10939 let _security_write = self.security_write()?;
10940 let (table_id, replaced_table_id) = {
10941 let catalog = self.catalog.read();
10942 let build = catalog.building(build_name).ok_or_else(|| {
10943 MongrelError::NotFound(format!("building table {build_name:?} not found"))
10944 })?;
10945 let replaced_table_id = match &build.state {
10946 TableState::Building {
10947 intended_name,
10948 replaces_table_id: Some(replaced_table_id),
10949 ..
10950 } if intended_name == new_name => *replaced_table_id,
10951 _ => {
10952 return Err(MongrelError::InvalidArgument(format!(
10953 "building table {build_name:?} is not a replacement for {new_name:?}"
10954 )))
10955 }
10956 };
10957 if catalog
10958 .live(new_name)
10959 .is_none_or(|entry| entry.table_id != replaced_table_id)
10960 {
10961 return Err(MongrelError::Conflict(format!(
10962 "table {new_name:?} changed while its replacement was built"
10963 )));
10964 }
10965 (build.table_id, replaced_table_id)
10966 };
10967
10968 let _commit = self.commit_lock.lock();
10969 let epoch = self.epoch.assigned().next();
10970 let txn_id = self.alloc_txn_id()?;
10971 let mut next_catalog = self.catalog.read().clone();
10972 apply_rebuilding_publish(
10973 &mut next_catalog,
10974 table_id,
10975 replaced_table_id,
10976 new_name,
10977 epoch.0,
10978 )?;
10979 if let Some(definition) = materialized_view.as_mut() {
10980 definition.last_refresh_epoch = epoch.0;
10981 }
10982 let materialized_view_json = materialized_view
10983 .as_ref()
10984 .map(DdlOp::encode_materialized_view)
10985 .transpose()?;
10986 if let Some(definition) = materialized_view {
10987 if let Some(existing) = next_catalog
10988 .materialized_views
10989 .iter_mut()
10990 .find(|existing| existing.name == definition.name)
10991 {
10992 *existing = definition;
10993 } else {
10994 next_catalog.materialized_views.push(definition);
10995 }
10996 }
10997 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
10998 if let Some(before_commit) = before_commit {
10999 before_commit()?;
11000 }
11001 let assigned_epoch = self.epoch.bump_assigned();
11002 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
11003 if assigned_epoch != epoch {
11004 return Err(MongrelError::Conflict(
11005 "commit epoch changed while sequencer lock was held".into(),
11006 ));
11007 }
11008 let commit_seq = {
11009 let mut wal = self.shared_wal.lock();
11010 let append: Result<u64> = (|| {
11011 wal.append(
11012 txn_id,
11013 table_id,
11014 crate::wal::Op::Ddl(DdlOp::ReplaceBuildingTable {
11015 table_id,
11016 replaced_table_id,
11017 new_name: new_name.to_string(),
11018 }),
11019 )?;
11020 if let Some(definition_json) = materialized_view_json {
11021 wal.append(
11022 txn_id,
11023 table_id,
11024 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
11025 name: new_name.to_string(),
11026 definition_json,
11027 }),
11028 )?;
11029 }
11030 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11031 wal.append_commit(txn_id, epoch, &[])
11032 })();
11033 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11034 };
11035 self.await_durable_commit(commit_seq, epoch)?;
11036
11037 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
11038 self.tables.write().remove(&replaced_table_id);
11039 if let Some(table) = self.tables.read().get(&table_id) {
11040 table.lock().set_catalog_name(new_name.to_string());
11041 }
11042 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
11043 Ok(epoch)
11044 }
11045
11046 fn rename_table_with_state(
11047 &self,
11048 name: &str,
11049 new_name: &str,
11050 building: bool,
11051 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
11052 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11053 ) -> Result<Epoch> {
11054 use crate::wal::DdlOp;
11055 use std::sync::atomic::Ordering;
11056
11057 self.require(&crate::auth::Permission::Ddl)?;
11058 if self.poisoned.load(Ordering::Relaxed) {
11059 return Err(MongrelError::Other(
11060 "database poisoned by fsync error".into(),
11061 ));
11062 }
11063
11064 if name == new_name {
11067 return Ok(self.visible_epoch());
11068 }
11069 if new_name.is_empty() {
11070 return Err(MongrelError::InvalidArgument(
11071 "rename_table: new name must not be empty".into(),
11072 ));
11073 }
11074
11075 let _g = self.ddl_lock.lock();
11076 let _security_write = self.security_write()?;
11077 self.require(&crate::auth::Permission::Ddl)?;
11078 let table_id = {
11079 let cat = self.catalog.read();
11080 let src = if building {
11081 cat.building(name)
11082 } else {
11083 cat.live(name)
11084 }
11085 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
11086 if building
11087 && !matches!(
11088 &src.state,
11089 TableState::Building { intended_name, .. } if intended_name == new_name
11090 )
11091 {
11092 return Err(MongrelError::InvalidArgument(format!(
11093 "building table {name:?} is not reserved for {new_name:?}"
11094 )));
11095 }
11096 if cat.live(new_name).is_some() || (!building && cat.building_for(new_name).is_some()) {
11100 return Err(MongrelError::InvalidArgument(format!(
11101 "rename_table: a table named {new_name:?} already exists"
11102 )));
11103 }
11104 src.table_id
11105 };
11106
11107 let commit_lock = Arc::clone(&self.commit_lock);
11108 let _c = commit_lock.lock();
11109 let epoch = self.epoch.bump_assigned();
11110 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
11111 let txn_id = self.alloc_txn_id()?;
11112 if let Some(definition) = materialized_view.as_mut() {
11113 definition.last_refresh_epoch = epoch.0;
11114 }
11115 let materialized_view_json = materialized_view
11116 .as_ref()
11117 .map(DdlOp::encode_materialized_view)
11118 .transpose()?;
11119 let mut next_catalog = self.catalog.read().clone();
11120 let entry = next_catalog
11121 .tables
11122 .iter_mut()
11123 .find(|t| t.table_id == table_id)
11124 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
11125 entry.name = new_name.to_string();
11126 if building {
11127 entry.state = TableState::Live;
11128 }
11129 for trigger in &mut next_catalog.triggers {
11130 if matches!(
11131 &trigger.trigger.target,
11132 TriggerTarget::Table(target) if target == name
11133 ) {
11134 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
11135 }
11136 }
11137 if let Some(definition) = next_catalog
11138 .materialized_views
11139 .iter_mut()
11140 .find(|definition| definition.name == name)
11141 {
11142 definition.name = new_name.to_string();
11143 }
11144 if let Some(definition) = materialized_view.take() {
11145 next_catalog.materialized_views.push(definition);
11146 }
11147 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11148 for table in &mut next_catalog.security.rls_tables {
11149 if table == name {
11150 *table = new_name.to_string();
11151 }
11152 }
11153 for policy in &mut next_catalog.security.policies {
11154 if policy.table == name {
11155 policy.table = new_name.to_string();
11156 }
11157 }
11158 for mask in &mut next_catalog.security.masks {
11159 if mask.table == name {
11160 mask.table = new_name.to_string();
11161 }
11162 }
11163 for role in &mut next_catalog.roles {
11164 for permission in &mut role.permissions {
11165 rename_permission_table(permission, name, new_name);
11166 }
11167 }
11168 advance_security_version(&mut next_catalog)?;
11169 let ddl = if building {
11170 DdlOp::PublishBuildingTable {
11171 table_id,
11172 new_name: new_name.to_string(),
11173 }
11174 } else {
11175 DdlOp::RenameTable {
11176 table_id,
11177 new_name: new_name.to_string(),
11178 }
11179 };
11180 let commit_seq = {
11181 let mut wal = self.shared_wal.lock();
11182 if let Some(before_commit) = before_commit {
11183 before_commit()?;
11184 }
11185 let append: Result<u64> = (|| {
11186 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
11187 if let Some(definition_json) = materialized_view_json {
11188 wal.append(
11189 txn_id,
11190 table_id,
11191 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
11192 name: new_name.to_string(),
11193 definition_json,
11194 }),
11195 )?;
11196 }
11197 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11198 wal.append_commit(txn_id, epoch, &[])
11199 })();
11200 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11201 };
11202 self.await_durable_commit(commit_seq, epoch)?;
11203
11204 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
11205 if let Some(table) = self.tables.read().get(&table_id) {
11208 table.lock().set_catalog_name(new_name.to_string());
11209 }
11210 self.finish_durable_publish(epoch, &mut _epoch_guard, checkpoint)?;
11211 Ok(epoch)
11212 }
11213
11214 pub fn alter_column(
11215 &self,
11216 table_name: &str,
11217 column_name: &str,
11218 change: AlterColumn,
11219 ) -> Result<ColumnDef> {
11220 self.alter_column_with_epoch(table_name, column_name, change)
11221 .map(|(column, _)| column)
11222 }
11223
11224 pub fn alter_column_with_epoch(
11225 &self,
11226 table_name: &str,
11227 column_name: &str,
11228 change: AlterColumn,
11229 ) -> Result<(ColumnDef, Option<Epoch>)> {
11230 self.alter_column_with_epoch_inner(table_name, column_name, change, None, None, None)
11231 }
11232
11233 pub fn alter_column_with_epoch_controlled<B, A>(
11238 &self,
11239 table_name: &str,
11240 column_name: &str,
11241 change: AlterColumn,
11242 control: &crate::ExecutionControl,
11243 mut before_commit: B,
11244 mut after_commit: A,
11245 ) -> Result<(ColumnDef, Option<Epoch>)>
11246 where
11247 B: FnMut() -> Result<()>,
11248 A: FnMut(Option<Epoch>) -> Result<()>,
11249 {
11250 self.alter_column_with_epoch_inner(
11251 table_name,
11252 column_name,
11253 change,
11254 Some(control),
11255 Some(&mut before_commit),
11256 Some(&mut after_commit),
11257 )
11258 }
11259
11260 #[allow(clippy::too_many_arguments)]
11261 fn alter_column_with_epoch_inner(
11262 &self,
11263 table_name: &str,
11264 column_name: &str,
11265 change: AlterColumn,
11266 control: Option<&crate::ExecutionControl>,
11267 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11268 mut after_commit: Option<&mut dyn FnMut(Option<Epoch>) -> Result<()>>,
11269 ) -> Result<(ColumnDef, Option<Epoch>)> {
11270 use crate::wal::DdlOp;
11271 use std::sync::atomic::Ordering;
11272
11273 self.require(&crate::auth::Permission::Ddl)?;
11274 commit_prepare_checkpoint(control, 0)?;
11275 if self.poisoned.load(Ordering::Relaxed) {
11276 return Err(MongrelError::Other(
11277 "database poisoned by fsync error".into(),
11278 ));
11279 }
11280
11281 let _g = self.ddl_lock.lock();
11282 let table_id = {
11283 let cat = self.catalog.read();
11284 cat.live(table_name)
11285 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
11286 .table_id
11287 };
11288 let handle =
11289 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
11290 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
11291 })?;
11292
11293 let backfill = {
11299 let table = handle.lock();
11300 let old = table
11301 .schema()
11302 .column(column_name)
11303 .cloned()
11304 .ok_or_else(|| MongrelError::Schema(format!("unknown column {column_name}")))?;
11305 let next_flags = change.flags.unwrap_or(old.flags);
11306 if old.flags.contains(crate::schema::ColumnFlags::NULLABLE)
11307 && !next_flags.contains(crate::schema::ColumnFlags::NULLABLE)
11308 && old.default_value.is_some()
11309 {
11310 let snapshot = Snapshot::at(self.epoch.visible());
11311 let mut updates = Vec::new();
11312 let rows = match control {
11313 Some(control) => table.visible_rows_controlled(snapshot, control)?,
11314 None => table.visible_rows(snapshot)?,
11315 };
11316 for (row_index, row) in rows.into_iter().enumerate() {
11317 commit_prepare_checkpoint(control, row_index)?;
11318 if row
11319 .columns
11320 .get(&old.id)
11321 .is_some_and(|value| !matches!(value, Value::Null))
11322 {
11323 continue;
11324 }
11325 let mut cells: Vec<(u16, Value)> = row.columns.into_iter().collect();
11326 table.apply_defaults(&mut cells)?;
11327 updates.push((
11328 table_id,
11329 crate::txn::Staged::Update {
11330 row_id: row.row_id,
11331 new_row: cells,
11332 changed_columns: vec![old.id],
11333 },
11334 ));
11335 }
11336 updates
11337 } else {
11338 Vec::new()
11339 }
11340 };
11341 let durable_epoch = std::cell::Cell::new(None);
11342 let backfill_epoch = if backfill.is_empty() {
11343 None
11344 } else {
11345 let (principal, catalog_bound) = self.transaction_principal_snapshot();
11346 let txn_id = self.alloc_txn_id()?;
11347 let mut entered_fence = false;
11348 let commit_result = match (control, before_commit.as_deref_mut()) {
11349 (Some(control), Some(before_commit)) => self
11350 .commit_transaction_with_external_states_controlled(
11351 txn_id,
11352 self.epoch.visible(),
11353 backfill,
11354 Vec::new(),
11355 Vec::new(),
11356 principal.clone(),
11357 catalog_bound,
11358 None,
11359 control,
11360 &mut || {
11361 before_commit()?;
11362 entered_fence = true;
11363 Ok(())
11364 },
11365 )
11366 .map(|(epoch, _)| epoch),
11367 _ => self
11368 .commit_transaction_with_external_states(
11369 txn_id,
11370 self.epoch.visible(),
11371 backfill,
11372 Vec::new(),
11373 Vec::new(),
11374 principal,
11375 catalog_bound,
11376 None,
11377 )
11378 .map(|(epoch, _)| epoch),
11379 };
11380 let commit_result = if entered_fence {
11381 finish_controlled_commit_attempt(commit_result, &mut after_commit)
11382 } else {
11383 commit_result
11384 };
11385 match &commit_result {
11386 Ok(epoch) => durable_epoch.set(Some(*epoch)),
11387 Err(MongrelError::DurableCommit { epoch, .. }) => {
11388 durable_epoch.set(Some(Epoch(*epoch)));
11389 }
11390 Err(_) => {}
11391 }
11392 Some(commit_result?)
11393 };
11394 let result: Result<(ColumnDef, Option<Epoch>)> = (|| {
11395 let _security_write = self.security_write()?;
11396 self.require(&crate::auth::Permission::Ddl)?;
11397 if self
11398 .catalog
11399 .read()
11400 .live(table_name)
11401 .is_none_or(|entry| entry.table_id != table_id)
11402 {
11403 return Err(MongrelError::Conflict(format!(
11404 "table {table_name:?} changed during ALTER"
11405 )));
11406 }
11407 let mut table = handle.lock();
11408 let (column, prepared_schema) = table.prepare_alter_column(column_name, &change)?;
11409 let renamed_column = (column.name != column_name).then(|| column.name.clone());
11410 let Some(prepared_schema) = prepared_schema else {
11411 return Ok((column, backfill_epoch));
11412 };
11413
11414 let commit_lock = Arc::clone(&self.commit_lock);
11415 let _c = commit_lock.lock();
11416 let epoch = self.epoch.bump_assigned();
11417 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
11418 let txn_id = self.alloc_txn_id()?;
11419 let column_json = DdlOp::encode_column(&column)?;
11420 let mut next_catalog = self.catalog.read().clone();
11421 let catalog_entry_index = next_catalog
11422 .tables
11423 .iter()
11424 .position(|entry| entry.table_id == table_id)
11425 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
11426 if let Some(new_column_name) = &renamed_column {
11427 for (trigger_index, trigger) in next_catalog.triggers.iter_mut().enumerate() {
11428 commit_prepare_checkpoint(control, trigger_index)?;
11429 if matches!(
11430 &trigger.trigger.target,
11431 TriggerTarget::Table(target) if target == table_name
11432 ) {
11433 trigger.trigger = trigger.trigger.renamed_update_column(
11434 column_name,
11435 new_column_name.clone(),
11436 epoch.0,
11437 )?;
11438 }
11439 }
11440 for (role_index, role) in next_catalog.roles.iter_mut().enumerate() {
11441 commit_prepare_checkpoint(control, role_index)?;
11442 for (permission_index, permission) in role.permissions.iter_mut().enumerate() {
11443 commit_prepare_checkpoint(control, permission_index)?;
11444 rename_permission_column(
11445 permission,
11446 table_name,
11447 column_name,
11448 new_column_name,
11449 );
11450 }
11451 }
11452 advance_security_version(&mut next_catalog)?;
11453 }
11454 next_catalog.tables[catalog_entry_index].schema = prepared_schema.clone();
11455 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11456 commit_prepare_checkpoint(control, 0)?;
11457 let mut entered_fence = false;
11458 if let Some(before_commit) = before_commit.as_deref_mut() {
11459 before_commit()?;
11460 entered_fence = true;
11461 }
11462 let commit_result: Result<Epoch> = (|| {
11463 let commit_seq = {
11464 let mut wal = self.shared_wal.lock();
11465 let append: Result<u64> = (|| {
11466 wal.append(
11467 txn_id,
11468 table_id,
11469 crate::wal::Op::Ddl(DdlOp::AlterTable {
11470 table_id,
11471 column_json,
11472 }),
11473 )?;
11474 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11475 wal.append_commit(txn_id, epoch, &[])
11476 })();
11477 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11478 };
11479 self.await_durable_commit(commit_seq, epoch)?;
11480 durable_epoch.set(Some(epoch));
11481
11482 table.apply_altered_schema_prepared(prepared_schema);
11483 let schema = table.schema().clone();
11484 let table_checkpoint = table.checkpoint_altered_schema();
11485 drop(table);
11486 next_catalog.tables[catalog_entry_index].schema = schema;
11487 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11488 let catalog_result =
11489 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref());
11490 let security_version = next_catalog.security_version;
11491 *self.catalog.write() = next_catalog;
11492 if renamed_column.is_some() {
11493 self.security_coordinator
11494 .version
11495 .store(security_version, Ordering::Release);
11496 }
11497 self.epoch.publish_in_order(epoch);
11498 _epoch_guard.disarm();
11499 if let Err(error) = table_checkpoint.and(catalog_result) {
11500 self.poisoned.store(true, Ordering::Relaxed);
11501 return Err(MongrelError::DurableCommit {
11502 epoch: epoch.0,
11503 message: error.to_string(),
11504 });
11505 }
11506 Ok(epoch)
11507 })();
11508 let commit_result = if entered_fence {
11509 finish_controlled_commit_attempt(commit_result, &mut after_commit)
11510 } else {
11511 commit_result
11512 };
11513 let epoch = commit_result?;
11514 Ok((column, Some(epoch)))
11515 })();
11516 result.map_err(|error| match (durable_epoch.get(), error) {
11517 (_, error @ MongrelError::DurableCommit { .. }) => error,
11518 (Some(epoch), error) => MongrelError::DurableCommit {
11519 epoch: epoch.0,
11520 message: error.to_string(),
11521 },
11522 (None, error) => error,
11523 })
11524 }
11525
11526 pub fn set_table_ttl(
11529 &self,
11530 table_name: &str,
11531 column_name: &str,
11532 duration_nanos: u64,
11533 ) -> Result<crate::manifest::TtlPolicy> {
11534 let policy = self.replace_table_ttl(table_name, Some((column_name, duration_nanos)))?;
11535 policy.ok_or_else(|| MongrelError::Other("set TTL produced no policy".into()))
11536 }
11537
11538 #[doc(hidden)]
11540 pub fn set_building_table_ttl(
11541 &self,
11542 table_name: &str,
11543 column_name: &str,
11544 duration_nanos: u64,
11545 ) -> Result<crate::manifest::TtlPolicy> {
11546 let policy = self.replace_table_ttl_with_state(
11547 table_name,
11548 Some((column_name, duration_nanos)),
11549 true,
11550 )?;
11551 policy
11552 .ok_or_else(|| MongrelError::Other("set building-table TTL produced no policy".into()))
11553 }
11554
11555 pub fn clear_table_ttl(&self, table_name: &str) -> Result<()> {
11556 self.replace_table_ttl(table_name, None)?;
11557 Ok(())
11558 }
11559
11560 fn replace_table_ttl(
11561 &self,
11562 table_name: &str,
11563 requested: Option<(&str, u64)>,
11564 ) -> Result<Option<crate::manifest::TtlPolicy>> {
11565 self.replace_table_ttl_with_state(table_name, requested, false)
11566 }
11567
11568 fn replace_table_ttl_with_state(
11569 &self,
11570 table_name: &str,
11571 requested: Option<(&str, u64)>,
11572 building: bool,
11573 ) -> Result<Option<crate::manifest::TtlPolicy>> {
11574 use crate::wal::DdlOp;
11575 use std::sync::atomic::Ordering;
11576
11577 self.require(&crate::auth::Permission::Ddl)?;
11578 if self.poisoned.load(Ordering::Relaxed) {
11579 return Err(MongrelError::Other(
11580 "database poisoned by fsync error".into(),
11581 ));
11582 }
11583
11584 let _g = self.ddl_lock.lock();
11585 let _security_write = self.security_write()?;
11586 self.require(&crate::auth::Permission::Ddl)?;
11587 let table_id = {
11588 let cat = self.catalog.read();
11589 if building {
11590 cat.building(table_name)
11591 } else {
11592 cat.live(table_name)
11593 }
11594 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
11595 .table_id
11596 };
11597 let handle =
11598 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
11599 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
11600 })?;
11601 let mut table = handle.lock();
11602 let policy = match requested {
11603 Some((column, duration)) => Some(table.prepare_ttl_policy(column, duration)?),
11604 None => None,
11605 };
11606 if table.ttl() == policy {
11607 return Ok(policy);
11608 }
11609
11610 let commit_lock = Arc::clone(&self.commit_lock);
11611 let _c = commit_lock.lock();
11612 let epoch = self.epoch.bump_assigned();
11613 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
11614 let txn_id = self.alloc_txn_id()?;
11615 let policy_json = DdlOp::encode_ttl(policy)?;
11616 let mut next_catalog = self.catalog.read().clone();
11617 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11618 let commit_seq = {
11619 let mut wal = self.shared_wal.lock();
11620 let append: Result<u64> = (|| {
11621 wal.append(
11622 txn_id,
11623 table_id,
11624 crate::wal::Op::Ddl(DdlOp::SetTtl {
11625 table_id,
11626 policy_json,
11627 }),
11628 )?;
11629 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11630 wal.append_commit(txn_id, epoch, &[])
11631 })();
11632 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11633 };
11634 self.await_durable_commit(commit_seq, epoch)?;
11635
11636 let mut publish_error = table.apply_ttl_policy_at(policy, epoch).err();
11637 drop(table);
11638 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
11639 publish_error.get_or_insert(error);
11640 }
11641 self.finish_durable_publish(epoch, &mut _epoch_guard, publish_error.map_or(Ok(()), Err))?;
11642 Ok(policy)
11643 }
11644
11645 pub fn gc(&self) -> Result<usize> {
11651 let control = crate::ExecutionControl::new(None);
11652 self.gc_controlled(&control, || true)
11653 }
11654
11655 #[doc(hidden)]
11658 pub fn gc_controlled<F>(
11659 &self,
11660 control: &crate::ExecutionControl,
11661 before_publish: F,
11662 ) -> Result<usize>
11663 where
11664 F: FnOnce() -> bool,
11665 {
11666 self.gc_controlled_with_receipt(control, before_publish)
11667 .map(|(reclaimed, _)| reclaimed)
11668 }
11669
11670 #[doc(hidden)]
11673 pub fn gc_controlled_with_receipt<F>(
11674 &self,
11675 control: &crate::ExecutionControl,
11676 before_publish: F,
11677 ) -> Result<(usize, Option<MaintenanceReceipt>)>
11678 where
11679 F: FnOnce() -> bool,
11680 {
11681 enum Candidate {
11682 Directory(PathBuf),
11683 File(PathBuf),
11684 }
11685
11686 self.require(&crate::auth::Permission::Ddl)?;
11687 let _ddl = self.ddl_lock.lock();
11688 self.require(&crate::auth::Permission::Ddl)?;
11689 control.checkpoint()?;
11690 let maintenance_epoch = self.epoch.visible();
11691 let min_active = self.snapshots.min_active(maintenance_epoch).0;
11692 let mut candidates = Vec::new();
11693
11694 let cat = self.catalog.read();
11696 for (entry_index, entry) in cat.tables.iter().enumerate() {
11697 if entry_index % 256 == 0 {
11698 control.checkpoint()?;
11699 }
11700 if let TableState::Dropped { at_epoch } = entry.state {
11701 if at_epoch <= min_active {
11702 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
11703 if tdir.exists() {
11704 candidates.push(Candidate::Directory(tdir));
11705 }
11706 }
11707 }
11708 }
11709 drop(cat);
11710
11711 let cat = self.catalog.read();
11716 for (entry_index, entry) in cat.tables.iter().enumerate() {
11717 if entry_index % 256 == 0 {
11718 control.checkpoint()?;
11719 }
11720 if !matches!(entry.state, TableState::Live) {
11721 continue;
11722 }
11723 let txn_dir = self
11724 .root
11725 .join(TABLES_DIR)
11726 .join(entry.table_id.to_string())
11727 .join("_txn");
11728 if !txn_dir.exists() {
11729 continue;
11730 }
11731 for (sub_index, sub) in std::fs::read_dir(&txn_dir)?.enumerate() {
11732 if sub_index % 256 == 0 {
11733 control.checkpoint()?;
11734 }
11735 let sub = sub?;
11736 let name = sub.file_name();
11737 let Some(name) = name.to_str() else { continue };
11738 let is_active = name
11740 .parse::<u64>()
11741 .map(|id| self.active_spills.is_active(id))
11742 .unwrap_or(false);
11743 if is_active {
11744 continue;
11745 }
11746 candidates.push(Candidate::Directory(sub.path()));
11747 }
11748 }
11749 drop(cat);
11750
11751 let external_names = {
11752 let cat = self.catalog.read();
11753 cat.external_tables
11754 .iter()
11755 .map(|entry| entry.name.clone())
11756 .collect::<std::collections::HashSet<_>>()
11757 };
11758 let vtab_dir = self.root.join(VTAB_DIR);
11759 if vtab_dir.exists() {
11760 for (entry_index, entry) in std::fs::read_dir(&vtab_dir)?.enumerate() {
11761 if entry_index % 256 == 0 {
11762 control.checkpoint()?;
11763 }
11764 let entry = entry?;
11765 let name = entry.file_name();
11766 let Some(name) = name.to_str() else { continue };
11767 if external_names.contains(name) {
11768 continue;
11769 }
11770 let path = entry.path();
11771 if path.is_dir() {
11772 candidates.push(Candidate::Directory(path));
11773 } else {
11774 candidates.push(Candidate::File(path));
11775 }
11776 }
11777 }
11778
11779 let tables = self
11783 .tables
11784 .read()
11785 .iter()
11786 .map(|(table_id, handle)| (*table_id, handle.clone()))
11787 .collect::<Vec<_>>();
11788 let mut retiring = Vec::new();
11789 for (table_index, (table_id, handle)) in tables.iter().enumerate() {
11790 if table_index % 256 == 0 {
11791 control.checkpoint()?;
11792 }
11793 let backup_pinned: HashSet<u128> = self
11794 .backup_pins
11795 .lock()
11796 .keys()
11797 .filter_map(|(pinned_table, run_id)| {
11798 (*pinned_table == *table_id).then_some(*run_id)
11799 })
11800 .collect();
11801 if handle
11802 .lock()
11803 .has_reapable_retiring(Epoch(min_active), &backup_pinned)
11804 {
11805 retiring.push((handle.clone(), backup_pinned));
11806 }
11807 }
11808
11809 let all_durable = self.active_spills.is_idle()
11817 && tables.iter().all(|(_, handle)| {
11818 let g = handle.lock();
11819 g.memtable_len() == 0 && g.mutable_run_len() == 0
11820 });
11821 let retain = self
11822 .replication_wal_retention_segments
11823 .load(std::sync::atomic::Ordering::Relaxed);
11824 let reap_wal = all_durable
11825 && self
11826 .shared_wal
11827 .lock()
11828 .has_gc_segments_retain_recent(retain)?;
11829
11830 if candidates.is_empty() && retiring.is_empty() && !reap_wal {
11831 return Ok((0, None));
11832 }
11833 control.checkpoint()?;
11834 if !before_publish() {
11835 return Err(MongrelError::Cancelled);
11836 }
11837
11838 let mut reclaimed = 0;
11839 for candidate in candidates {
11840 match candidate {
11841 Candidate::Directory(path) => std::fs::remove_dir_all(path)?,
11842 Candidate::File(path) => std::fs::remove_file(path)?,
11843 }
11844 reclaimed += 1;
11845 }
11846 for (handle, backup_pinned) in retiring {
11847 reclaimed += handle
11848 .lock()
11849 .reap_retiring(Epoch(min_active), &backup_pinned)?;
11850 }
11851 if reap_wal {
11852 reclaimed += self
11853 .shared_wal
11854 .lock()
11855 .gc_segments_retain_recent(u64::MAX, retain)?;
11856 }
11857
11858 Ok((
11859 reclaimed,
11860 Some(MaintenanceReceipt {
11861 epoch: maintenance_epoch,
11862 }),
11863 ))
11864 }
11865
11866 pub fn checkpoint(&self) -> Result<()> {
11884 self.checkpoint_controlled(|| Ok(()))
11885 }
11886
11887 #[doc(hidden)]
11890 pub fn checkpoint_controlled<F>(&self, before_wal_reset: F) -> Result<()>
11891 where
11892 F: FnOnce() -> Result<()>,
11893 {
11894 self.require(&crate::auth::Permission::Ddl)?;
11895 let _replication = self.replication_barrier.write();
11899 let _ddl = self.ddl_lock.lock();
11900 let _security = self.security_coordinator.gate.read();
11901 self.require(&crate::auth::Permission::Ddl)?;
11902
11903 let mut handles = self
11904 .tables
11905 .read()
11906 .iter()
11907 .map(|(table_id, handle)| (*table_id, handle.clone()))
11908 .collect::<Vec<_>>();
11909 handles.sort_by_key(|(table_id, _)| *table_id);
11910 let mut tables = handles
11911 .iter()
11912 .map(|(table_id, handle)| (*table_id, handle.lock()))
11913 .collect::<Vec<_>>();
11914
11915 for (_, table) in &mut tables {
11917 if table.has_pending_writes() || table.memtable_len() > 0 || table.mutable_run_len() > 0
11918 {
11919 table.force_flush()?;
11920 }
11921 }
11922
11923 for (_, table) in &mut tables {
11926 if table.run_count() >= 2 || table.should_compact() {
11927 table.compact()?;
11928 }
11929 }
11930
11931 before_wal_reset()?;
11932
11933 let maintenance_epoch = self.epoch.visible();
11935 let min_active = self.snapshots.min_active(maintenance_epoch);
11936 for (table_id, table) in &mut tables {
11937 let backup_pinned: HashSet<u128> = self
11938 .backup_pins
11939 .lock()
11940 .keys()
11941 .filter_map(|(pinned_table, run_id)| {
11942 (*pinned_table == *table_id).then_some(*run_id)
11943 })
11944 .collect();
11945 table.reap_retiring(min_active, &backup_pinned)?;
11946 }
11947
11948 self.shared_wal.lock().reset_after_checkpoint()?;
11951
11952 let catalog_snapshot = self.catalog.read().clone();
11954 for entry in &catalog_snapshot.tables {
11955 if matches!(entry.state, TableState::Dropped { at_epoch } if at_epoch <= min_active.0) {
11956 crate::durable_file::remove_directory_all(
11957 &self.root.join(TABLES_DIR).join(entry.table_id.to_string()),
11958 )?;
11959 }
11960 if !matches!(entry.state, TableState::Live) {
11961 continue;
11962 }
11963 let transaction_dir = self
11964 .root
11965 .join(TABLES_DIR)
11966 .join(entry.table_id.to_string())
11967 .join("_txn");
11968 if transaction_dir.is_dir() {
11969 for child in std::fs::read_dir(&transaction_dir)? {
11970 let child = child?;
11971 let active = child
11972 .file_name()
11973 .to_str()
11974 .and_then(|name| name.parse::<u64>().ok())
11975 .is_some_and(|txn_id| self.active_spills.is_active(txn_id));
11976 if !active {
11977 crate::durable_file::remove_directory_all(&child.path())?;
11978 }
11979 }
11980 }
11981 }
11982 let external_names = catalog_snapshot
11983 .external_tables
11984 .iter()
11985 .map(|entry| entry.name.as_str())
11986 .collect::<HashSet<_>>();
11987 let external_root = self.root.join(VTAB_DIR);
11988 if external_root.is_dir() {
11989 for entry in std::fs::read_dir(&external_root)? {
11990 let entry = entry?;
11991 let name = entry.file_name();
11992 if name
11993 .to_str()
11994 .is_some_and(|name| external_names.contains(name))
11995 {
11996 continue;
11997 }
11998 if entry.file_type()?.is_dir() {
11999 crate::durable_file::remove_directory_all(&entry.path())?;
12000 } else {
12001 std::fs::remove_file(entry.path())?;
12002 crate::durable_file::sync_directory(&external_root)?;
12003 }
12004 }
12005 }
12006
12007 catalog::write_atomic(&self.root, &catalog_snapshot, self.meta_dek.as_ref())?;
12010 let visible = self.epoch.visible();
12011 for (_, table) in &tables {
12012 table.persist_manifest(visible)?;
12013 }
12014
12015 Ok(())
12016 }
12017 fn alloc_txn_id(&self) -> Result<u64> {
12018 crate::txn::allocate_txn_id(&self.next_txn_id)
12019 }
12020
12021 pub fn set_spill_threshold(&self, bytes: u64) {
12025 self.spill_threshold
12026 .store(bytes, std::sync::atomic::Ordering::Relaxed);
12027 }
12028
12029 #[doc(hidden)]
12033 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12034 *self.spill_hook.lock() = Some(Box::new(f));
12035 }
12036
12037 #[doc(hidden)]
12040 pub fn __set_security_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12041 *self.security_commit_hook.lock() = Some(Box::new(f));
12042 }
12043
12044 #[doc(hidden)]
12047 pub fn __set_catalog_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12048 *self.catalog_commit_hook.lock() = Some(Box::new(f));
12049 }
12050
12051 #[doc(hidden)]
12054 pub fn __set_backup_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12055 *self.backup_hook.lock() = Some(Box::new(f));
12056 }
12057
12058 #[doc(hidden)]
12060 pub fn __set_replication_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12061 *self.replication_hook.lock() = Some(Box::new(f));
12062 }
12063
12064 #[doc(hidden)]
12068 pub fn __wal_group_sync_count(&self) -> u64 {
12069 self.shared_wal.lock().group_sync_count()
12070 }
12071
12072 #[doc(hidden)]
12075 pub fn __poison(&self) {
12076 self.poisoned
12077 .store(true, std::sync::atomic::Ordering::Relaxed);
12078 }
12079
12080 pub fn check(&self) -> Vec<CheckIssue> {
12093 match self.check_inner(None) {
12094 Ok(issues) => issues,
12095 Err(error) => vec![CheckIssue {
12096 table_id: WAL_TABLE_ID,
12097 table_name: "shared WAL".into(),
12098 severity: "error".into(),
12099 description: error.to_string(),
12100 }],
12101 }
12102 }
12103
12104 #[doc(hidden)]
12106 pub fn check_controlled(&self, control: &crate::ExecutionControl) -> Result<Vec<CheckIssue>> {
12107 self.check_inner(Some(control))
12108 }
12109
12110 fn check_inner(&self, control: Option<&crate::ExecutionControl>) -> Result<Vec<CheckIssue>> {
12111 let mut issues = Vec::new();
12112 let cat = self.catalog.read();
12113 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
12114 for (table_index, entry) in cat.tables.iter().enumerate() {
12115 if table_index % 256 == 0 {
12116 if let Some(control) = control {
12117 control.checkpoint()?;
12118 }
12119 }
12120 if !matches!(entry.state, TableState::Live) {
12121 continue;
12122 }
12123 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
12124 let mut err = |sev: &str, desc: String| {
12125 issues.push(CheckIssue {
12126 table_id: entry.table_id,
12127 table_name: entry.name.clone(),
12128 severity: sev.into(),
12129 description: desc,
12130 });
12131 };
12132 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
12133 Ok(m) => m,
12134 Err(e) => {
12135 err("error", format!("manifest read failed: {e}"));
12136 continue;
12137 }
12138 };
12139 if m.flushed_epoch > m.current_epoch {
12140 err(
12141 "error",
12142 format!(
12143 "flushed_epoch {} exceeds current_epoch {} (impossible)",
12144 m.flushed_epoch, m.current_epoch
12145 ),
12146 );
12147 }
12148
12149 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
12150 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
12151 for (run_index, rr) in m.runs.iter().enumerate() {
12152 if run_index % 256 == 0 {
12153 if let Some(control) = control {
12154 control.checkpoint()?;
12155 }
12156 }
12157 referenced.insert(rr.run_id);
12158 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
12159 if !run_path.exists() {
12160 err("error", format!("missing run file: r-{}.sr", rr.run_id));
12161 continue;
12162 }
12163 match crate::sorted_run::RunReader::open(
12164 &run_path,
12165 entry.schema.clone(),
12166 self.kek.clone(),
12167 ) {
12168 Ok(reader) => {
12169 if reader.row_count() as u64 != rr.row_count {
12170 err(
12171 "error",
12172 format!(
12173 "run r-{} row count mismatch: manifest {} vs run {}",
12174 rr.run_id,
12175 rr.row_count,
12176 reader.row_count()
12177 ),
12178 );
12179 }
12180 }
12181 Err(e) => {
12182 err(
12183 "error",
12184 format!("run r-{} integrity check failed: {e}", rr.run_id),
12185 );
12186 }
12187 }
12188 }
12189
12190 for r in &m.retiring {
12194 referenced.insert(r.run_id);
12195 }
12196
12197 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
12199 for (entry_index, ent) in rd.flatten().enumerate() {
12200 if entry_index % 256 == 0 {
12201 if let Some(control) = control {
12202 control.checkpoint()?;
12203 }
12204 }
12205 let p = ent.path();
12206 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
12207 continue;
12208 }
12209 let run_id = p
12210 .file_stem()
12211 .and_then(|s| s.to_str())
12212 .and_then(|s| s.strip_prefix("r-"))
12213 .and_then(|s| s.parse::<u128>().ok());
12214 if let Some(id) = run_id {
12215 if !referenced.contains(&id) {
12216 err(
12217 "warning",
12218 format!("orphan run file r-{id}.sr not referenced by the manifest"),
12219 );
12220 }
12221 }
12222 }
12223 }
12224 }
12225
12226 let external_names = cat
12227 .external_tables
12228 .iter()
12229 .map(|entry| entry.name.clone())
12230 .collect::<std::collections::HashSet<_>>();
12231 let vtab_dir = self.root.join(VTAB_DIR);
12232 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
12233 for (entry_index, entry) in entries.flatten().enumerate() {
12234 if entry_index % 256 == 0 {
12235 if let Some(control) = control {
12236 control.checkpoint()?;
12237 }
12238 }
12239 let name = entry.file_name();
12240 let Some(name) = name.to_str() else { continue };
12241 if !external_names.contains(name) {
12242 issues.push(CheckIssue {
12243 table_id: EXTERNAL_TABLE_ID,
12244 table_name: name.to_string(),
12245 severity: "warning".into(),
12246 description: format!(
12247 "orphan external table state entry {:?} not referenced by the catalog",
12248 entry.path()
12249 ),
12250 });
12251 }
12252 }
12253 }
12254
12255 if let Some(control) = control {
12262 control.checkpoint()?;
12263 }
12264 for (seg, msg) in self.shared_wal.lock().verify_segments() {
12265 issues.push(CheckIssue {
12266 table_id: WAL_TABLE_ID,
12267 table_name: "<wal>".into(),
12268 severity: "error".into(),
12269 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
12270 });
12271 }
12272 Ok(issues)
12273 }
12274
12275 pub fn doctor(&self) -> Result<Vec<u64>> {
12279 let control = crate::ExecutionControl::new(None);
12280 self.doctor_controlled(&control, || true)
12281 }
12282
12283 #[doc(hidden)]
12287 pub fn doctor_controlled<F>(
12288 &self,
12289 control: &crate::ExecutionControl,
12290 before_publish: F,
12291 ) -> Result<Vec<u64>>
12292 where
12293 F: FnOnce() -> bool,
12294 {
12295 self.doctor_controlled_with_receipt(control, before_publish)
12296 .map(|(quarantined, _)| quarantined)
12297 }
12298
12299 #[doc(hidden)]
12302 pub fn doctor_controlled_with_receipt<F>(
12303 &self,
12304 control: &crate::ExecutionControl,
12305 before_publish: F,
12306 ) -> Result<(Vec<u64>, Option<MaintenanceReceipt>)>
12307 where
12308 F: FnOnce() -> bool,
12309 {
12310 let _ddl = self.ddl_lock.lock();
12313 let _security_write = self.security_write()?;
12314 let issues = self.check_inner(Some(control))?;
12315 let bad_tables: std::collections::HashSet<u64> = issues
12320 .iter()
12321 .filter(|i| {
12322 i.severity == "error"
12323 && i.table_id != WAL_TABLE_ID
12324 && i.table_id != EXTERNAL_TABLE_ID
12325 })
12326 .map(|i| i.table_id)
12327 .collect();
12328 if bad_tables.is_empty() {
12329 return Ok((Vec::new(), None));
12330 }
12331 let _commit = self.commit_lock.lock();
12332 control.checkpoint()?;
12333 if !before_publish() {
12334 return Err(MongrelError::Cancelled);
12335 }
12336 let maintenance_epoch = self.epoch.bump_assigned();
12337 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), maintenance_epoch);
12338
12339 let qdir = self.root.join("_quarantine");
12340 crate::durable_file::create_directory(&qdir)?;
12341 let mut bad_tables = bad_tables.into_iter().collect::<Vec<_>>();
12342 bad_tables.sort_unstable();
12343
12344 let mut handles = self
12348 .tables
12349 .read()
12350 .iter()
12351 .filter(|(table_id, _)| bad_tables.binary_search(table_id).is_ok())
12352 .map(|(table_id, handle)| (*table_id, handle.clone()))
12353 .collect::<Vec<_>>();
12354 handles.sort_by_key(|(table_id, _)| *table_id);
12355 let mut table_guards = handles
12356 .iter()
12357 .map(|(table_id, handle)| (*table_id, handle.lock()))
12358 .collect::<Vec<_>>();
12359
12360 let mut next_catalog = self.catalog.read().clone();
12361 for table_id in &bad_tables {
12362 if let Some(entry) = next_catalog
12363 .tables
12364 .iter_mut()
12365 .find(|entry| entry.table_id == *table_id)
12366 {
12367 entry.state = TableState::Dropped {
12368 at_epoch: maintenance_epoch.0,
12369 };
12370 }
12371 }
12372 next_catalog.db_epoch = next_catalog.db_epoch.max(maintenance_epoch.0);
12373
12374 let txn_id = self.alloc_txn_id()?;
12375 let commit_seq = {
12376 let mut wal = self.shared_wal.lock();
12377 let append: Result<u64> = (|| {
12378 for table_id in &bad_tables {
12379 wal.append(
12380 txn_id,
12381 *table_id,
12382 crate::wal::Op::Ddl(crate::wal::DdlOp::DropTable {
12383 table_id: *table_id,
12384 }),
12385 )?;
12386 }
12387 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
12388 wal.append_commit(txn_id, maintenance_epoch, &[])
12389 })();
12390 append.map_err(|error| self.commit_outcome_unknown(maintenance_epoch, error))?
12391 };
12392 self.await_durable_commit(commit_seq, maintenance_epoch)?;
12393 for (_, table) in &mut table_guards {
12394 table.mark_unavailable_after_quarantine();
12395 }
12396 {
12397 let mut live_tables = self.tables.write();
12398 for table_id in &bad_tables {
12399 live_tables.remove(table_id);
12400 }
12401 }
12402 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
12403 self.finish_durable_publish(maintenance_epoch, &mut epoch_guard, checkpoint)?;
12404
12405 for table_id in &bad_tables {
12409 let source = self.root.join(TABLES_DIR).join(table_id.to_string());
12410 if source.exists() {
12411 let destination = qdir.join(table_id.to_string());
12412 if let Err(error) = crate::durable_file::rename(&source, &destination) {
12413 return Err(MongrelError::DurableCommit {
12414 epoch: maintenance_epoch.0,
12415 message: format!(
12416 "DOCTOR dropped table {table_id} but quarantine move failed: {error}"
12417 ),
12418 });
12419 }
12420 }
12421 }
12422 Ok((
12423 bad_tables,
12424 Some(MaintenanceReceipt {
12425 epoch: maintenance_epoch,
12426 }),
12427 ))
12428 }
12429
12430 #[allow(dead_code)]
12432 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
12433 self.kek.as_ref()
12434 }
12435
12436 #[allow(dead_code)]
12438 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
12439 &self.epoch
12440 }
12441
12442 #[allow(dead_code)]
12444 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
12445 &self.snapshots
12446 }
12447}
12448
12449fn external_state_dir(root: &Path, name: &str) -> PathBuf {
12450 root.join(VTAB_DIR).join(name)
12451}
12452
12453fn append_catalog_snapshot(
12454 wal: &mut crate::wal::SharedWal,
12455 txn_id: u64,
12456 catalog: &Catalog,
12457) -> Result<()> {
12458 let catalog_json = crate::wal::DdlOp::encode_catalog(catalog)?;
12459 wal.append(
12460 txn_id,
12461 WAL_TABLE_ID,
12462 crate::wal::Op::Ddl(crate::wal::DdlOp::CatalogSnapshot { catalog_json }),
12463 )?;
12464 Ok(())
12465}
12466
12467fn filter_ignored_staging(
12468 staging: Vec<(u64, crate::txn::Staged)>,
12469 ignored_indices: &std::collections::BTreeSet<usize>,
12470) -> Vec<(u64, crate::txn::Staged)> {
12471 if ignored_indices.is_empty() {
12472 return staging;
12473 }
12474 staging
12475 .into_iter()
12476 .enumerate()
12477 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
12478 .collect()
12479}
12480
12481fn external_state_file(root: &Path, name: &str) -> PathBuf {
12482 external_state_dir(root, name).join("state.json")
12483}
12484
12485fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
12486 let path = external_state_file(root, name);
12487 match std::fs::read(path) {
12488 Ok(bytes) => Ok(bytes),
12489 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
12490 Err(e) => Err(e.into()),
12491 }
12492}
12493
12494fn current_external_state_bytes(
12495 root: &Path,
12496 external_states: &[(String, Vec<u8>)],
12497 name: &str,
12498) -> Result<Vec<u8>> {
12499 for (table, state) in external_states.iter().rev() {
12500 if table == name {
12501 return Ok(state.clone());
12502 }
12503 }
12504 read_external_state_file(root, name)
12505}
12506
12507fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
12508 let mut out = external_states;
12509 dedup_external_states_in_place(&mut out);
12510 out
12511}
12512
12513fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
12514 let mut seen = std::collections::HashSet::new();
12515 let mut out = Vec::with_capacity(external_states.len());
12516 for (name, state) in std::mem::take(external_states).into_iter().rev() {
12517 if seen.insert(name.clone()) {
12518 out.push((name, state));
12519 }
12520 }
12521 out.reverse();
12522 *external_states = out;
12523}
12524
12525fn prepare_external_state_file(
12526 root: &Path,
12527 name: &str,
12528 state: &[u8],
12529 txn_id: u64,
12530) -> Result<PathBuf> {
12531 crate::durable_file::create_directory(&root.join(VTAB_DIR))?;
12532 let dir = external_state_dir(root, name);
12533 crate::durable_file::create_directory(&dir)?;
12534 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
12535 {
12536 let mut file = std::fs::OpenOptions::new()
12537 .create_new(true)
12538 .write(true)
12539 .open(&pending)?;
12540 file.write_all(state)?;
12541 file.sync_all()?;
12542 }
12543 Ok(pending)
12544}
12545
12546fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
12547 let path = external_state_file(root, name);
12548 crate::durable_file::replace(pending, &path)?;
12549 Ok(())
12550}
12551
12552fn write_external_state_file(
12553 durable: &crate::durable_file::DurableRoot,
12554 name: &str,
12555 state: &[u8],
12556) -> Result<()> {
12557 let directory = Path::new(VTAB_DIR).join(name);
12558 durable.create_directory_all(&directory)?;
12559 durable.write_atomic(directory.join("state.json"), state)?;
12560 Ok(())
12561}
12562
12563fn validate_recovered_data_table(
12564 catalog: &Catalog,
12565 tables: &HashMap<u64, TableHandle>,
12566 table_id: u64,
12567 commit_epoch: u64,
12568 offset: u64,
12569) -> Result<bool> {
12570 let entry = catalog
12571 .tables
12572 .iter()
12573 .find(|entry| entry.table_id == table_id)
12574 .ok_or_else(|| MongrelError::CorruptWal {
12575 offset,
12576 reason: format!("committed record references unknown table {table_id}"),
12577 })?;
12578 if commit_epoch < entry.created_epoch {
12579 return Err(MongrelError::CorruptWal {
12580 offset,
12581 reason: format!(
12582 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
12583 entry.created_epoch
12584 ),
12585 });
12586 }
12587 match entry.state {
12588 TableState::Dropped { at_epoch } => {
12589 let abandoned_build_boundary =
12594 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
12595 if commit_epoch >= at_epoch && !abandoned_build_boundary {
12596 Err(MongrelError::CorruptWal {
12597 offset,
12598 reason: format!(
12599 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
12600 ),
12601 })
12602 } else {
12603 Ok(false)
12604 }
12605 }
12606 TableState::Live | TableState::Building { .. } => {
12607 if tables.contains_key(&table_id) {
12608 Ok(true)
12609 } else {
12610 Err(MongrelError::CorruptWal {
12611 offset,
12612 reason: format!("live table {table_id} has no mounted recovery handle"),
12613 })
12614 }
12615 }
12616 }
12617}
12618
12619type RecoveryTableStage = (
12620 Vec<crate::memtable::Row>,
12621 Vec<(crate::rowid::RowId, Epoch)>,
12622 Option<Epoch>,
12623 Epoch,
12624);
12625
12626#[derive(Clone)]
12627struct RecoveryValidationTable {
12628 schema: Schema,
12629 flushed_epoch: u64,
12630}
12631
12632fn validate_shared_wal_recovery_plan(
12633 durable_root: &crate::durable_file::DurableRoot,
12634 catalog: &Catalog,
12635 recovered_table_ids: &HashSet<u64>,
12636 reconciled_table_ids: &HashSet<u64>,
12637 meta_dek: Option<&[u8; META_DEK_LEN]>,
12638 kek: Option<Arc<crate::encryption::Kek>>,
12639 records: &[crate::wal::Record],
12640) -> Result<()> {
12641 use crate::wal::{DdlOp, Op};
12642
12643 let mut tables = HashMap::<u64, RecoveryValidationTable>::new();
12644 for entry in &catalog.tables {
12645 if !matches!(entry.state, TableState::Live) {
12646 continue;
12647 }
12648 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
12649 let manifest = match crate::manifest::read_durable(durable_root, &relative_dir, meta_dek) {
12650 Ok(manifest) => Some(manifest),
12651 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
12652 Err(error) => return Err(error),
12653 };
12654 let flushed_epoch = if let Some(manifest) = manifest {
12655 if manifest.table_id != entry.table_id {
12656 return Err(MongrelError::Conflict(format!(
12657 "catalog table {} storage identity mismatch",
12658 entry.table_id
12659 )));
12660 }
12661 if (manifest.schema_id != entry.schema.schema_id
12662 && !reconciled_table_ids.contains(&entry.table_id))
12663 || manifest.flushed_epoch > manifest.current_epoch
12664 || manifest.global_idx_epoch > manifest.current_epoch
12665 || manifest.next_row_id == u64::MAX
12666 || manifest.auto_inc_next < 0
12667 || manifest.auto_inc_next == i64::MAX
12668 || (entry.schema.auto_increment_column().is_none() && manifest.auto_inc_next != 0)
12669 {
12670 return Err(MongrelError::InvalidArgument(format!(
12671 "table {} manifest counters or schema identity are invalid",
12672 entry.table_id
12673 )));
12674 }
12675 #[cfg(feature = "encryption")]
12676 let idx_dek = kek.as_ref().map(|key| key.derive_idx_key());
12677 #[cfg(not(feature = "encryption"))]
12678 let idx_dek: Option<zeroize::Zeroizing<[u8; 32]>> = None;
12679 crate::global_idx::read_durable_for(
12680 durable_root,
12681 &relative_dir,
12682 entry.table_id,
12683 &entry.schema,
12684 idx_dek.as_deref(),
12685 )?;
12686 let mut run_ids = HashSet::new();
12687 let mut maximum_row_id = None::<u64>;
12688 for run in &manifest.runs {
12689 if run.run_id >= u64::MAX as u128
12690 || run.epoch_created > manifest.current_epoch
12691 || !run_ids.insert(run.run_id)
12692 {
12693 return Err(MongrelError::InvalidArgument(format!(
12694 "table {} manifest contains an invalid or duplicate run id",
12695 entry.table_id
12696 )));
12697 }
12698 let relative = relative_dir
12699 .join(crate::engine::RUNS_DIR)
12700 .join(format!("r-{}.sr", run.run_id as u64));
12701 let file = durable_root.open_regular(&relative)?;
12702 let mut reader = crate::sorted_run::RunReader::open_file(
12703 file,
12704 entry.schema.clone(),
12705 kek.clone(),
12706 )?;
12707 let header = reader.header();
12708 if header.run_id != run.run_id
12709 || header.level != run.level
12710 || header.row_count != run.row_count
12711 || !header.is_uniform_epoch() && header.epoch_created != run.epoch_created
12712 || header.is_uniform_epoch() && header.epoch_created != 0
12713 || header.schema_id > entry.schema.schema_id
12714 {
12715 return Err(MongrelError::InvalidArgument(format!(
12716 "table {} run {} differs from its manifest: header=(id {}, level {}, rows {}, epoch {}, schema {}), manifest=(id {}, level {}, rows {}, epoch {}, schema <= {})",
12717 entry.table_id,
12718 run.run_id,
12719 header.run_id,
12720 header.level,
12721 header.row_count,
12722 header.epoch_created,
12723 header.schema_id,
12724 run.run_id,
12725 run.level,
12726 run.row_count,
12727 run.epoch_created,
12728 entry.schema.schema_id,
12729 )));
12730 }
12731 if header.row_count != 0 {
12732 maximum_row_id = Some(
12733 maximum_row_id
12734 .map_or(header.max_row_id, |value| value.max(header.max_row_id)),
12735 );
12736 }
12737 reader.validate_all_pages()?;
12738 }
12739 if maximum_row_id.is_some_and(|maximum| manifest.next_row_id <= maximum) {
12740 return Err(MongrelError::InvalidArgument(format!(
12741 "table {} next_row_id does not advance beyond persisted rows",
12742 entry.table_id
12743 )));
12744 }
12745 for run in &manifest.retiring {
12746 if run.run_id >= u64::MAX as u128
12747 || run.retire_epoch > manifest.current_epoch
12748 || !run_ids.insert(run.run_id)
12749 {
12750 return Err(MongrelError::InvalidArgument(format!(
12751 "table {} manifest contains an invalid or aliased retired run",
12752 entry.table_id
12753 )));
12754 }
12755 }
12756 manifest.flushed_epoch
12757 } else {
12758 if !recovered_table_ids.contains(&entry.table_id) {
12759 return Err(MongrelError::NotFound(format!(
12760 "live table {} manifest is missing",
12761 entry.table_id
12762 )));
12763 }
12764 0
12765 };
12766 tables.insert(
12767 entry.table_id,
12768 RecoveryValidationTable {
12769 schema: entry.schema.clone(),
12770 flushed_epoch,
12771 },
12772 );
12773 }
12774
12775 let committed = records
12776 .iter()
12777 .filter_map(|record| match record.op {
12778 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
12779 _ => None,
12780 })
12781 .collect::<HashMap<_, _>>();
12782 let mut run_ids = HashSet::new();
12783 let mut recovered_row_ids = HashMap::<u64, HashSet<u64>>::new();
12784 for record in records {
12785 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
12786 continue;
12787 };
12788 match &record.op {
12789 Op::Put { table_id, rows } => {
12790 let table = validate_recovery_data_table_plan(
12791 catalog,
12792 &tables,
12793 *table_id,
12794 commit_epoch,
12795 record.seq.0,
12796 )?;
12797 let decoded: Vec<crate::memtable::Row> =
12798 bincode::deserialize(rows).map_err(|error| MongrelError::CorruptWal {
12799 offset: record.seq.0,
12800 reason: format!(
12801 "committed Put payload for transaction {} could not be decoded: {error}",
12802 record.txn_id
12803 ),
12804 })?;
12805 if let Some(table) = table {
12806 for row in &decoded {
12807 if !recovered_row_ids
12808 .entry(*table_id)
12809 .or_default()
12810 .insert(row.row_id.0)
12811 {
12812 return Err(MongrelError::CorruptWal {
12813 offset: record.seq.0,
12814 reason: format!(
12815 "committed WAL repeats recovered row id {} for table {table_id}",
12816 row.row_id.0
12817 ),
12818 });
12819 }
12820 validate_recovered_row(&table.schema, row)?;
12821 }
12822 }
12823 }
12824 Op::Delete { table_id, .. } | Op::TruncateTable { table_id } => {
12825 validate_recovery_data_table_plan(
12826 catalog,
12827 &tables,
12828 *table_id,
12829 commit_epoch,
12830 record.seq.0,
12831 )?;
12832 }
12833 Op::ExternalTableState { name, .. } => validate_recovered_external_name(name)?,
12834 Op::Ddl(DdlOp::ResetExternalTableState {
12835 name,
12836 generation_epoch,
12837 }) => {
12838 if *generation_epoch != commit_epoch {
12839 return Err(MongrelError::CorruptWal {
12840 offset: record.seq.0,
12841 reason: format!(
12842 "external state reset epoch {generation_epoch} does not match WAL commit epoch {commit_epoch}"
12843 ),
12844 });
12845 }
12846 validate_recovered_external_name(name)?;
12847 }
12848 Op::TxnCommit { added_runs, .. } => {
12849 for added in added_runs {
12850 let Some(table) = validate_recovery_data_table_plan(
12851 catalog,
12852 &tables,
12853 added.table_id,
12854 commit_epoch,
12855 record.seq.0,
12856 )?
12857 else {
12858 continue;
12859 };
12860 if added.run_id >= u64::MAX as u128
12861 || !run_ids.insert((added.table_id, added.run_id))
12862 {
12863 return Err(MongrelError::CorruptWal {
12864 offset: record.seq.0,
12865 reason: format!(
12866 "duplicate or invalid recovered run {} for table {}",
12867 added.run_id, added.table_id
12868 ),
12869 });
12870 }
12871 if commit_epoch <= table.flushed_epoch {
12872 continue;
12873 }
12874 validate_planned_spilled_run(
12875 durable_root,
12876 record.txn_id,
12877 commit_epoch,
12878 added,
12879 &table.schema,
12880 kek.clone(),
12881 )?;
12882 }
12883 }
12884 _ => {}
12885 }
12886 }
12887 Ok(())
12888}
12889
12890fn validate_recovery_data_table_plan<'a>(
12891 catalog: &Catalog,
12892 tables: &'a HashMap<u64, RecoveryValidationTable>,
12893 table_id: u64,
12894 commit_epoch: u64,
12895 offset: u64,
12896) -> Result<Option<&'a RecoveryValidationTable>> {
12897 let entry = catalog
12898 .tables
12899 .iter()
12900 .find(|entry| entry.table_id == table_id)
12901 .ok_or_else(|| MongrelError::CorruptWal {
12902 offset,
12903 reason: format!("committed record references unknown table {table_id}"),
12904 })?;
12905 if commit_epoch < entry.created_epoch {
12906 return Err(MongrelError::CorruptWal {
12907 offset,
12908 reason: format!(
12909 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
12910 entry.created_epoch
12911 ),
12912 });
12913 }
12914 match entry.state {
12915 TableState::Dropped { at_epoch } => {
12916 let abandoned =
12917 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
12918 if commit_epoch >= at_epoch && !abandoned {
12919 return Err(MongrelError::CorruptWal {
12920 offset,
12921 reason: format!(
12922 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
12923 ),
12924 });
12925 }
12926 Ok(None)
12927 }
12928 TableState::Live => {
12929 tables
12930 .get(&table_id)
12931 .map(Some)
12932 .ok_or_else(|| MongrelError::CorruptWal {
12933 offset,
12934 reason: format!("live table {table_id} has no recovery plan"),
12935 })
12936 }
12937 TableState::Building { .. } => Err(MongrelError::CorruptWal {
12938 offset,
12939 reason: format!("building table {table_id} was not normalized before recovery"),
12940 }),
12941 }
12942}
12943
12944fn validate_planned_spilled_run(
12945 root: &crate::durable_file::DurableRoot,
12946 txn_id: u64,
12947 commit_epoch: u64,
12948 added: &crate::wal::AddedRun,
12949 schema: &Schema,
12950 kek: Option<Arc<crate::encryption::Kek>>,
12951) -> Result<()> {
12952 let table = Path::new(TABLES_DIR).join(added.table_id.to_string());
12953 let destination = table
12954 .join(crate::engine::RUNS_DIR)
12955 .join(format!("r-{}.sr", added.run_id as u64));
12956 let pending = table
12957 .join("_txn")
12958 .join(txn_id.to_string())
12959 .join(format!("r-{}.sr", added.run_id as u64));
12960 let file = match root.open_regular(&destination) {
12961 Ok(file) => file,
12962 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
12963 root.open_regular(&pending).map_err(|pending_error| {
12964 if pending_error.kind() == std::io::ErrorKind::NotFound {
12965 MongrelError::CorruptWal {
12966 offset: commit_epoch,
12967 reason: format!(
12968 "committed spilled run {} for transaction {txn_id} is missing",
12969 added.run_id
12970 ),
12971 }
12972 } else {
12973 pending_error.into()
12974 }
12975 })?
12976 }
12977 Err(error) => return Err(error.into()),
12978 };
12979 let mut reader = crate::sorted_run::RunReader::open_file(file, schema.clone(), kek)?;
12980 let header = reader.header();
12981 if header.run_id != added.run_id
12982 || header.content_hash != added.content_hash
12983 || header.row_count != added.row_count
12984 || header.level != added.level
12985 || header.min_row_id != added.min_row_id
12986 || header.max_row_id != added.max_row_id
12987 || header.schema_id != schema.schema_id
12988 || !header.is_uniform_epoch()
12989 || header.epoch_created != 0
12990 {
12991 return Err(MongrelError::CorruptWal {
12992 offset: commit_epoch,
12993 reason: format!(
12994 "committed spilled run {} metadata differs from WAL",
12995 added.run_id
12996 ),
12997 });
12998 }
12999 reader.validate_all_pages()?;
13000 Ok(())
13001}
13002
13003fn recover_shared_wal(
13012 durable_root: &crate::durable_file::DurableRoot,
13013 tables: &HashMap<u64, TableHandle>,
13014 catalog: &Catalog,
13015 epoch: &EpochAuthority,
13016 records: &[crate::wal::Record],
13017) -> Result<()> {
13018 use crate::memtable::Row;
13019 use crate::wal::{DdlOp, Op};
13020
13021 let mut committed: HashMap<u64, u64> = HashMap::new();
13023 let mut spilled_to_link: Vec<(
13024 u64, u64, Vec<crate::wal::AddedRun>,
13027 )> = Vec::new();
13028 for r in records {
13029 if let Op::TxnCommit {
13030 epoch: ce,
13031 ref added_runs,
13032 } = r.op
13033 {
13034 committed.insert(r.txn_id, ce);
13035 if !added_runs.is_empty() {
13036 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
13037 }
13038 }
13039 }
13040 for record in records {
13041 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
13042 continue;
13043 };
13044 match &record.op {
13045 Op::Put { table_id, .. }
13046 | Op::Delete { table_id, .. }
13047 | Op::TruncateTable { table_id } => {
13048 validate_recovered_data_table(
13049 catalog,
13050 tables,
13051 *table_id,
13052 commit_epoch,
13053 record.seq.0,
13054 )?;
13055 }
13056 Op::TxnCommit { added_runs, .. } => {
13057 for run in added_runs {
13058 validate_recovered_data_table(
13059 catalog,
13060 tables,
13061 run.table_id,
13062 commit_epoch,
13063 record.seq.0,
13064 )?;
13065 }
13066 }
13067 _ => {}
13068 }
13069 }
13070 let truncated_transactions: HashSet<(u64, u64)> = records
13071 .iter()
13072 .filter_map(|record| {
13073 committed.get(&record.txn_id)?;
13074 match record.op {
13075 Op::TruncateTable { table_id } => Some((record.txn_id, table_id)),
13076 _ => None,
13077 }
13078 })
13079 .collect();
13080
13081 enum ExternalRecoveryAction {
13083 Write { name: String, state: Vec<u8> },
13084 Reset { name: String },
13085 }
13086 let mut stage: HashMap<u64, RecoveryTableStage> = HashMap::new();
13087 let mut external_actions = Vec::new();
13088 let mut max_epoch = epoch.visible().0;
13089 for r in records.iter().cloned() {
13090 let Some(&ce) = committed.get(&r.txn_id) else {
13091 continue; };
13093 let commit_epoch = Epoch(ce);
13094 max_epoch = max_epoch.max(ce);
13095 match r.op {
13096 Op::Put { table_id, rows } => {
13097 let skip = tables
13099 .get(&table_id)
13100 .map(|h| h.lock().flushed_epoch() >= ce)
13101 .unwrap_or(true);
13102 if skip {
13103 continue;
13104 }
13105 let rows: Vec<Row> = bincode::deserialize(&rows).map_err(|error| {
13106 MongrelError::CorruptWal {
13107 offset: r.seq.0,
13108 reason: format!(
13109 "committed Put payload for transaction {} could not be decoded: {error}",
13110 r.txn_id
13111 ),
13112 }
13113 })?;
13114 let rows: Vec<Row> = rows
13117 .into_iter()
13118 .map(|mut row| {
13119 row.committed_epoch = commit_epoch;
13120 row
13121 })
13122 .collect();
13123 let entry = stage
13124 .entry(table_id)
13125 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
13126 entry.0.extend(rows);
13127 entry.3 = commit_epoch;
13128 }
13129 Op::Delete { table_id, row_ids } => {
13130 let skip = tables
13131 .get(&table_id)
13132 .map(|h| h.lock().flushed_epoch() >= ce)
13133 .unwrap_or(true);
13134 if skip {
13135 continue;
13136 }
13137 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
13138 let entry = stage
13139 .entry(table_id)
13140 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
13141 entry.1.extend(dels);
13142 entry.3 = commit_epoch;
13143 }
13144 Op::TruncateTable { table_id } => {
13145 let skip = tables
13146 .get(&table_id)
13147 .map(|h| h.lock().flushed_epoch() >= ce)
13148 .unwrap_or(true);
13149 if skip {
13150 continue;
13151 }
13152 stage.insert(
13153 table_id,
13154 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
13155 );
13156 }
13157 Op::ExternalTableState { name, state } => {
13158 let current_generation = catalog
13159 .external_tables
13160 .iter()
13161 .find(|entry| entry.name == name)
13162 .map(|entry| entry.created_epoch);
13163 if current_generation.is_some_and(|created_epoch| ce >= created_epoch) {
13164 validate_recovered_external_name(&name)?;
13165 external_actions.push(ExternalRecoveryAction::Write { name, state });
13166 }
13167 }
13168 Op::Ddl(DdlOp::ResetExternalTableState {
13169 name,
13170 generation_epoch,
13171 }) => {
13172 if generation_epoch != ce {
13173 return Err(MongrelError::CorruptWal {
13174 offset: r.seq.0,
13175 reason: format!(
13176 "external state reset epoch {generation_epoch} does not match WAL commit epoch {ce}"
13177 ),
13178 });
13179 }
13180 validate_recovered_external_name(&name)?;
13181 external_actions.push(ExternalRecoveryAction::Reset { name });
13182 }
13183 Op::Flush { .. }
13184 | Op::TxnCommit { .. }
13185 | Op::TxnAbort
13186 | Op::Ddl(_)
13187 | Op::BeforeImage { .. }
13188 | Op::CommitTimestamp { .. }
13189 | Op::SpilledRows { .. } => {}
13190 }
13191 }
13192 for (_, commit_epoch, added_runs) in &mut spilled_to_link {
13193 added_runs.retain(|added| {
13194 tables
13195 .get(&added.table_id)
13196 .is_some_and(|table| table.lock().flushed_epoch() < *commit_epoch)
13197 });
13198 }
13199 spilled_to_link.retain(|(_, _, added_runs)| !added_runs.is_empty());
13200 validate_recovery_table_stages(tables, &stage)?;
13201 validate_recovery_spilled_runs(durable_root, tables, &spilled_to_link)?;
13202
13203 for action in external_actions {
13207 match action {
13208 ExternalRecoveryAction::Write { name, state } => {
13209 write_external_state_file(durable_root, &name, &state)?;
13210 }
13211 ExternalRecoveryAction::Reset { name } => {
13212 durable_root.create_directory_all(VTAB_DIR)?;
13213 durable_root.remove_directory_all(Path::new(VTAB_DIR).join(name))?;
13214 }
13215 }
13216 }
13217 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
13218 let Some(handle) = tables.get(&table_id) else {
13219 continue;
13220 };
13221 let mut t = handle.lock();
13222 if let Some(epoch) = truncate_epoch {
13223 t.apply_truncate(epoch);
13224 }
13225 t.recover_apply(rows, deletes)?;
13226 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
13230 t.live_count = rows.len() as u64;
13231 t.persist_manifest(table_epoch.max(epoch.visible()))?;
13235 }
13236
13237 for (txn_id, ce, added_runs) in &spilled_to_link {
13241 for ar in added_runs {
13242 let Some(handle) = tables.get(&ar.table_id) else {
13243 continue;
13244 };
13245 let mut t = handle.lock();
13246 let table_dir = Path::new(TABLES_DIR).join(ar.table_id.to_string());
13247 let destination = table_dir
13248 .join(crate::engine::RUNS_DIR)
13249 .join(format!("r-{}.sr", ar.run_id));
13250 match durable_root.open_regular(&destination) {
13251 Ok(_) => {}
13252 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
13253 let pending = table_dir
13254 .join("_txn")
13255 .join(txn_id.to_string())
13256 .join(format!("r-{}.sr", ar.run_id));
13257 durable_root.rename_file_new(&pending, &destination)?;
13258 }
13259 Err(error) => return Err(error.into()),
13260 }
13261 let linked = t.recover_spilled_run(crate::manifest::RunRef {
13267 run_id: ar.run_id,
13268 level: ar.level,
13269 epoch_created: *ce,
13270 row_count: ar.row_count,
13271 });
13272 let replaced = truncated_transactions.contains(&(*txn_id, ar.table_id));
13273 if replaced {
13274 t.set_flushed_epoch(Epoch(*ce));
13275 }
13276 if linked || replaced {
13277 t.persist_manifest(Epoch(*ce).max(epoch.visible()))?;
13278 }
13279 }
13280 }
13281
13282 epoch.advance_recovered(Epoch(max_epoch));
13283 Ok(())
13284}
13285
13286fn reconcile_recovered_table_metadata(
13287 tables: &HashMap<u64, TableHandle>,
13288 epoch: Epoch,
13289) -> Result<()> {
13290 let mut table_ids = tables.keys().copied().collect::<Vec<_>>();
13291 table_ids.sort_unstable();
13292 let mut plans = Vec::with_capacity(table_ids.len());
13293 for table_id in &table_ids {
13294 let handle = tables.get(table_id).ok_or_else(|| {
13295 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
13296 })?;
13297 plans.push((*table_id, handle.lock().plan_recovered_metadata()?));
13298 }
13299 for (table_id, plan) in plans {
13302 let handle = tables.get(&table_id).ok_or_else(|| {
13303 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
13304 })?;
13305 handle.lock().apply_recovered_metadata(plan, epoch)?;
13306 }
13307 Ok(())
13308}
13309
13310fn validate_recovered_external_name(name: &str) -> Result<()> {
13311 if name.is_empty()
13312 || !name.chars().all(|character| {
13313 character.is_ascii_alphanumeric() || character == '_' || character == '-'
13314 })
13315 {
13316 return Err(MongrelError::CorruptWal {
13317 offset: 0,
13318 reason: format!("unsafe recovered external-table name {name:?}"),
13319 });
13320 }
13321 Ok(())
13322}
13323
13324fn validate_recovery_table_stages(
13325 tables: &HashMap<u64, TableHandle>,
13326 stages: &HashMap<u64, RecoveryTableStage>,
13327) -> Result<()> {
13328 for (table_id, (rows, _, _, _)) in stages {
13329 let handle = tables
13330 .get(table_id)
13331 .ok_or_else(|| MongrelError::CorruptWal {
13332 offset: *table_id,
13333 reason: format!("recovery stage references unmounted table {table_id}"),
13334 })?;
13335 let table = handle.lock();
13336 table.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
13339 for row in rows {
13340 validate_recovered_row(table.schema(), row)?;
13341 }
13342 }
13343 Ok(())
13344}
13345
13346fn validate_recovered_row(schema: &Schema, row: &crate::memtable::Row) -> Result<()> {
13347 if row.deleted || row.row_id.0 == u64::MAX {
13348 return Err(MongrelError::CorruptWal {
13349 offset: row.row_id.0,
13350 reason: "committed Put payload contains a tombstone or exhausted row id".into(),
13351 });
13352 }
13353 let cells = row
13354 .columns
13355 .iter()
13356 .map(|(column, value)| (*column, value.clone()))
13357 .collect::<Vec<_>>();
13358 schema
13359 .validate_persisted_values(&cells)
13360 .map_err(|error| MongrelError::CorruptWal {
13361 offset: row.row_id.0,
13362 reason: format!("recovered row violates table schema: {error}"),
13363 })?;
13364 if schema.auto_increment_column().is_some_and(|column| {
13365 matches!(row.columns.get(&column.id), Some(Value::Int64(value)) if *value == i64::MAX)
13366 }) {
13367 return Err(MongrelError::CorruptWal {
13368 offset: row.row_id.0,
13369 reason: "recovered AUTO_INCREMENT value exhausts i64".into(),
13370 });
13371 }
13372 Ok(())
13373}
13374
13375fn validate_recovery_spilled_runs(
13376 root: &crate::durable_file::DurableRoot,
13377 tables: &HashMap<u64, TableHandle>,
13378 spilled: &[(u64, u64, Vec<crate::wal::AddedRun>)],
13379) -> Result<()> {
13380 let mut identities = HashSet::new();
13381 for (txn_id, commit_epoch, added_runs) in spilled {
13382 for added in added_runs {
13383 if added.run_id >= u64::MAX as u128 {
13384 return Err(MongrelError::CorruptWal {
13385 offset: *commit_epoch,
13386 reason: format!(
13387 "recovered run id {} exceeds the on-disk namespace",
13388 added.run_id
13389 ),
13390 });
13391 }
13392 let Some(handle) = tables.get(&added.table_id) else {
13393 continue;
13394 };
13395 if !identities.insert((added.table_id, added.run_id)) {
13396 return Err(MongrelError::CorruptWal {
13397 offset: *commit_epoch,
13398 reason: format!(
13399 "duplicate recovered run {} for table {}",
13400 added.run_id, added.table_id
13401 ),
13402 });
13403 }
13404 let table = handle.lock();
13405 validate_planned_spilled_run(
13406 root,
13407 *txn_id,
13408 *commit_epoch,
13409 added,
13410 table.schema(),
13411 table.kek(),
13412 )?;
13413 }
13414 }
13415 Ok(())
13416}
13417
13418fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
13419 match condition {
13420 ProcedureCondition::Pk { .. } => {
13421 if schema.primary_key().is_none() {
13422 return Err(MongrelError::InvalidArgument(
13423 "procedure condition Pk references a table without a primary key".into(),
13424 ));
13425 }
13426 }
13427 ProcedureCondition::BitmapEq { column_id, .. }
13428 | ProcedureCondition::BitmapIn { column_id, .. }
13429 | ProcedureCondition::Range { column_id, .. }
13430 | ProcedureCondition::RangeF64 { column_id, .. }
13431 | ProcedureCondition::IsNull { column_id }
13432 | ProcedureCondition::IsNotNull { column_id }
13433 | ProcedureCondition::FmContains { column_id, .. } => {
13434 validate_column_id(*column_id, schema)?;
13435 }
13436 }
13437 Ok(())
13438}
13439
13440fn bind_procedure_args(
13441 procedure: &StoredProcedure,
13442 mut args: HashMap<String, crate::Value>,
13443) -> Result<HashMap<String, crate::Value>> {
13444 let mut out = HashMap::new();
13445 for param in &procedure.params {
13446 let value = match args.remove(¶m.name) {
13447 Some(value) => value,
13448 None => param.default.clone().ok_or_else(|| {
13449 MongrelError::InvalidArgument(format!(
13450 "missing required procedure parameter {:?}",
13451 param.name
13452 ))
13453 })?,
13454 };
13455 if !param.nullable && matches!(value, crate::Value::Null) {
13456 return Err(MongrelError::InvalidArgument(format!(
13457 "procedure parameter {:?} must not be NULL",
13458 param.name
13459 )));
13460 }
13461 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty.clone()) {
13462 return Err(MongrelError::InvalidArgument(format!(
13463 "procedure parameter {:?} has wrong type",
13464 param.name
13465 )));
13466 }
13467 out.insert(param.name.clone(), value);
13468 }
13469 if let Some(extra) = args.keys().next() {
13470 return Err(MongrelError::InvalidArgument(format!(
13471 "unknown procedure parameter {extra:?}"
13472 )));
13473 }
13474 Ok(out)
13475}
13476
13477fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
13478 matches!(
13479 (value, ty),
13480 (crate::Value::Bool(_), crate::TypeId::Bool)
13481 | (crate::Value::Int64(_), crate::TypeId::Int8)
13482 | (crate::Value::Int64(_), crate::TypeId::Int16)
13483 | (crate::Value::Int64(_), crate::TypeId::Int32)
13484 | (crate::Value::Int64(_), crate::TypeId::Int64)
13485 | (crate::Value::Int64(_), crate::TypeId::UInt8)
13486 | (crate::Value::Int64(_), crate::TypeId::UInt16)
13487 | (crate::Value::Int64(_), crate::TypeId::UInt32)
13488 | (crate::Value::Int64(_), crate::TypeId::UInt64)
13489 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
13490 | (crate::Value::Int64(_), crate::TypeId::Date32)
13491 | (crate::Value::Float64(_), crate::TypeId::Float32)
13492 | (crate::Value::Float64(_), crate::TypeId::Float64)
13493 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
13494 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
13495 )
13496}
13497
13498fn eval_cells(
13499 cells: &[crate::procedure::ProcedureCell],
13500 args: &HashMap<String, crate::Value>,
13501 outputs: &HashMap<String, ProcedureCallOutput>,
13502) -> Result<Vec<(u16, crate::Value)>> {
13503 cells
13504 .iter()
13505 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
13506 .collect()
13507}
13508
13509fn eval_condition(
13510 condition: &ProcedureCondition,
13511 args: &HashMap<String, crate::Value>,
13512 outputs: &HashMap<String, ProcedureCallOutput>,
13513) -> Result<crate::Condition> {
13514 Ok(match condition {
13515 ProcedureCondition::Pk { value } => {
13516 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
13517 }
13518 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
13519 column_id: *column_id,
13520 value: eval_value(value, args, outputs)?.encode_key(),
13521 },
13522 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
13523 column_id: *column_id,
13524 values: values
13525 .iter()
13526 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
13527 .collect::<Result<Vec<_>>>()?,
13528 },
13529 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
13530 column_id: *column_id,
13531 lo: expect_i64(eval_value(lo, args, outputs)?)?,
13532 hi: expect_i64(eval_value(hi, args, outputs)?)?,
13533 },
13534 ProcedureCondition::RangeF64 {
13535 column_id,
13536 lo,
13537 lo_inclusive,
13538 hi,
13539 hi_inclusive,
13540 } => crate::Condition::RangeF64 {
13541 column_id: *column_id,
13542 lo: expect_f64(eval_value(lo, args, outputs)?)?,
13543 lo_inclusive: *lo_inclusive,
13544 hi: expect_f64(eval_value(hi, args, outputs)?)?,
13545 hi_inclusive: *hi_inclusive,
13546 },
13547 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
13548 column_id: *column_id,
13549 },
13550 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
13551 column_id: *column_id,
13552 },
13553 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
13554 column_id: *column_id,
13555 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
13556 },
13557 })
13558}
13559
13560fn eval_value(
13561 value: &ProcedureValue,
13562 args: &HashMap<String, crate::Value>,
13563 outputs: &HashMap<String, ProcedureCallOutput>,
13564) -> Result<crate::Value> {
13565 match value {
13566 ProcedureValue::Literal(value) => Ok(value.clone()),
13567 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
13568 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
13569 }),
13570 ProcedureValue::StepScalar(id) => match outputs.get(id) {
13571 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
13572 _ => Err(MongrelError::InvalidArgument(format!(
13573 "procedure step {id:?} did not return a scalar"
13574 ))),
13575 },
13576 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
13577 Err(MongrelError::InvalidArgument(
13578 "row-valued procedure reference cannot be used as a scalar".into(),
13579 ))
13580 }
13581 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
13582 "structured procedure value cannot be used as a scalar cell".into(),
13583 )),
13584 }
13585}
13586
13587fn eval_return_output(
13588 value: &ProcedureValue,
13589 args: &HashMap<String, crate::Value>,
13590 outputs: &HashMap<String, ProcedureCallOutput>,
13591) -> Result<ProcedureCallOutput> {
13592 match value {
13593 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
13594 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
13595 args.get(name).cloned().ok_or_else(|| {
13596 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
13597 })?,
13598 )),
13599 ProcedureValue::StepRows(id)
13600 | ProcedureValue::StepRow(id)
13601 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
13602 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
13603 }),
13604 ProcedureValue::Object(fields) => {
13605 let mut out = Vec::with_capacity(fields.len());
13606 for (name, value) in fields {
13607 out.push((name.clone(), eval_return_output(value, args, outputs)?));
13608 }
13609 Ok(ProcedureCallOutput::Object(out))
13610 }
13611 ProcedureValue::Array(values) => {
13612 let mut out = Vec::with_capacity(values.len());
13613 for value in values {
13614 out.push(eval_return_output(value, args, outputs)?);
13615 }
13616 Ok(ProcedureCallOutput::Array(out))
13617 }
13618 }
13619}
13620
13621fn expect_i64(value: crate::Value) -> Result<i64> {
13622 match value {
13623 crate::Value::Int64(value) => Ok(value),
13624 _ => Err(MongrelError::InvalidArgument(
13625 "procedure value must be Int64".into(),
13626 )),
13627 }
13628}
13629
13630fn expect_f64(value: crate::Value) -> Result<f64> {
13631 match value {
13632 crate::Value::Float64(value) => Ok(value),
13633 _ => Err(MongrelError::InvalidArgument(
13634 "procedure value must be Float64".into(),
13635 )),
13636 }
13637}
13638
13639fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
13640 match value {
13641 crate::Value::Bytes(value) => Ok(value),
13642 _ => Err(MongrelError::InvalidArgument(
13643 "procedure value must be Bytes".into(),
13644 )),
13645 }
13646}
13647
13648fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
13649 if schema.columns.iter().any(|c| c.id == column_id) {
13650 Ok(())
13651 } else {
13652 Err(MongrelError::InvalidArgument(format!(
13653 "unknown column id {column_id}"
13654 )))
13655 }
13656}
13657
13658fn trigger_matches_event(
13659 trigger: &StoredTrigger,
13660 event: &WriteEvent,
13661 cat: &Catalog,
13662) -> Result<bool> {
13663 if trigger.event != event.kind {
13664 return Ok(false);
13665 }
13666 let TriggerTarget::Table(target) = &trigger.target else {
13667 return Ok(false);
13668 };
13669 if target != &event.table {
13670 return Ok(false);
13671 }
13672 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
13673 let schema = &cat
13674 .live(target)
13675 .ok_or_else(|| {
13676 MongrelError::InvalidArgument(format!(
13677 "trigger {:?} references unknown table {target:?}",
13678 trigger.name
13679 ))
13680 })?
13681 .schema;
13682 let mut watched = Vec::with_capacity(trigger.update_of.len());
13683 for name in &trigger.update_of {
13684 let col = schema.column(name).ok_or_else(|| {
13685 MongrelError::InvalidArgument(format!(
13686 "trigger {:?} references unknown UPDATE OF column {name:?}",
13687 trigger.name
13688 ))
13689 })?;
13690 watched.push(col.id);
13691 }
13692 if !event
13693 .changed_columns
13694 .iter()
13695 .any(|column_id| watched.contains(column_id))
13696 {
13697 return Ok(false);
13698 }
13699 }
13700 Ok(true)
13701}
13702
13703fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
13704 let mut ids = std::collections::BTreeSet::new();
13705 if let Some(old) = old {
13706 ids.extend(old.columns.keys().copied());
13707 }
13708 if let Some(new) = new {
13709 ids.extend(new.columns.keys().copied());
13710 }
13711 ids.into_iter()
13712 .filter(|id| {
13713 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
13714 })
13715 .collect()
13716}
13717
13718fn eval_trigger_cells(
13719 cells: &[crate::trigger::TriggerCell],
13720 event: &WriteEvent,
13721 selected: Option<&TriggerRowImage>,
13722) -> Result<Vec<(u16, Value)>> {
13723 cells
13724 .iter()
13725 .map(|cell| {
13726 Ok((
13727 cell.column_id,
13728 eval_trigger_value(&cell.value, event, selected)?,
13729 ))
13730 })
13731 .collect()
13732}
13733
13734fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
13735 match expr {
13736 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
13737 Value::Bool(value) => Ok(value),
13738 Value::Null => Ok(false),
13739 other => Err(MongrelError::InvalidArgument(format!(
13740 "trigger WHEN value must be boolean, got {other:?}"
13741 ))),
13742 },
13743 TriggerExpr::Eq { left, right } => Ok(values_equal(
13744 &eval_trigger_value(left, event, None)?,
13745 &eval_trigger_value(right, event, None)?,
13746 )),
13747 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
13748 &eval_trigger_value(left, event, None)?,
13749 &eval_trigger_value(right, event, None)?,
13750 )),
13751 TriggerExpr::Lt { left, right } => match value_order(
13752 &eval_trigger_value(left, event, None)?,
13753 &eval_trigger_value(right, event, None)?,
13754 ) {
13755 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
13756 None => Ok(false),
13757 },
13758 TriggerExpr::Lte { 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::Greater),
13763 None => Ok(false),
13764 },
13765 TriggerExpr::Gt { 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::Gte { 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::Less),
13777 None => Ok(false),
13778 },
13779 TriggerExpr::IsNull(value) => Ok(matches!(
13780 eval_trigger_value(value, event, None)?,
13781 Value::Null
13782 )),
13783 TriggerExpr::IsNotNull(value) => Ok(!matches!(
13784 eval_trigger_value(value, event, None)?,
13785 Value::Null
13786 )),
13787 TriggerExpr::And { left, right } => {
13788 if !eval_trigger_expr(left, event)? {
13789 Ok(false)
13790 } else {
13791 Ok(eval_trigger_expr(right, event)?)
13792 }
13793 }
13794 TriggerExpr::Or { left, right } => {
13795 if eval_trigger_expr(left, event)? {
13796 Ok(true)
13797 } else {
13798 Ok(eval_trigger_expr(right, event)?)
13799 }
13800 }
13801 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
13802 }
13803}
13804
13805fn eval_trigger_condition(
13806 condition: &TriggerCondition,
13807 event: &WriteEvent,
13808 selected: &TriggerRowImage,
13809 schema: &Schema,
13810) -> Result<bool> {
13811 match condition {
13812 TriggerCondition::Pk { value } => {
13813 let pk = schema.primary_key().ok_or_else(|| {
13814 MongrelError::InvalidArgument(
13815 "trigger condition Pk references a table without a primary key".into(),
13816 )
13817 })?;
13818 let lhs = eval_trigger_value(value, event, Some(selected))?;
13819 Ok(values_equal(
13820 &lhs,
13821 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
13822 ))
13823 }
13824 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
13825 selected.columns.get(column_id).unwrap_or(&Value::Null),
13826 &eval_trigger_value(value, event, Some(selected))?,
13827 )),
13828 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
13829 selected.columns.get(column_id).unwrap_or(&Value::Null),
13830 &eval_trigger_value(value, event, Some(selected))?,
13831 )),
13832 TriggerCondition::Lt { column_id, value } => match value_order(
13833 selected.columns.get(column_id).unwrap_or(&Value::Null),
13834 &eval_trigger_value(value, event, Some(selected))?,
13835 ) {
13836 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
13837 None => Ok(false),
13838 },
13839 TriggerCondition::Lte { 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::Greater),
13844 None => Ok(false),
13845 },
13846 TriggerCondition::Gt { 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::Gte { 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::Less),
13858 None => Ok(false),
13859 },
13860 TriggerCondition::IsNull { column_id } => Ok(matches!(
13861 selected.columns.get(column_id),
13862 None | Some(Value::Null)
13863 )),
13864 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
13865 selected.columns.get(column_id),
13866 None | Some(Value::Null)
13867 )),
13868 TriggerCondition::And { left, right } => {
13869 if !eval_trigger_condition(left, event, selected, schema)? {
13870 Ok(false)
13871 } else {
13872 Ok(eval_trigger_condition(right, event, selected, schema)?)
13873 }
13874 }
13875 TriggerCondition::Or { left, right } => {
13876 if eval_trigger_condition(left, event, selected, schema)? {
13877 Ok(true)
13878 } else {
13879 Ok(eval_trigger_condition(right, event, selected, schema)?)
13880 }
13881 }
13882 TriggerCondition::Not(condition) => {
13883 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
13884 }
13885 }
13886}
13887
13888fn eval_trigger_value(
13889 value: &TriggerValue,
13890 event: &WriteEvent,
13891 selected: Option<&TriggerRowImage>,
13892) -> Result<Value> {
13893 match value {
13894 TriggerValue::Literal(value) => Ok(value.clone()),
13895 TriggerValue::NewColumn(column_id) => event
13896 .new
13897 .as_ref()
13898 .and_then(|row| row.columns.get(column_id))
13899 .cloned()
13900 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
13901 TriggerValue::OldColumn(column_id) => event
13902 .old
13903 .as_ref()
13904 .and_then(|row| row.columns.get(column_id))
13905 .cloned()
13906 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
13907 TriggerValue::SelectedColumn(column_id) => selected
13908 .and_then(|row| row.columns.get(column_id))
13909 .cloned()
13910 .ok_or_else(|| {
13911 MongrelError::InvalidArgument("SELECTED column is not available".into())
13912 }),
13913 }
13914}
13915
13916fn values_equal(left: &Value, right: &Value) -> bool {
13917 match (left, right) {
13918 (Value::Null, Value::Null) => true,
13919 (Value::Bool(a), Value::Bool(b)) => a == b,
13920 (Value::Int64(a), Value::Int64(b)) => a == b,
13921 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
13922 (Value::Bytes(a), Value::Bytes(b)) => a == b,
13923 (Value::Embedding(a), Value::Embedding(b)) => {
13924 a.len() == b.len()
13925 && a.iter()
13926 .zip(b.iter())
13927 .all(|(a, b)| a.to_bits() == b.to_bits())
13928 }
13929 _ => false,
13930 }
13931}
13932
13933fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
13934 match (left, right) {
13935 (Value::Null, _) | (_, Value::Null) => None,
13936 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
13937 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
13938 (Value::Int64(a), Value::Float64(b)) => {
13941 let af = *a as f64;
13942 Some(af.total_cmp(b))
13943 }
13944 (Value::Float64(a), Value::Int64(b)) => {
13947 let bf = *b as f64;
13948 Some(a.total_cmp(&bf))
13949 }
13950 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
13951 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
13952 (Value::Embedding(_), Value::Embedding(_)) => None,
13953 _ => None,
13954 }
13955}
13956
13957fn trigger_message(value: Value) -> String {
13958 match value {
13959 Value::Null => "NULL".into(),
13960 Value::Bool(value) => value.to_string(),
13961 Value::Int64(value) => value.to_string(),
13962 Value::Float64(value) => value.to_string(),
13963 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
13964 Value::Embedding(value) => format!("{value:?}"),
13965 Value::Decimal(value) => value.to_string(),
13966 Value::Interval {
13967 months,
13968 days,
13969 nanos,
13970 } => format!("{months}m {days}d {nanos}ns"),
13971 Value::Uuid(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
13972 Value::Json(b) => String::from_utf8_lossy(&b).into_owned(),
13973 }
13974}
13975
13976fn validate_trigger_step<'a>(
13977 step: &TriggerStep,
13978 cat: &'a Catalog,
13979 target_schema: &Schema,
13980 event: TriggerEvent,
13981 select_schemas: &mut HashMap<String, &'a Schema>,
13982) -> Result<()> {
13983 match step {
13984 TriggerStep::SetNew { cells } => {
13985 if event == TriggerEvent::Delete {
13986 return Err(MongrelError::InvalidArgument(
13987 "SetNew trigger step is not valid for DELETE triggers".into(),
13988 ));
13989 }
13990 for cell in cells {
13991 validate_column_id(cell.column_id, target_schema)?;
13992 validate_trigger_value(&cell.value, target_schema, event)?;
13993 }
13994 }
13995 TriggerStep::Insert { table, cells } => {
13996 let schema = trigger_write_schema(cat, table, "insert")?;
13997 for cell in cells {
13998 validate_column_id(cell.column_id, schema)?;
13999 validate_trigger_value(&cell.value, target_schema, event)?;
14000 }
14001 }
14002 TriggerStep::UpdateByPk { table, pk, cells } => {
14003 let schema = trigger_write_schema(cat, table, "update")?;
14004 if schema.primary_key().is_none() {
14005 return Err(MongrelError::InvalidArgument(format!(
14006 "trigger update_by_pk references table {table:?} without a primary key"
14007 )));
14008 }
14009 validate_trigger_value(pk, target_schema, event)?;
14010 for cell in cells {
14011 validate_column_id(cell.column_id, schema)?;
14012 validate_trigger_value(&cell.value, target_schema, event)?;
14013 }
14014 }
14015 TriggerStep::DeleteByPk { table, pk } => {
14016 let schema = trigger_write_schema(cat, table, "delete")?;
14017 if schema.primary_key().is_none() {
14018 return Err(MongrelError::InvalidArgument(format!(
14019 "trigger delete_by_pk references table {table:?} without a primary key"
14020 )));
14021 }
14022 validate_trigger_value(pk, target_schema, event)?;
14023 }
14024 TriggerStep::Select {
14025 id,
14026 table,
14027 conditions,
14028 } => {
14029 let schema = trigger_read_schema(cat, table)?;
14030 for condition in conditions {
14031 validate_trigger_condition(condition, schema, target_schema, event)?;
14032 }
14033 if select_schemas.contains_key(id) {
14034 return Err(MongrelError::InvalidArgument(format!(
14035 "duplicate select id {id:?} in trigger program"
14036 )));
14037 }
14038 select_schemas.insert(id.clone(), schema);
14039 }
14040 TriggerStep::Foreach { id, steps } => {
14041 if !select_schemas.contains_key(id) {
14042 return Err(MongrelError::InvalidArgument(format!(
14043 "foreach references unknown select id {id:?}"
14044 )));
14045 }
14046 let mut inner_select_schemas = select_schemas.clone();
14047 for step in steps {
14048 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
14049 }
14050 }
14051 TriggerStep::DeleteWhere { table, conditions } => {
14052 let schema = trigger_write_schema(cat, table, "delete")?;
14053 for condition in conditions {
14054 validate_trigger_condition(condition, schema, target_schema, event)?;
14055 }
14056 }
14057 TriggerStep::UpdateWhere {
14058 table,
14059 conditions,
14060 cells,
14061 } => {
14062 let schema = trigger_write_schema(cat, table, "update")?;
14063 for condition in conditions {
14064 validate_trigger_condition(condition, schema, target_schema, event)?;
14065 }
14066 for cell in cells {
14067 validate_column_id(cell.column_id, schema)?;
14068 validate_trigger_value(&cell.value, target_schema, event)?;
14069 }
14070 }
14071 TriggerStep::Raise { message, .. } => {
14072 validate_trigger_value(message, target_schema, event)?
14073 }
14074 }
14075 Ok(())
14076}
14077
14078fn trigger_validation_error(error: MongrelError) -> MongrelError {
14079 match error {
14080 MongrelError::TriggerValidation(_) => error,
14081 MongrelError::InvalidArgument(message)
14082 | MongrelError::Conflict(message)
14083 | MongrelError::NotFound(message) => MongrelError::TriggerValidation(message),
14084 error => error,
14085 }
14086}
14087
14088fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
14089 if let Some(entry) = cat.live(table) {
14090 return Ok(&entry.schema);
14091 }
14092 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
14093 let allowed = match op {
14094 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
14095 "update" | "delete" => entry.capabilities.writable,
14096 _ => false,
14097 };
14098 if !allowed {
14099 return Err(MongrelError::InvalidArgument(format!(
14100 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
14101 entry.module
14102 )));
14103 }
14104 if !entry.capabilities.transaction_safe {
14105 return Err(MongrelError::InvalidArgument(format!(
14106 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
14107 entry.module
14108 )));
14109 }
14110 return Ok(&entry.declared_schema);
14111 }
14112 Err(MongrelError::InvalidArgument(format!(
14113 "trigger references unknown table {table:?}"
14114 )))
14115}
14116
14117fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
14118 if let Some(entry) = cat.live(table) {
14119 return Ok(&entry.schema);
14120 }
14121 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
14122 if entry.capabilities.trigger_safe {
14123 return Ok(&entry.declared_schema);
14124 }
14125 return Err(MongrelError::InvalidArgument(format!(
14126 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
14127 entry.module
14128 )));
14129 }
14130 Err(MongrelError::InvalidArgument(format!(
14131 "trigger references unknown table {table:?}"
14132 )))
14133}
14134
14135fn validate_trigger_condition(
14136 condition: &TriggerCondition,
14137 schema: &Schema,
14138 target_schema: &Schema,
14139 event: TriggerEvent,
14140) -> Result<()> {
14141 match condition {
14142 TriggerCondition::Pk { value } => {
14143 if schema.primary_key().is_none() {
14144 return Err(MongrelError::InvalidArgument(
14145 "trigger condition Pk references a table without a primary key".into(),
14146 ));
14147 }
14148 validate_trigger_value(value, target_schema, event)
14149 }
14150 TriggerCondition::Eq { column_id, value }
14151 | TriggerCondition::NotEq { column_id, value }
14152 | TriggerCondition::Lt { column_id, value }
14153 | TriggerCondition::Lte { column_id, value }
14154 | TriggerCondition::Gt { column_id, value }
14155 | TriggerCondition::Gte { column_id, value } => {
14156 validate_column_id(*column_id, schema)?;
14157 validate_trigger_value(value, target_schema, event)
14158 }
14159 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
14160 validate_column_id(*column_id, schema)
14161 }
14162 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
14163 validate_trigger_condition(left, schema, target_schema, event)?;
14164 validate_trigger_condition(right, schema, target_schema, event)
14165 }
14166 TriggerCondition::Not(condition) => {
14167 validate_trigger_condition(condition, schema, target_schema, event)
14168 }
14169 }
14170}
14171
14172fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
14173 match expr {
14174 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
14175 validate_trigger_value(value, schema, event)
14176 }
14177 TriggerExpr::Eq { left, right }
14178 | TriggerExpr::NotEq { left, right }
14179 | TriggerExpr::Lt { left, right }
14180 | TriggerExpr::Lte { left, right }
14181 | TriggerExpr::Gt { left, right }
14182 | TriggerExpr::Gte { left, right } => {
14183 validate_trigger_value(left, schema, event)?;
14184 validate_trigger_value(right, schema, event)
14185 }
14186 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
14187 validate_trigger_expr(left, schema, event)?;
14188 validate_trigger_expr(right, schema, event)
14189 }
14190 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
14191 }
14192}
14193
14194fn validate_trigger_value(
14195 value: &TriggerValue,
14196 schema: &Schema,
14197 event: TriggerEvent,
14198) -> Result<()> {
14199 match value {
14200 TriggerValue::Literal(_) => Ok(()),
14201 TriggerValue::NewColumn(id) => {
14202 if event == TriggerEvent::Delete {
14203 return Err(MongrelError::InvalidArgument(
14204 "DELETE triggers cannot reference NEW".into(),
14205 ));
14206 }
14207 validate_column_id(*id, schema)
14208 }
14209 TriggerValue::OldColumn(id) => {
14210 if event == TriggerEvent::Insert {
14211 return Err(MongrelError::InvalidArgument(
14212 "INSERT triggers cannot reference OLD".into(),
14213 ));
14214 }
14215 validate_column_id(*id, schema)
14216 }
14217 TriggerValue::SelectedColumn(_) => Ok(()),
14221 }
14222}
14223
14224fn recover_ddl_from_wal(
14230 root: &Path,
14231 durable_root: Option<&crate::durable_file::DurableRoot>,
14232 target_catalog: &mut Catalog,
14233 meta_dek: Option<&[u8; META_DEK_LEN]>,
14234 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
14235 apply: bool,
14236 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
14237) -> Result<()> {
14238 use crate::wal::SharedWal;
14239 let records = match durable_root {
14240 Some(root) => SharedWal::replay_durable_with_dek(root, wal_dek)?,
14241 None => SharedWal::replay_with_dek(root, wal_dek)?,
14242 };
14243 recover_ddl_from_records(
14244 root,
14245 durable_root,
14246 target_catalog,
14247 meta_dek,
14248 apply,
14249 table_roots,
14250 &records,
14251 )
14252}
14253
14254fn recover_ddl_from_records(
14255 root: &Path,
14256 durable_root: Option<&crate::durable_file::DurableRoot>,
14257 target_catalog: &mut Catalog,
14258 meta_dek: Option<&[u8; META_DEK_LEN]>,
14259 apply: bool,
14260 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
14261 records: &[crate::wal::Record],
14262) -> Result<()> {
14263 use crate::wal::{DdlOp, Op};
14264
14265 let original_catalog = target_catalog.clone();
14266 let mut recovered_catalog = original_catalog.clone();
14267 let cat = &mut recovered_catalog;
14268 let mut created_table_ids = HashSet::<u64>::new();
14269 let mut ttl_updates = HashMap::<u64, (Option<crate::manifest::TtlPolicy>, u64)>::new();
14270
14271 let mut committed: HashMap<u64, u64> = HashMap::new();
14272 for r in records {
14273 if let Op::TxnCommit { epoch: ce, .. } = r.op {
14274 committed.insert(r.txn_id, ce);
14275 }
14276 }
14277 let catalog_snapshot_txns = records
14278 .iter()
14279 .filter_map(|record| {
14280 (committed.contains_key(&record.txn_id)
14281 && matches!(&record.op, Op::Ddl(DdlOp::CatalogSnapshot { .. })))
14282 .then_some(record.txn_id)
14283 })
14284 .collect::<HashSet<_>>();
14285
14286 let mut changed = false;
14287 let mut applied_catalog_epoch = cat.db_epoch;
14288 let max_committed_epoch = committed.values().copied().max().unwrap_or(cat.db_epoch);
14289 for r in records.iter().cloned() {
14290 let Some(&ce) = committed.get(&r.txn_id) else {
14291 continue;
14292 };
14293 let txn_id = r.txn_id;
14294 match r.op {
14295 Op::Ddl(DdlOp::CreateTable {
14296 table_id,
14297 ref name,
14298 ref schema_json,
14299 }) => {
14300 if cat.tables.iter().any(|t| t.table_id == table_id) {
14301 continue;
14302 }
14303 let schema = DdlOp::decode_schema(schema_json)?;
14304 validate_recovered_schema(&schema)?;
14305 created_table_ids.insert(table_id);
14306 cat.tables.push(CatalogEntry {
14307 table_id,
14308 name: name.clone(),
14309 schema,
14310 state: TableState::Live,
14311 created_epoch: ce,
14312 });
14313 cat.next_table_id =
14314 cat.next_table_id
14315 .max(table_id.checked_add(1).ok_or_else(|| {
14316 MongrelError::Full("table id namespace exhausted".into())
14317 })?);
14318 changed = true;
14319 }
14320 Op::Ddl(DdlOp::CreateBuildingTable {
14321 table_id,
14322 ref build_name,
14323 ref intended_name,
14324 ref query_id,
14325 created_at_unix_nanos,
14326 ref schema_json,
14327 }) => {
14328 if cat.tables.iter().any(|table| table.table_id == table_id) {
14329 continue;
14330 }
14331 let schema = DdlOp::decode_schema(schema_json)?;
14332 validate_recovered_schema(&schema)?;
14333 created_table_ids.insert(table_id);
14334 cat.tables.push(CatalogEntry {
14335 table_id,
14336 name: build_name.clone(),
14337 schema,
14338 state: TableState::Building {
14339 intended_name: intended_name.clone(),
14340 query_id: query_id.clone(),
14341 created_at_unix_nanos,
14342 replaces_table_id: None,
14343 },
14344 created_epoch: ce,
14345 });
14346 cat.next_table_id =
14347 cat.next_table_id
14348 .max(table_id.checked_add(1).ok_or_else(|| {
14349 MongrelError::Full("table id namespace exhausted".into())
14350 })?);
14351 changed = true;
14352 }
14353 Op::Ddl(DdlOp::CreateRebuildingTable {
14354 table_id,
14355 ref build_name,
14356 ref intended_name,
14357 ref query_id,
14358 created_at_unix_nanos,
14359 replaces_table_id,
14360 ref schema_json,
14361 }) => {
14362 if cat.tables.iter().any(|table| table.table_id == table_id) {
14363 continue;
14364 }
14365 let schema = DdlOp::decode_schema(schema_json)?;
14366 validate_recovered_schema(&schema)?;
14367 created_table_ids.insert(table_id);
14368 cat.tables.push(CatalogEntry {
14369 table_id,
14370 name: build_name.clone(),
14371 schema,
14372 state: TableState::Building {
14373 intended_name: intended_name.clone(),
14374 query_id: query_id.clone(),
14375 created_at_unix_nanos,
14376 replaces_table_id: Some(replaces_table_id),
14377 },
14378 created_epoch: ce,
14379 });
14380 cat.next_table_id =
14381 cat.next_table_id
14382 .max(table_id.checked_add(1).ok_or_else(|| {
14383 MongrelError::Full("table id namespace exhausted".into())
14384 })?);
14385 changed = true;
14386 }
14387 Op::Ddl(DdlOp::DropTable { table_id }) => {
14388 let mut dropped_name = None;
14389 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
14390 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
14391 dropped_name = Some(entry.name.clone());
14392 entry.state = TableState::Dropped { at_epoch: ce };
14393 changed = true;
14394 }
14395 }
14396 if let Some(name) = dropped_name {
14397 let before = cat.materialized_views.len();
14398 cat.materialized_views
14399 .retain(|definition| definition.name != name);
14400 changed |= before != cat.materialized_views.len();
14401 cat.security.rls_tables.retain(|table| table != &name);
14402 cat.security.policies.retain(|policy| policy.table != name);
14403 cat.security.masks.retain(|mask| mask.table != name);
14404 for role in &mut cat.roles {
14405 role.permissions
14406 .retain(|permission| permission_table(permission) != Some(&name));
14407 }
14408 if !catalog_snapshot_txns.contains(&txn_id) {
14409 advance_security_version(cat)?;
14410 }
14411 }
14412 }
14413 Op::Ddl(DdlOp::PublishBuildingTable {
14414 table_id,
14415 ref new_name,
14416 }) => {
14417 if let Some(entry) = cat
14418 .tables
14419 .iter_mut()
14420 .find(|table| table.table_id == table_id)
14421 {
14422 if entry.name != *new_name || !matches!(entry.state, TableState::Live) {
14423 entry.name = new_name.clone();
14424 entry.state = TableState::Live;
14425 changed = true;
14426 }
14427 }
14428 }
14429 Op::Ddl(DdlOp::ReplaceBuildingTable {
14430 table_id,
14431 replaced_table_id,
14432 ref new_name,
14433 }) => {
14434 changed |=
14435 apply_rebuilding_publish(cat, table_id, replaced_table_id, new_name, ce)?;
14436 }
14437 Op::Ddl(DdlOp::RenameTable {
14438 table_id,
14439 ref new_name,
14440 }) => {
14441 let mut old_name = None;
14442 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
14443 if entry.name != *new_name {
14444 old_name = Some(entry.name.clone());
14445 entry.name = new_name.clone();
14446 changed = true;
14447 }
14448 }
14449 if let Some(old_name) = old_name {
14450 if let Some(definition) = cat
14451 .materialized_views
14452 .iter_mut()
14453 .find(|definition| definition.name == old_name)
14454 {
14455 definition.name = new_name.clone();
14456 }
14457 for table in &mut cat.security.rls_tables {
14458 if *table == old_name {
14459 *table = new_name.clone();
14460 }
14461 }
14462 for policy in &mut cat.security.policies {
14463 if policy.table == old_name {
14464 policy.table = new_name.clone();
14465 }
14466 }
14467 for mask in &mut cat.security.masks {
14468 if mask.table == old_name {
14469 mask.table = new_name.clone();
14470 }
14471 }
14472 for role in &mut cat.roles {
14473 for permission in &mut role.permissions {
14474 rename_permission_table(permission, &old_name, new_name);
14475 }
14476 }
14477 if !catalog_snapshot_txns.contains(&txn_id) {
14478 advance_security_version(cat)?;
14479 }
14480 }
14481 }
14485 Op::Ddl(DdlOp::AlterTable {
14486 table_id,
14487 ref column_json,
14488 }) => {
14489 let column = DdlOp::decode_column(column_json)?;
14490 let mut renamed = None;
14491 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
14492 renamed = entry
14493 .schema
14494 .columns
14495 .iter()
14496 .find(|existing| existing.id == column.id && existing.name != column.name)
14497 .map(|existing| {
14498 (
14499 entry.name.clone(),
14500 existing.name.clone(),
14501 column.name.clone(),
14502 )
14503 });
14504 if apply_recovered_column_def(&mut entry.schema, column)? {
14505 validate_recovered_schema(&entry.schema)?;
14506 changed = true;
14507 }
14508 }
14509 if let Some((table, old_name, new_name)) = renamed {
14510 for role in &mut cat.roles {
14511 for permission in &mut role.permissions {
14512 rename_permission_column(permission, &table, &old_name, &new_name);
14513 }
14514 }
14515 if !catalog_snapshot_txns.contains(&txn_id) {
14516 advance_security_version(cat)?;
14517 }
14518 }
14519 }
14520 Op::Ddl(DdlOp::SetTtl {
14521 table_id,
14522 ref policy_json,
14523 }) => {
14524 let policy = DdlOp::decode_ttl(policy_json)?;
14525 let entry = cat
14526 .tables
14527 .iter()
14528 .find(|entry| entry.table_id == table_id)
14529 .ok_or_else(|| {
14530 MongrelError::Schema(format!(
14531 "recovered TTL references unknown table id {table_id}"
14532 ))
14533 })?;
14534 if let Some(policy) = policy {
14535 let valid = entry
14536 .schema
14537 .columns
14538 .iter()
14539 .find(|column| column.id == policy.column_id)
14540 .is_some_and(|column| {
14541 column.ty == TypeId::TimestampNanos
14542 && policy.duration_nanos > 0
14543 && policy.duration_nanos <= i64::MAX as u64
14544 });
14545 if !valid {
14546 return Err(MongrelError::Schema(format!(
14547 "invalid recovered TTL policy for table id {table_id}"
14548 )));
14549 }
14550 }
14551 ttl_updates.insert(table_id, (policy, ce));
14552 }
14553 Op::Ddl(DdlOp::SetMaterializedView {
14554 ref name,
14555 ref definition_json,
14556 }) => {
14557 let definition = DdlOp::decode_materialized_view(definition_json)?;
14558 if definition.name != *name {
14559 return Err(MongrelError::Schema(format!(
14560 "materialized view WAL name mismatch: {name:?}"
14561 )));
14562 }
14563 if cat.live(name).is_some() {
14564 if let Some(existing) = cat
14565 .materialized_views
14566 .iter_mut()
14567 .find(|existing| existing.name == *name)
14568 {
14569 if *existing != definition {
14570 *existing = definition;
14571 changed = true;
14572 }
14573 } else {
14574 cat.materialized_views.push(definition);
14575 changed = true;
14576 }
14577 }
14578 }
14579 Op::Ddl(DdlOp::SetSecurityCatalog { ref security_json }) => {
14580 let security = DdlOp::decode_security(security_json)?;
14581 validate_security_catalog(cat, &security)?;
14582 if cat.security != security {
14583 cat.security = security;
14584 if !catalog_snapshot_txns.contains(&txn_id) {
14585 advance_security_version(cat)?;
14586 }
14587 changed = true;
14588 }
14589 }
14590 Op::Ddl(DdlOp::SetSqlPragma { ref key, value }) => {
14591 let target = match key.as_str() {
14592 "user_version" => &mut cat.user_version,
14593 "application_id" => &mut cat.application_id,
14594 _ => {
14595 return Err(MongrelError::InvalidArgument(format!(
14596 "unsupported recovered SQL pragma {key:?}"
14597 )))
14598 }
14599 };
14600 if *target != Some(value) {
14601 *target = Some(value);
14602 cat.db_epoch = cat.db_epoch.max(ce);
14603 changed = true;
14604 }
14605 }
14606 Op::Ddl(DdlOp::CatalogSnapshot { ref catalog_json }) => {
14607 if ce <= applied_catalog_epoch {
14608 continue;
14609 }
14610 let snapshot = DdlOp::decode_catalog(catalog_json)?;
14611 if snapshot.db_epoch != ce {
14612 return Err(MongrelError::Schema(format!(
14613 "catalog snapshot epoch {} does not match WAL commit epoch {ce}",
14614 snapshot.db_epoch
14615 )));
14616 }
14617 validate_recovered_catalog(&snapshot)?;
14618 validate_catalog_transition(cat, &snapshot)?;
14619 *cat = snapshot;
14620 applied_catalog_epoch = ce;
14621 changed = true;
14622 }
14623 _ => {}
14624 }
14625 }
14626
14627 if cat.db_epoch < max_committed_epoch {
14628 cat.db_epoch = max_committed_epoch;
14629 changed = true;
14630 }
14631 changed |= repair_catalog_allocator_counters(cat)?;
14632
14633 validate_recovered_catalog(cat)?;
14634 let storage_reconciliation = validate_recovered_storage_plan(
14635 root,
14636 durable_root,
14637 cat,
14638 &created_table_ids,
14639 &ttl_updates,
14640 meta_dek,
14641 )?;
14642
14643 let needs_storage_apply = !storage_reconciliation.is_empty() || !ttl_updates.is_empty();
14644 if apply && (changed || needs_storage_apply) {
14645 for table_id in storage_reconciliation {
14646 let entry = cat
14647 .tables
14648 .iter()
14649 .find(|entry| entry.table_id == table_id)
14650 .ok_or_else(|| MongrelError::CorruptWal {
14651 offset: table_id,
14652 reason: "recovery storage plan lost its catalog table".into(),
14653 })?;
14654 ensure_recovered_table_storage(
14655 table_roots
14656 .and_then(|roots| roots.get(&table_id))
14657 .map(Arc::as_ref),
14658 durable_root,
14659 &root.join(TABLES_DIR).join(table_id.to_string()),
14660 table_id,
14661 &entry.schema,
14662 meta_dek,
14663 )?;
14664 }
14665 for (table_id, (policy, ttl_epoch)) in ttl_updates {
14666 let Some(entry) = cat.tables.iter().find(|entry| {
14667 entry.table_id == table_id
14668 && matches!(entry.state, TableState::Live | TableState::Building { .. })
14669 }) else {
14670 continue;
14671 };
14672 let table_root = if let Some(root) = table_roots.and_then(|roots| roots.get(&table_id))
14673 {
14674 root.try_clone()?
14675 } else if let Some(root) = durable_root {
14676 root.open_directory(Path::new(TABLES_DIR).join(table_id.to_string()))?
14677 } else {
14678 crate::durable_file::DurableRoot::open(
14679 root.join(TABLES_DIR).join(table_id.to_string()),
14680 )?
14681 };
14682 let table_dir = table_root.io_path()?;
14683 let mut manifest = crate::manifest::read_durable(&table_root, "", meta_dek)?;
14684 if manifest.ttl != policy || manifest.current_epoch < ttl_epoch {
14685 manifest.ttl = policy;
14686 manifest.current_epoch = manifest.current_epoch.max(ttl_epoch);
14687 manifest.schema_id = entry.schema.schema_id;
14688 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
14689 }
14690 }
14691 if changed {
14692 match durable_root {
14693 Some(root) => catalog::write_durable(root, cat, meta_dek)?,
14694 None => catalog::write_atomic(root, cat, meta_dek)?,
14695 }
14696 }
14697 }
14698 *target_catalog = recovered_catalog;
14699 Ok(())
14700}
14701
14702fn ensure_recovered_table_storage(
14703 pinned_table: Option<&crate::durable_file::DurableRoot>,
14704 durable_root: Option<&crate::durable_file::DurableRoot>,
14705 fallback_table_dir: &Path,
14706 table_id: u64,
14707 schema: &Schema,
14708 meta_dek: Option<&[u8; META_DEK_LEN]>,
14709) -> Result<()> {
14710 let table_root = if let Some(root) = pinned_table {
14711 root.try_clone()?
14712 } else if let Some(root) = durable_root {
14713 let relative = Path::new(TABLES_DIR).join(table_id.to_string());
14714 match root.open_directory(&relative) {
14715 Ok(table) => table,
14716 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
14717 root.create_directory_all_pinned(relative)?
14718 }
14719 Err(error) => return Err(error.into()),
14720 }
14721 } else {
14722 crate::durable_file::create_directory_all(fallback_table_dir)?;
14723 crate::durable_file::DurableRoot::open(fallback_table_dir)?
14724 };
14725 let table_dir = table_root.io_path()?;
14726 let mut existing_manifest = match crate::manifest::read_durable(&table_root, "", meta_dek) {
14727 Ok(manifest) => {
14728 if manifest.table_id != table_id {
14729 return Err(MongrelError::Conflict(format!(
14730 "recovered table directory id mismatch: expected {table_id}, found {}",
14731 manifest.table_id
14732 )));
14733 }
14734 Some(manifest)
14735 }
14736 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
14737 Err(error) => return Err(error),
14738 };
14739
14740 table_root.create_directory_all(crate::engine::WAL_DIR)?;
14741 table_root.create_directory_all(crate::engine::RUNS_DIR)?;
14742 crate::engine::write_schema(&table_dir, schema)?;
14743
14744 if let Some(mut manifest) = existing_manifest.take() {
14745 if manifest.schema_id != schema.schema_id {
14746 manifest.schema_id = schema.schema_id;
14747 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
14748 }
14749 } else {
14750 let mut manifest = crate::manifest::Manifest::new(table_id, schema.schema_id);
14752 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
14753 }
14754 Ok(())
14755}
14756
14757fn validate_recovered_schema(schema: &Schema) -> Result<()> {
14758 schema.validate_auto_increment()?;
14759 schema.validate_defaults()?;
14760 schema.validate_ai()?;
14761 let mut column_ids = HashSet::new();
14762 let mut column_names = HashSet::new();
14763 for column in &schema.columns {
14764 if !column_ids.insert(column.id) || !column_names.insert(column.name.as_str()) {
14765 return Err(MongrelError::Schema(
14766 "recovered schema contains duplicate columns".into(),
14767 ));
14768 }
14769 match &column.ty {
14770 TypeId::Decimal128 { precision, scale }
14771 if *precision == 0 || *precision > 38 || scale.unsigned_abs() > *precision =>
14772 {
14773 return Err(MongrelError::Schema(format!(
14774 "column {:?} has invalid decimal precision or scale",
14775 column.name
14776 )));
14777 }
14778 TypeId::Enum { variants }
14779 if variants.is_empty()
14780 || variants.iter().any(String::is_empty)
14781 || variants.iter().collect::<HashSet<_>>().len() != variants.len() =>
14782 {
14783 return Err(MongrelError::Schema(format!(
14784 "column {:?} has invalid enum variants",
14785 column.name
14786 )));
14787 }
14788 _ => {}
14789 }
14790 }
14791 let mut index_names = HashSet::new();
14792 for index in &schema.indexes {
14793 index.validate_options()?;
14794 if index.name.is_empty()
14795 || !index_names.insert(index.name.as_str())
14796 || schema
14797 .columns
14798 .iter()
14799 .all(|column| column.id != index.column_id)
14800 {
14801 return Err(MongrelError::Schema(format!(
14802 "recovered index {:?} references missing column {}",
14803 index.name, index.column_id
14804 )));
14805 }
14806 }
14807 let mut colocated = HashSet::new();
14808 for group in &schema.colocation {
14809 if group.is_empty()
14810 || group.iter().any(|id| !column_ids.contains(id))
14811 || group.iter().any(|id| !colocated.insert(*id))
14812 {
14813 return Err(MongrelError::Schema(
14814 "recovered schema contains invalid column co-location groups".into(),
14815 ));
14816 }
14817 }
14818
14819 let mut constraint_ids = HashSet::new();
14820 let mut constraint_names = HashSet::<String>::new();
14821 let mut validate_constraint_identity = |id: u16, name: &str| -> Result<()> {
14822 if name.is_empty()
14823 || !constraint_ids.insert(id)
14824 || !constraint_names.insert(name.to_owned())
14825 {
14826 return Err(MongrelError::Schema(
14827 "recovered schema contains duplicate or empty constraint identities".into(),
14828 ));
14829 }
14830 Ok(())
14831 };
14832 for unique in &schema.constraints.uniques {
14833 validate_constraint_identity(unique.id, &unique.name)?;
14834 if unique.columns.is_empty()
14835 || unique.columns.iter().any(|id| !column_ids.contains(id))
14836 || unique.columns.iter().collect::<HashSet<_>>().len() != unique.columns.len()
14837 {
14838 return Err(MongrelError::Schema(format!(
14839 "unique constraint {:?} has invalid columns",
14840 unique.name
14841 )));
14842 }
14843 }
14844 for foreign_key in &schema.constraints.foreign_keys {
14845 validate_constraint_identity(foreign_key.id, &foreign_key.name)?;
14846 if foreign_key.ref_table.is_empty()
14847 || foreign_key.columns.is_empty()
14848 || foreign_key.columns.len() != foreign_key.ref_columns.len()
14849 || foreign_key
14850 .columns
14851 .iter()
14852 .any(|id| !column_ids.contains(id))
14853 || foreign_key.columns.iter().collect::<HashSet<_>>().len() != foreign_key.columns.len()
14854 || foreign_key.ref_columns.iter().collect::<HashSet<_>>().len()
14855 != foreign_key.ref_columns.len()
14856 {
14857 return Err(MongrelError::Schema(format!(
14858 "foreign key {:?} has invalid columns",
14859 foreign_key.name
14860 )));
14861 }
14862 if (matches!(foreign_key.on_delete, crate::constraint::FkAction::SetNull)
14863 || matches!(foreign_key.on_update, crate::constraint::FkAction::SetNull))
14864 && foreign_key.columns.iter().any(|id| {
14865 schema
14866 .columns
14867 .iter()
14868 .find(|column| column.id == *id)
14869 .is_none_or(|column| {
14870 !column.flags.contains(crate::schema::ColumnFlags::NULLABLE)
14871 })
14872 })
14873 {
14874 return Err(MongrelError::Schema(format!(
14875 "foreign key {:?} uses SET NULL on a non-nullable column",
14876 foreign_key.name
14877 )));
14878 }
14879 }
14880 for check in &schema.constraints.checks {
14881 validate_constraint_identity(check.id, &check.name)?;
14882 check.expr.validate()?;
14883 validate_check_columns(&check.expr, &column_ids)?;
14884 }
14885 Ok(())
14886}
14887
14888fn validate_check_columns(
14889 expression: &crate::constraint::CheckExpr,
14890 column_ids: &HashSet<u16>,
14891) -> Result<()> {
14892 use crate::constraint::CheckExpr;
14893 match expression {
14894 CheckExpr::Col(id) | CheckExpr::IsNull(id) | CheckExpr::IsNotNull(id) => {
14895 if column_ids.contains(id) {
14896 Ok(())
14897 } else {
14898 Err(MongrelError::Schema(format!(
14899 "check constraint references unknown column {id}"
14900 )))
14901 }
14902 }
14903 CheckExpr::Regex { col, .. } => {
14904 if column_ids.contains(col) {
14905 Ok(())
14906 } else {
14907 Err(MongrelError::Schema(format!(
14908 "check constraint references unknown column {col}"
14909 )))
14910 }
14911 }
14912 CheckExpr::Add(left, right)
14913 | CheckExpr::Sub(left, right)
14914 | CheckExpr::Mul(left, right)
14915 | CheckExpr::Div(left, right)
14916 | CheckExpr::Mod(left, right)
14917 | CheckExpr::Eq(left, right)
14918 | CheckExpr::Ne(left, right)
14919 | CheckExpr::Lt(left, right)
14920 | CheckExpr::Le(left, right)
14921 | CheckExpr::Gt(left, right)
14922 | CheckExpr::Ge(left, right)
14923 | CheckExpr::And(left, right)
14924 | CheckExpr::Or(left, right) => {
14925 validate_check_columns(left, column_ids)?;
14926 validate_check_columns(right, column_ids)
14927 }
14928 CheckExpr::Not(inner) => validate_check_columns(inner, column_ids),
14929 CheckExpr::True | CheckExpr::Lit(_) => Ok(()),
14930 }
14931}
14932
14933fn validate_catalog_transition(current: &Catalog, next: &Catalog) -> Result<()> {
14934 for (name, prior, candidate) in [
14935 ("db_epoch", current.db_epoch, next.db_epoch),
14936 ("next_table_id", current.next_table_id, next.next_table_id),
14937 (
14938 "next_segment_no",
14939 current.next_segment_no,
14940 next.next_segment_no,
14941 ),
14942 ("next_user_id", current.next_user_id, next.next_user_id),
14943 (
14944 "security_version",
14945 current.security_version,
14946 next.security_version,
14947 ),
14948 ] {
14949 if candidate < prior {
14950 return Err(MongrelError::Schema(format!(
14951 "catalog snapshot rolls back {name} from {prior} to {candidate}"
14952 )));
14953 }
14954 }
14955 for prior in ¤t.tables {
14956 let Some(candidate) = next
14957 .tables
14958 .iter()
14959 .find(|entry| entry.table_id == prior.table_id)
14960 else {
14961 return Err(MongrelError::Schema(format!(
14962 "catalog snapshot removes table identity {}",
14963 prior.table_id
14964 )));
14965 };
14966 if candidate.created_epoch != prior.created_epoch
14967 || candidate.schema.schema_id < prior.schema.schema_id
14968 || matches!(prior.state, TableState::Dropped { .. })
14969 && !matches!(candidate.state, TableState::Dropped { .. })
14970 {
14971 return Err(MongrelError::Schema(format!(
14972 "catalog snapshot rolls back table identity {}",
14973 prior.table_id
14974 )));
14975 }
14976 }
14977 for prior in ¤t.users {
14978 if let Some(candidate) = next.users.iter().find(|user| user.id == prior.id) {
14979 if candidate.username != prior.username
14980 || candidate.created_epoch != prior.created_epoch
14981 {
14982 return Err(MongrelError::Schema(format!(
14983 "catalog snapshot reuses user identity {}",
14984 prior.id
14985 )));
14986 }
14987 }
14988 }
14989 Ok(())
14990}
14991
14992fn validate_recovered_catalog(catalog: &Catalog) -> Result<()> {
14993 let mut table_ids = HashSet::new();
14994 let mut active_names = HashSet::new();
14995 let mut max_table_id = None::<u64>;
14996 for entry in &catalog.tables {
14997 if !table_ids.insert(entry.table_id) {
14998 return Err(MongrelError::Schema(format!(
14999 "catalog contains duplicate table id {}",
15000 entry.table_id
15001 )));
15002 }
15003 max_table_id = Some(max_table_id.map_or(entry.table_id, |value| value.max(entry.table_id)));
15004 if entry.name.is_empty() || entry.created_epoch > catalog.db_epoch {
15005 return Err(MongrelError::Schema(format!(
15006 "catalog table {} has invalid name or creation epoch",
15007 entry.table_id
15008 )));
15009 }
15010 validate_recovered_schema(&entry.schema)?;
15011 match &entry.state {
15012 TableState::Live => {
15013 if !active_names.insert(entry.name.as_str()) {
15014 return Err(MongrelError::Schema(format!(
15015 "catalog contains duplicate active table name {:?}",
15016 entry.name
15017 )));
15018 }
15019 }
15020 TableState::Dropped { at_epoch } => {
15021 if *at_epoch < entry.created_epoch || *at_epoch > catalog.db_epoch {
15022 return Err(MongrelError::Schema(format!(
15023 "catalog table {} has invalid drop epoch {at_epoch}",
15024 entry.table_id
15025 )));
15026 }
15027 }
15028 TableState::Building {
15029 intended_name,
15030 query_id,
15031 replaces_table_id,
15032 ..
15033 } => {
15034 if intended_name.is_empty() || query_id.is_empty() {
15035 return Err(MongrelError::Schema(format!(
15036 "building table {} has empty identity fields",
15037 entry.table_id
15038 )));
15039 }
15040 if !active_names.insert(entry.name.as_str()) {
15041 return Err(MongrelError::Schema(format!(
15042 "catalog contains duplicate active/building table name {:?}",
15043 entry.name
15044 )));
15045 }
15046 if replaces_table_id.is_some_and(|id| id == entry.table_id) {
15047 return Err(MongrelError::Schema(
15048 "building table cannot replace itself".into(),
15049 ));
15050 }
15051 }
15052 }
15053 }
15054 if let Some(maximum) = max_table_id {
15055 let required = maximum
15056 .checked_add(1)
15057 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
15058 if catalog.next_table_id < required {
15059 return Err(MongrelError::Schema(format!(
15060 "catalog next_table_id {} precedes required {required}",
15061 catalog.next_table_id
15062 )));
15063 }
15064 }
15065 for entry in &catalog.tables {
15066 if let TableState::Building {
15067 replaces_table_id: Some(replaced),
15068 ..
15069 } = entry.state
15070 {
15071 if !table_ids.contains(&replaced) {
15072 return Err(MongrelError::Schema(format!(
15073 "building table {} replaces unknown table {replaced}",
15074 entry.table_id
15075 )));
15076 }
15077 }
15078 }
15079 for entry in &catalog.tables {
15080 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
15081 validate_foreign_key_targets(catalog, &entry.schema)?;
15082 }
15083 }
15084
15085 let mut external_names = HashSet::new();
15086 for entry in &catalog.external_tables {
15087 entry.validate()?;
15088 validate_recovered_schema(&entry.declared_schema)?;
15089 if !entry.declared_schema.constraints.is_empty() {
15090 return Err(MongrelError::Schema(format!(
15091 "external table {:?} cannot carry engine-enforced constraints",
15092 entry.name
15093 )));
15094 }
15095 if entry.created_epoch > catalog.db_epoch
15096 || !external_names.insert(entry.name.as_str())
15097 || active_names.contains(entry.name.as_str())
15098 {
15099 return Err(MongrelError::Schema(format!(
15100 "invalid or duplicate external table {:?}",
15101 entry.name
15102 )));
15103 }
15104 }
15105
15106 let mut procedure_names = HashSet::new();
15107 for entry in &catalog.procedures {
15108 entry.procedure.validate()?;
15109 if entry.procedure.created_epoch > entry.procedure.updated_epoch
15110 || entry.procedure.updated_epoch > catalog.db_epoch
15111 || !procedure_names.insert(entry.procedure.name.as_str())
15112 {
15113 return Err(MongrelError::Schema(format!(
15114 "invalid or duplicate procedure {:?}",
15115 entry.procedure.name
15116 )));
15117 }
15118 validate_recovered_procedure_references(catalog, &entry.procedure)?;
15119 }
15120
15121 let mut trigger_names = HashSet::new();
15122 for entry in &catalog.triggers {
15123 entry.trigger.validate()?;
15124 if entry.trigger.created_epoch > entry.trigger.updated_epoch
15125 || entry.trigger.updated_epoch > catalog.db_epoch
15126 || !trigger_names.insert(entry.trigger.name.as_str())
15127 {
15128 return Err(MongrelError::Schema(format!(
15129 "invalid or duplicate trigger {:?}",
15130 entry.trigger.name
15131 )));
15132 }
15133 validate_recovered_trigger_references(catalog, &entry.trigger)?;
15134 }
15135
15136 let mut views = HashSet::new();
15137 for view in &catalog.materialized_views {
15138 let target = catalog.live(&view.name).ok_or_else(|| {
15139 MongrelError::Schema(format!(
15140 "materialized view {:?} has no live table",
15141 view.name
15142 ))
15143 })?;
15144 if view.name.is_empty()
15145 || view.query.trim().is_empty()
15146 || view.last_refresh_epoch > catalog.db_epoch
15147 || !views.insert(view.name.as_str())
15148 {
15149 return Err(MongrelError::Schema(format!(
15150 "materialized view {:?} has no unique live table",
15151 view.name
15152 )));
15153 }
15154 if let Some(incremental) = &view.incremental {
15155 let source = catalog.live(&incremental.source_table).ok_or_else(|| {
15156 MongrelError::Schema(format!(
15157 "materialized view {:?} references missing source {:?}",
15158 view.name, incremental.source_table
15159 ))
15160 })?;
15161 if source.table_id != incremental.source_table_id
15162 || source
15163 .schema
15164 .columns
15165 .iter()
15166 .all(|column| column.id != incremental.group_column)
15167 {
15168 return Err(MongrelError::Schema(format!(
15169 "materialized view {:?} has invalid incremental source",
15170 view.name
15171 )));
15172 }
15173 let target_ids = target
15174 .schema
15175 .columns
15176 .iter()
15177 .map(|column| column.id)
15178 .collect::<HashSet<_>>();
15179 let mut output_ids = HashSet::new();
15180 let count_outputs = incremental
15181 .outputs
15182 .iter()
15183 .filter(|output| {
15184 matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
15185 })
15186 .count();
15187 if incremental.checkpoint_event_id.is_empty()
15188 || !target_ids.contains(&incremental.group_output_column)
15189 || !target_ids.contains(&incremental.count_output_column)
15190 || incremental.outputs.is_empty()
15191 || count_outputs != 1
15192 || incremental.outputs.iter().any(|output| {
15193 !target_ids.contains(&output.output_column)
15194 || output.output_column == incremental.group_output_column
15195 || !output_ids.insert(output.output_column)
15196 || matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
15197 && output.output_column != incremental.count_output_column
15198 || match output.kind {
15199 crate::catalog::IncrementalAggregateKind::Sum { source_column } => {
15200 source
15201 .schema
15202 .columns
15203 .iter()
15204 .all(|column| column.id != source_column)
15205 }
15206 crate::catalog::IncrementalAggregateKind::Count => false,
15207 }
15208 })
15209 {
15210 return Err(MongrelError::Schema(format!(
15211 "materialized view {:?} has invalid incremental outputs",
15212 view.name
15213 )));
15214 }
15215 }
15216 }
15217
15218 validate_security_catalog(catalog, &catalog.security)?;
15219 validate_recovered_auth_catalog(catalog)?;
15220 Ok(())
15221}
15222
15223fn repair_catalog_allocator_counters(catalog: &mut Catalog) -> Result<bool> {
15224 let mut changed = false;
15225 if let Some(maximum) = catalog.tables.iter().map(|entry| entry.table_id).max() {
15226 let required = maximum
15227 .checked_add(1)
15228 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
15229 if catalog.next_table_id < required {
15230 catalog.next_table_id = required;
15231 changed = true;
15232 }
15233 }
15234 if let Some(maximum) = catalog.users.iter().map(|user| user.id).max() {
15235 let required = maximum
15236 .checked_add(1)
15237 .ok_or_else(|| MongrelError::Full("user id namespace exhausted".into()))?;
15238 if catalog.next_user_id < required {
15239 catalog.next_user_id = required;
15240 changed = true;
15241 }
15242 }
15243 Ok(changed)
15244}
15245
15246fn validate_foreign_key_targets(catalog: &Catalog, schema: &Schema) -> Result<()> {
15247 for foreign_key in &schema.constraints.foreign_keys {
15248 let parent = catalog.live(&foreign_key.ref_table).ok_or_else(|| {
15249 MongrelError::Schema(format!(
15250 "foreign key {:?} references unknown live table {:?}",
15251 foreign_key.name, foreign_key.ref_table
15252 ))
15253 })?;
15254 let referenced_unique = parent
15255 .schema
15256 .constraints
15257 .uniques
15258 .iter()
15259 .any(|unique| unique.columns == foreign_key.ref_columns)
15260 || foreign_key.ref_columns.len() == 1
15261 && parent
15262 .schema
15263 .primary_key()
15264 .is_some_and(|column| column.id == foreign_key.ref_columns[0]);
15265 if !referenced_unique {
15266 return Err(MongrelError::Schema(format!(
15267 "foreign key {:?} does not reference a unique key",
15268 foreign_key.name
15269 )));
15270 }
15271 for (local_id, parent_id) in foreign_key.columns.iter().zip(&foreign_key.ref_columns) {
15272 let local = schema.columns.iter().find(|column| column.id == *local_id);
15273 let referenced = parent
15274 .schema
15275 .columns
15276 .iter()
15277 .find(|column| column.id == *parent_id);
15278 if local
15279 .zip(referenced)
15280 .is_none_or(|(local, referenced)| local.ty != referenced.ty)
15281 {
15282 return Err(MongrelError::Schema(format!(
15283 "foreign key {:?} has missing or incompatible columns",
15284 foreign_key.name
15285 )));
15286 }
15287 }
15288 }
15289 Ok(())
15290}
15291
15292fn validate_recovered_procedure_references(
15293 catalog: &Catalog,
15294 procedure: &StoredProcedure,
15295) -> Result<()> {
15296 for step in &procedure.body.steps {
15297 let Some(table_name) = step.table() else {
15298 continue;
15299 };
15300 let schema = &catalog
15301 .live(table_name)
15302 .ok_or_else(|| {
15303 MongrelError::Schema(format!(
15304 "procedure {:?} references unknown table {table_name:?}",
15305 procedure.name
15306 ))
15307 })?
15308 .schema;
15309 match step {
15310 ProcedureStep::NativeQuery {
15311 conditions,
15312 projection,
15313 ..
15314 } => {
15315 for condition in conditions {
15316 validate_condition_columns(condition, schema)?;
15317 }
15318 for id in projection.iter().flatten() {
15319 validate_column_id(*id, schema)?;
15320 }
15321 }
15322 ProcedureStep::Put { cells, .. } => {
15323 for cell in cells {
15324 validate_column_id(cell.column_id, schema)?;
15325 }
15326 }
15327 ProcedureStep::Upsert {
15328 cells,
15329 update_cells,
15330 ..
15331 } => {
15332 for cell in cells.iter().chain(update_cells.iter().flatten()) {
15333 validate_column_id(cell.column_id, schema)?;
15334 }
15335 }
15336 ProcedureStep::DeleteByPk { .. } if schema.primary_key().is_none() => {
15337 return Err(MongrelError::Schema(format!(
15338 "procedure {:?} deletes by primary key on table without one",
15339 procedure.name
15340 )));
15341 }
15342 ProcedureStep::DeleteByPk { .. }
15343 | ProcedureStep::DeleteRows { .. }
15344 | ProcedureStep::SqlQuery { .. } => {}
15345 }
15346 }
15347 Ok(())
15348}
15349
15350fn validate_recovered_trigger_references(catalog: &Catalog, trigger: &StoredTrigger) -> Result<()> {
15351 let target_schema = match &trigger.target {
15352 TriggerTarget::Table(name) => catalog
15353 .live(name)
15354 .ok_or_else(|| {
15355 MongrelError::Schema(format!(
15356 "trigger {:?} references unknown table {name:?}",
15357 trigger.name
15358 ))
15359 })?
15360 .schema
15361 .clone(),
15362 TriggerTarget::View(_) => Schema {
15363 columns: trigger.target_columns.clone(),
15364 ..Schema::default()
15365 },
15366 };
15367 for column in &trigger.update_of {
15368 if target_schema.column(column).is_none() {
15369 return Err(MongrelError::Schema(format!(
15370 "trigger {:?} references unknown UPDATE OF column {column:?}",
15371 trigger.name
15372 )));
15373 }
15374 }
15375 if let Some(expr) = &trigger.when {
15376 validate_trigger_expr(expr, &target_schema, trigger.event)?;
15377 }
15378 let mut selects = HashMap::new();
15379 for step in &trigger.program.steps {
15380 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before {
15381 return Err(MongrelError::Schema(
15382 "SetNew is only valid in BEFORE triggers".into(),
15383 ));
15384 }
15385 validate_trigger_step(step, catalog, &target_schema, trigger.event, &mut selects)?;
15386 }
15387 Ok(())
15388}
15389
15390fn validate_recovered_auth_catalog(catalog: &Catalog) -> Result<()> {
15391 let mut role_names = HashSet::new();
15392 for role in &catalog.roles {
15393 if role.name.is_empty()
15394 || role.created_epoch > catalog.db_epoch
15395 || !role_names.insert(role.name.as_str())
15396 {
15397 return Err(MongrelError::Schema(format!(
15398 "invalid or duplicate role {:?}",
15399 role.name
15400 )));
15401 }
15402 for permission in &role.permissions {
15403 if let Some(table) = permission_table(permission) {
15404 let schema = catalog
15405 .live(table)
15406 .map(|entry| &entry.schema)
15407 .or_else(|| {
15408 catalog
15409 .external_tables
15410 .iter()
15411 .find(|entry| entry.name == table)
15412 .map(|entry| &entry.declared_schema)
15413 })
15414 .ok_or_else(|| {
15415 MongrelError::Schema(format!(
15416 "role {:?} references unknown table {table:?}",
15417 role.name
15418 ))
15419 })?;
15420 let columns = match permission {
15421 crate::auth::Permission::SelectColumns { columns, .. }
15422 | crate::auth::Permission::InsertColumns { columns, .. }
15423 | crate::auth::Permission::UpdateColumns { columns, .. } => Some(columns),
15424 _ => None,
15425 };
15426 if columns.is_some_and(|columns| {
15427 columns.is_empty()
15428 || columns.iter().any(|column| schema.column(column).is_none())
15429 }) {
15430 return Err(MongrelError::Schema(format!(
15431 "role {:?} contains invalid column permissions",
15432 role.name
15433 )));
15434 }
15435 }
15436 }
15437 }
15438 let mut user_ids = HashSet::new();
15439 let mut usernames = HashSet::new();
15440 let mut maximum_user_id = 0;
15441 for user in &catalog.users {
15442 maximum_user_id = maximum_user_id.max(user.id);
15443 if user.id == 0
15444 || user.username.is_empty()
15445 || user.password_hash.is_empty()
15446 || user.created_epoch > catalog.db_epoch
15447 || !user_ids.insert(user.id)
15448 || !usernames.insert(user.username.as_str())
15449 || user
15450 .roles
15451 .iter()
15452 .any(|role| !role_names.contains(role.as_str()))
15453 {
15454 return Err(MongrelError::Schema(format!(
15455 "invalid or duplicate user {:?}",
15456 user.username
15457 )));
15458 }
15459 }
15460 if !catalog.users.is_empty() && catalog.next_user_id <= maximum_user_id {
15461 return Err(MongrelError::Schema(
15462 "catalog next_user_id does not advance beyond existing user ids".into(),
15463 ));
15464 }
15465 if catalog.require_auth && !catalog.users.iter().any(|user| user.is_admin) {
15466 return Err(MongrelError::Schema(
15467 "authenticated catalog has no administrator".into(),
15468 ));
15469 }
15470 Ok(())
15471}
15472
15473fn validate_recovered_storage_plan(
15474 root: &Path,
15475 durable_root: Option<&crate::durable_file::DurableRoot>,
15476 catalog: &Catalog,
15477 created_table_ids: &HashSet<u64>,
15478 ttl_updates: &HashMap<u64, (Option<crate::manifest::TtlPolicy>, u64)>,
15479 meta_dek: Option<&[u8; META_DEK_LEN]>,
15480) -> Result<Vec<u64>> {
15481 const MAX_SCHEMA_BYTES: u64 = 16 * 1024 * 1024;
15482 let mut reconcile = Vec::new();
15483 for entry in &catalog.tables {
15484 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
15485 continue;
15486 }
15487 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
15488 let table_dir = root.join(TABLES_DIR).join(entry.table_id.to_string());
15489 let table_exists = match durable_root {
15490 Some(root) => match root.open_directory(&relative_dir) {
15491 Ok(_) => true,
15492 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
15493 Err(error) => return Err(error.into()),
15494 },
15495 None => table_dir.is_dir(),
15496 };
15497 if !table_exists {
15498 if created_table_ids.contains(&entry.table_id) {
15499 reconcile.push(entry.table_id);
15500 continue;
15501 }
15502 return Err(MongrelError::NotFound(format!(
15503 "catalog table {} storage is missing",
15504 entry.table_id
15505 )));
15506 }
15507 let manifest_result = match durable_root {
15508 Some(root) => crate::manifest::read_durable(root, &relative_dir, meta_dek),
15509 None => crate::manifest::read(&table_dir, meta_dek),
15510 };
15511 let manifest = match manifest_result {
15512 Ok(manifest) => manifest,
15513 Err(MongrelError::Io(error))
15514 if created_table_ids.contains(&entry.table_id)
15515 && error.kind() == std::io::ErrorKind::NotFound =>
15516 {
15517 reconcile.push(entry.table_id);
15518 continue;
15519 }
15520 Err(error) => return Err(error),
15521 };
15522 if manifest.table_id != entry.table_id {
15523 return Err(MongrelError::Conflict(format!(
15524 "catalog table {} storage identity mismatch",
15525 entry.table_id
15526 )));
15527 }
15528 let schema_result = match durable_root {
15529 Some(root) => root
15530 .open_regular(relative_dir.join(crate::engine::SCHEMA_FILENAME))
15531 .map_err(MongrelError::from),
15532 None => crate::durable_file::open_regular_nofollow(
15533 &table_dir.join(crate::engine::SCHEMA_FILENAME),
15534 ),
15535 };
15536 let file = match schema_result {
15537 Ok(file) => file,
15538 Err(MongrelError::Io(error))
15539 if created_table_ids.contains(&entry.table_id)
15540 && error.kind() == std::io::ErrorKind::NotFound =>
15541 {
15542 reconcile.push(entry.table_id);
15543 continue;
15544 }
15545 Err(error) => return Err(error),
15546 };
15547 let length = file.metadata()?.len();
15548 if length > MAX_SCHEMA_BYTES {
15549 return Err(MongrelError::ResourceLimitExceeded {
15550 resource: "recovered schema bytes",
15551 requested: usize::try_from(length).unwrap_or(usize::MAX),
15552 limit: MAX_SCHEMA_BYTES as usize,
15553 });
15554 }
15555 let disk_schema: Schema = serde_json::from_reader(file.take(MAX_SCHEMA_BYTES + 1))
15556 .map_err(|error| MongrelError::Schema(format!("decode recovered schema: {error}")))?;
15557 if manifest.schema_id != entry.schema.schema_id
15558 || crate::wal::DdlOp::encode_schema(&disk_schema)?
15559 != crate::wal::DdlOp::encode_schema(&entry.schema)?
15560 {
15561 reconcile.push(entry.table_id);
15562 }
15563 }
15564 for table_id in ttl_updates.keys() {
15565 if !catalog.tables.iter().any(|entry| {
15566 entry.table_id == *table_id
15567 && matches!(entry.state, TableState::Live | TableState::Building { .. })
15568 }) {
15569 continue;
15570 }
15571 let relative_dir = Path::new(TABLES_DIR).join(table_id.to_string());
15572 let table_exists = match durable_root {
15573 Some(root) => match root.open_directory(&relative_dir) {
15574 Ok(_) => true,
15575 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
15576 Err(error) => return Err(error.into()),
15577 },
15578 None => root.join(&relative_dir).is_dir(),
15579 };
15580 if !table_exists && !created_table_ids.contains(table_id) {
15581 return Err(MongrelError::NotFound(format!(
15582 "TTL recovery table {table_id} storage is missing"
15583 )));
15584 }
15585 }
15586 reconcile.sort_unstable();
15587 reconcile.dedup();
15588 Ok(reconcile)
15589}
15590
15591fn validate_catalog_table_storage(
15592 root: &crate::durable_file::DurableRoot,
15593 catalog: &Catalog,
15594 meta_dek: Option<&[u8; META_DEK_LEN]>,
15595) -> Result<()> {
15596 for entry in &catalog.tables {
15597 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
15598 continue;
15599 }
15600 let table_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
15601 let manifest = crate::manifest::read_durable(root, &table_dir, meta_dek)?;
15602 if manifest.table_id != entry.table_id || manifest.schema_id != entry.schema.schema_id {
15603 return Err(MongrelError::Conflict(format!(
15604 "catalog table {} storage identity mismatch",
15605 entry.table_id
15606 )));
15607 }
15608 root.open_regular(table_dir.join(crate::engine::SCHEMA_FILENAME))?;
15609 }
15610 Ok(())
15611}
15612
15613fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> Result<bool> {
15614 match schema.columns.iter_mut().find(|c| c.id == column.id) {
15615 Some(existing) if *existing == column => Ok(false),
15616 Some(existing) => {
15617 *existing = column;
15618 schema.schema_id = schema
15619 .schema_id
15620 .checked_add(1)
15621 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
15622 Ok(true)
15623 }
15624 None => {
15625 schema.columns.push(column);
15626 schema.schema_id = schema
15627 .schema_id
15628 .checked_add(1)
15629 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
15630 Ok(true)
15631 }
15632 }
15633}
15634
15635fn permission_table(permission: &crate::auth::Permission) -> Option<&str> {
15636 use crate::auth::Permission;
15637 match permission {
15638 Permission::Select { table }
15639 | Permission::Insert { table }
15640 | Permission::Update { table }
15641 | Permission::Delete { table }
15642 | Permission::SelectColumns { table, .. }
15643 | Permission::InsertColumns { table, .. }
15644 | Permission::UpdateColumns { table, .. } => Some(table),
15645 Permission::All | Permission::Ddl | Permission::Admin => None,
15646 }
15647}
15648
15649fn apply_rebuilding_publish(
15650 catalog: &mut Catalog,
15651 table_id: u64,
15652 replaced_table_id: u64,
15653 new_name: &str,
15654 epoch: u64,
15655) -> Result<bool> {
15656 let already_published = catalog.tables.iter().any(|entry| {
15657 entry.table_id == table_id
15658 && entry.name == new_name
15659 && matches!(entry.state, TableState::Live)
15660 }) && catalog.tables.iter().any(|entry| {
15661 entry.table_id == replaced_table_id && matches!(entry.state, TableState::Dropped { .. })
15662 });
15663 if already_published {
15664 return Ok(false);
15665 }
15666 let schema = catalog
15667 .tables
15668 .iter()
15669 .find(|entry| entry.table_id == table_id)
15670 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?
15671 .schema
15672 .clone();
15673 let replaced = catalog
15674 .tables
15675 .iter_mut()
15676 .find(|entry| entry.table_id == replaced_table_id)
15677 .ok_or_else(|| MongrelError::NotFound(format!("table id {replaced_table_id} not found")))?;
15678 replaced.state = TableState::Dropped { at_epoch: epoch };
15679 let replacement = catalog
15680 .tables
15681 .iter_mut()
15682 .find(|entry| entry.table_id == table_id)
15683 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?;
15684 replacement.name = new_name.to_string();
15685 replacement.state = TableState::Live;
15686
15687 for role in &mut catalog.roles {
15688 role.permissions.retain_mut(|permission| {
15689 retain_rebuilt_permission_columns(permission, new_name, &schema)
15690 });
15691 }
15692 for definition in &mut catalog.materialized_views {
15693 if let Some(incremental) = definition.incremental.as_mut() {
15694 if incremental.source_table == new_name
15695 && incremental.source_table_id == replaced_table_id
15696 {
15697 incremental.source_table_id = table_id;
15698 }
15699 }
15700 }
15701 advance_security_version(catalog)?;
15702 Ok(true)
15703}
15704
15705fn retain_rebuilt_permission_columns(
15706 permission: &mut crate::auth::Permission,
15707 target_table: &str,
15708 schema: &Schema,
15709) -> bool {
15710 use crate::auth::Permission;
15711 let columns = match permission {
15712 Permission::SelectColumns { table, columns }
15713 | Permission::InsertColumns { table, columns }
15714 | Permission::UpdateColumns { table, columns }
15715 if table == target_table =>
15716 {
15717 Some(columns)
15718 }
15719 _ => None,
15720 };
15721 if let Some(columns) = columns {
15722 columns.retain(|column| schema.column(column).is_some());
15723 !columns.is_empty()
15724 } else {
15725 true
15726 }
15727}
15728
15729fn rename_permission_table(permission: &mut crate::auth::Permission, old: &str, new: &str) {
15730 use crate::auth::Permission;
15731 let table = match permission {
15732 Permission::Select { table }
15733 | Permission::Insert { table }
15734 | Permission::Update { table }
15735 | Permission::Delete { table }
15736 | Permission::SelectColumns { table, .. }
15737 | Permission::InsertColumns { table, .. }
15738 | Permission::UpdateColumns { table, .. } => Some(table),
15739 Permission::All | Permission::Ddl | Permission::Admin => None,
15740 };
15741 if let Some(table) = table.filter(|table| table.as_str() == old) {
15742 *table = new.to_string();
15743 }
15744}
15745
15746fn rename_permission_column(
15747 permission: &mut crate::auth::Permission,
15748 target_table: &str,
15749 old: &str,
15750 new: &str,
15751) {
15752 use crate::auth::Permission;
15753 let columns = match permission {
15754 Permission::SelectColumns { table, columns }
15755 | Permission::InsertColumns { table, columns }
15756 | Permission::UpdateColumns { table, columns }
15757 if table == target_table =>
15758 {
15759 Some(columns)
15760 }
15761 _ => None,
15762 };
15763 if let Some(column) = columns
15764 .into_iter()
15765 .flatten()
15766 .find(|column| column.as_str() == old)
15767 {
15768 *column = new.to_string();
15769 }
15770}
15771
15772fn merge_permission(
15773 permissions: &mut Vec<crate::auth::Permission>,
15774 permission: crate::auth::Permission,
15775) {
15776 use crate::auth::Permission;
15777 let (kind, table, mut columns) = match permission {
15778 Permission::SelectColumns { table, columns } => (0, table, columns),
15779 Permission::InsertColumns { table, columns } => (1, table, columns),
15780 Permission::UpdateColumns { table, columns } => (2, table, columns),
15781 permission if !permissions.contains(&permission) => {
15782 permissions.push(permission);
15783 return;
15784 }
15785 _ => return,
15786 };
15787 for permission in permissions.iter_mut() {
15788 let existing = match permission {
15789 Permission::SelectColumns {
15790 table: existing_table,
15791 columns,
15792 } if kind == 0 && existing_table == &table => Some(columns),
15793 Permission::InsertColumns {
15794 table: existing_table,
15795 columns,
15796 } if kind == 1 && existing_table == &table => Some(columns),
15797 Permission::UpdateColumns {
15798 table: existing_table,
15799 columns,
15800 } if kind == 2 && existing_table == &table => Some(columns),
15801 _ => None,
15802 };
15803 if let Some(existing) = existing {
15804 existing.append(&mut columns);
15805 existing.sort();
15806 existing.dedup();
15807 return;
15808 }
15809 }
15810 columns.sort();
15811 columns.dedup();
15812 let permission = if kind == 0 {
15813 Permission::SelectColumns { table, columns }
15814 } else if kind == 1 {
15815 Permission::InsertColumns { table, columns }
15816 } else {
15817 Permission::UpdateColumns { table, columns }
15818 };
15819 permissions.push(permission);
15820}
15821
15822fn revoke_permission_from(
15823 permissions: &mut Vec<crate::auth::Permission>,
15824 revoked: &crate::auth::Permission,
15825) {
15826 use crate::auth::Permission;
15827 let revoked_columns = match revoked {
15828 Permission::SelectColumns { table, columns } => Some((0, table, columns)),
15829 Permission::InsertColumns { table, columns } => Some((1, table, columns)),
15830 Permission::UpdateColumns { table, columns } => Some((2, table, columns)),
15831 _ => None,
15832 };
15833 let Some((kind, table, columns)) = revoked_columns else {
15834 permissions.retain(|permission| permission != revoked);
15835 return;
15836 };
15837 for permission in permissions.iter_mut() {
15838 let current = match permission {
15839 Permission::SelectColumns {
15840 table: current_table,
15841 columns,
15842 } if kind == 0 && current_table == table => Some(columns),
15843 Permission::InsertColumns {
15844 table: current_table,
15845 columns,
15846 } if kind == 1 && current_table == table => Some(columns),
15847 Permission::UpdateColumns {
15848 table: current_table,
15849 columns,
15850 } if kind == 2 && current_table == table => Some(columns),
15851 _ => None,
15852 };
15853 if let Some(current) = current {
15854 current.retain(|column| !columns.contains(column));
15855 }
15856 }
15857 permissions.retain(|permission| match permission {
15858 Permission::SelectColumns { columns, .. }
15859 | Permission::InsertColumns { columns, .. }
15860 | Permission::UpdateColumns { columns, .. } => !columns.is_empty(),
15861 _ => true,
15862 });
15863}
15864
15865fn validate_security_catalog(
15866 catalog: &Catalog,
15867 security: &crate::security::SecurityCatalog,
15868) -> Result<()> {
15869 let mut policy_names = HashSet::new();
15870 for table in &security.rls_tables {
15871 if catalog.live(table).is_none() {
15872 return Err(MongrelError::NotFound(format!(
15873 "RLS table {table:?} not found"
15874 )));
15875 }
15876 }
15877 for policy in &security.policies {
15878 if !policy_names.insert((policy.table.clone(), policy.name.clone())) {
15879 return Err(MongrelError::InvalidArgument(format!(
15880 "duplicate policy {:?} on {:?}",
15881 policy.name, policy.table
15882 )));
15883 }
15884 let schema = &catalog
15885 .live(&policy.table)
15886 .ok_or_else(|| {
15887 MongrelError::NotFound(format!("policy table {:?} not found", policy.table))
15888 })?
15889 .schema;
15890 if let Some(expression) = &policy.using {
15891 validate_security_expression(expression, schema)?;
15892 }
15893 if let Some(expression) = &policy.with_check {
15894 validate_security_expression(expression, schema)?;
15895 }
15896 }
15897 let mut mask_names = HashSet::new();
15898 for mask in &security.masks {
15899 if !mask_names.insert((mask.table.clone(), mask.name.clone())) {
15900 return Err(MongrelError::InvalidArgument(format!(
15901 "duplicate mask {:?} on {:?}",
15902 mask.name, mask.table
15903 )));
15904 }
15905 let column = catalog
15906 .live(&mask.table)
15907 .and_then(|entry| {
15908 entry
15909 .schema
15910 .columns
15911 .iter()
15912 .find(|column| column.id == mask.column)
15913 })
15914 .ok_or_else(|| {
15915 MongrelError::NotFound(format!(
15916 "mask column {} on {:?} not found",
15917 mask.column, mask.table
15918 ))
15919 })?;
15920 if matches!(
15921 mask.strategy,
15922 crate::security::MaskStrategy::Redact { .. } | crate::security::MaskStrategy::Sha256
15923 ) && !matches!(column.ty, TypeId::Bytes | TypeId::Enum { .. })
15924 {
15925 return Err(MongrelError::InvalidArgument(format!(
15926 "mask {:?} requires a string/bytes column",
15927 mask.name
15928 )));
15929 }
15930 }
15931 Ok(())
15932}
15933
15934fn validate_security_expression(
15935 expression: &crate::security::SecurityExpr,
15936 schema: &Schema,
15937) -> Result<()> {
15938 use crate::security::SecurityExpr;
15939 match expression {
15940 SecurityExpr::True => Ok(()),
15941 SecurityExpr::ColumnEqCurrentUser { column }
15942 | SecurityExpr::ColumnEqValue { column, .. } => {
15943 if schema
15944 .columns
15945 .iter()
15946 .any(|candidate| candidate.id == *column)
15947 {
15948 Ok(())
15949 } else {
15950 Err(MongrelError::InvalidArgument(format!(
15951 "security expression references unknown column id {column}"
15952 )))
15953 }
15954 }
15955 SecurityExpr::And { left, right } | SecurityExpr::Or { left, right } => {
15956 validate_security_expression(left, schema)?;
15957 validate_security_expression(right, schema)
15958 }
15959 SecurityExpr::Not { expression } => validate_security_expression(expression, schema),
15960 }
15961}
15962
15963fn sweep_unreferenced_table_dirs(root: &Path, cat: &Catalog) -> Result<()> {
15965 let referenced = cat
15966 .tables
15967 .iter()
15968 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
15969 .map(|entry| entry.table_id)
15970 .collect::<HashSet<_>>();
15971 let tables_dir = root.join(TABLES_DIR);
15972 let entries = match std::fs::read_dir(&tables_dir) {
15973 Ok(entries) => entries,
15974 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
15975 Err(error) => return Err(error.into()),
15976 };
15977 for entry in entries {
15978 let entry = entry?;
15979 if !entry.file_type()?.is_dir() {
15980 continue;
15981 }
15982 let file_name = entry.file_name();
15983 let Some(name) = file_name.to_str() else {
15984 continue;
15985 };
15986 let Ok(table_id) = name.parse::<u64>() else {
15987 continue;
15988 };
15989 if name != table_id.to_string() {
15990 continue;
15991 }
15992 if !referenced.contains(&table_id) {
15993 crate::durable_file::remove_directory_all(&entry.path())?;
15994 }
15995 }
15996 Ok(())
15997}
15998
15999fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
16004 for entry in &cat.tables {
16005 let txn_dir = root
16006 .join(TABLES_DIR)
16007 .join(entry.table_id.to_string())
16008 .join("_txn");
16009 if txn_dir.exists() {
16010 let _ = std::fs::remove_dir_all(&txn_dir);
16011 }
16012 }
16013}
16014
16015#[cfg(test)]
16016mod write_permission_tests {
16017 use super::*;
16018 use crate::txn::Staged;
16019
16020 struct NoopExternalBridge;
16021
16022 impl ExternalTriggerBridge for NoopExternalBridge {
16023 fn apply_trigger_external_write(
16024 &self,
16025 _entry: &ExternalTableEntry,
16026 base_state: Vec<u8>,
16027 _op: ExternalTriggerWrite,
16028 ) -> Result<ExternalTriggerWriteResult> {
16029 Ok(ExternalTriggerWriteResult::new(base_state))
16030 }
16031 }
16032
16033 fn assert_txn_namespace_full<T>(result: Result<T>) {
16034 assert!(matches!(result, Err(MongrelError::Full(_))));
16035 }
16036
16037 #[test]
16038 fn every_begin_api_preserves_transaction_id_exhaustion_without_wal_mutation() {
16039 let directory = tempfile::tempdir().unwrap();
16040 let database = Database::create(directory.path()).unwrap();
16041 let generation = (*database.next_txn_id.lock() >> 32).saturating_add(1);
16042 *database.next_txn_id.lock() = generation << 32;
16043 let before = crate::wal::SharedWal::replay(directory.path())
16044 .unwrap()
16045 .len();
16046 let bridge = NoopExternalBridge;
16047
16048 assert_txn_namespace_full(database.begin().commit());
16049 assert_txn_namespace_full(database.begin_as(None).commit_with_row_ids());
16050 assert_txn_namespace_full(
16051 database
16052 .begin_with_isolation(crate::txn::IsolationLevel::Serializable)
16053 .commit(),
16054 );
16055 assert_txn_namespace_full(
16056 database
16057 .begin_with_external_trigger_bridge(&bridge)
16058 .commit(),
16059 );
16060 assert_txn_namespace_full(
16061 database
16062 .begin_with_external_trigger_bridge_as(&bridge, None)
16063 .commit_controlled(&crate::ExecutionControl::new(None), || Ok(())),
16064 );
16065
16066 assert_eq!(
16067 crate::wal::SharedWal::replay(directory.path())
16068 .unwrap()
16069 .len(),
16070 before
16071 );
16072 drop(database);
16073 Database::open(directory.path()).unwrap();
16074 }
16075
16076 #[test]
16077 fn recovered_storage_identity_mismatch_does_not_mutate_directory() {
16078 let directory = tempfile::tempdir().unwrap();
16079 let table_dir = directory.path().join("7");
16080 crate::durable_file::create_directory_all(&table_dir).unwrap();
16081 let original_schema = test_schema();
16082 crate::engine::write_schema(&table_dir, &original_schema).unwrap();
16083 let mut manifest = crate::manifest::Manifest::new(8, original_schema.schema_id);
16084 crate::manifest::write_atomic(&table_dir, &mut manifest, None).unwrap();
16085 let schema_path = table_dir.join(crate::engine::SCHEMA_FILENAME);
16086 let original_bytes = std::fs::read(&schema_path).unwrap();
16087
16088 let mut replacement_schema = original_schema;
16089 replacement_schema.schema_id += 1;
16090 assert!(matches!(
16091 ensure_recovered_table_storage(None, None, &table_dir, 7, &replacement_schema, None,),
16092 Err(MongrelError::Conflict(_))
16093 ));
16094
16095 assert_eq!(std::fs::read(schema_path).unwrap(), original_bytes);
16096 assert!(!table_dir.join(crate::engine::WAL_DIR).exists());
16097 assert!(!table_dir.join(crate::engine::RUNS_DIR).exists());
16098 assert_eq!(crate::manifest::read(&table_dir, None).unwrap().table_id, 8);
16099 }
16100
16101 #[test]
16102 fn catalog_table_missing_storage_fails_without_recreating_it() {
16103 let directory = tempfile::tempdir().unwrap();
16104 let table_dir = {
16105 let database = Database::create(directory.path()).unwrap();
16106 database.create_table("docs", test_schema()).unwrap();
16107 directory
16108 .path()
16109 .join(TABLES_DIR)
16110 .join(database.table_id("docs").unwrap().to_string())
16111 };
16112 std::fs::remove_dir_all(&table_dir).unwrap();
16113
16114 assert!(matches!(
16115 Database::open(directory.path()),
16116 Err(MongrelError::NotFound(_))
16117 ));
16118 assert!(!table_dir.exists());
16119 }
16120
16121 #[test]
16122 fn authentication_and_principal_resolution_share_one_catalog_snapshot() {
16123 let directory = tempfile::tempdir().unwrap();
16124 let database = std::sync::Arc::new(
16125 Database::create_with_credentials(directory.path(), "admin", "admin-password").unwrap(),
16126 );
16127 database.create_user("alice", "old-password").unwrap();
16128 let old_identity = database.user_identity("alice").unwrap();
16129 let (verified_tx, verified_rx) = std::sync::mpsc::channel();
16130 let (resume_tx, resume_rx) = std::sync::mpsc::channel();
16131 let (mutation_started_tx, mutation_started_rx) = std::sync::mpsc::channel();
16132 let (mutation_done_tx, mutation_done_rx) = std::sync::mpsc::channel();
16133
16134 std::thread::scope(|scope| {
16135 let authenticate = {
16136 let database = std::sync::Arc::clone(&database);
16137 scope.spawn(move || {
16138 database.authenticate_principal_inner("alice", "old-password", || {
16139 verified_tx.send(()).unwrap();
16140 resume_rx.recv().unwrap();
16141 })
16142 })
16143 };
16144 verified_rx.recv().unwrap();
16145 let mutate = {
16146 let database = std::sync::Arc::clone(&database);
16147 scope.spawn(move || {
16148 mutation_started_tx.send(()).unwrap();
16149 database.drop_user("alice").unwrap();
16150 database.create_user("alice", "new-password").unwrap();
16151 mutation_done_tx.send(()).unwrap();
16152 })
16153 };
16154 mutation_started_rx.recv().unwrap();
16155 assert!(mutation_done_rx
16156 .recv_timeout(std::time::Duration::from_millis(50))
16157 .is_err());
16158 resume_tx.send(()).unwrap();
16159 let principal = authenticate.join().unwrap().unwrap().unwrap();
16160 assert_eq!((principal.user_id, principal.created_epoch), old_identity);
16161 mutate.join().unwrap();
16162 });
16163
16164 assert_ne!(database.user_identity("alice").unwrap(), old_identity);
16165 assert!(database
16166 .authenticate_principal("alice", "old-password")
16167 .unwrap()
16168 .is_none());
16169 assert!(database
16170 .authenticate_principal("alice", "new-password")
16171 .unwrap()
16172 .is_some());
16173 }
16174
16175 #[test]
16176 fn homogeneous_batch_summarizes_to_one_permission_decision() {
16177 let staging = (0..10_050)
16178 .map(|_| {
16179 (
16180 7,
16181 Staged::Put(vec![(2, Value::Int64(2)), (1, Value::Int64(1))]),
16182 )
16183 })
16184 .collect::<Vec<_>>();
16185
16186 let needs = summarize_write_permissions(&staging);
16187 let table = needs.get(&7).unwrap();
16188 assert_eq!(needs.len(), 1);
16189 assert!(table.insert);
16190 assert_eq!(table.insert_columns, [1, 2]);
16191 assert!(!table.update);
16192 assert!(!table.delete);
16193 assert!(!table.truncate);
16194 }
16195
16196 #[test]
16197 fn mixed_writes_union_columns_and_preserve_empty_operations() {
16198 let staging = vec![
16199 (7, Staged::Put(vec![(2, Value::Int64(2))])),
16200 (7, Staged::Put(vec![(1, Value::Int64(1))])),
16201 (
16202 7,
16203 Staged::Update {
16204 row_id: RowId(1),
16205 new_row: vec![(1, Value::Int64(1)), (2, Value::Int64(2))],
16206 changed_columns: vec![2],
16207 },
16208 ),
16209 (7, Staged::Delete(RowId(2))),
16210 (8, Staged::Truncate),
16211 ];
16212
16213 let needs = summarize_write_permissions(&staging);
16214 let table = needs.get(&7).unwrap();
16215 assert_eq!(table.insert_columns, [1, 2]);
16216 assert!(table.update);
16217 assert_eq!(table.update_columns, [2]);
16218 assert!(table.delete);
16219 assert!(needs.get(&8).unwrap().truncate);
16220 }
16221
16222 #[test]
16223 fn final_permission_decisions_do_not_scale_with_rows() {
16224 let credentialless_dir = tempfile::tempdir().unwrap();
16225 let credentialless = Database::create(credentialless_dir.path()).unwrap();
16226 credentialless.create_table("docs", test_schema()).unwrap();
16227 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16228 credentialless
16229 .validate_write_permissions(&puts(credentialless.table_id("docs").unwrap()), None, None)
16230 .unwrap();
16231 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 0));
16232
16233 let authenticated_dir = tempfile::tempdir().unwrap();
16234 let authenticated =
16235 Database::create_with_credentials(authenticated_dir.path(), "admin", "admin-password")
16236 .unwrap();
16237 authenticated.create_table("docs", test_schema()).unwrap();
16238 let admin = authenticated.resolve_principal("admin").unwrap();
16239 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16240 authenticated
16241 .validate_write_permissions(
16242 &puts(authenticated.table_id("docs").unwrap()),
16243 Some(&admin),
16244 None,
16245 )
16246 .unwrap();
16247 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
16248 }
16249
16250 #[test]
16251 fn delete_batch_checks_permission_once_when_staged_and_once_when_committed() {
16252 let dir = tempfile::tempdir().unwrap();
16253 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
16254 db.create_table("docs", test_schema()).unwrap();
16255 let admin = db.resolve_principal("admin").unwrap();
16256 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16257
16258 let mut transaction = db.begin_as(Some(admin));
16259 transaction
16260 .delete_batch("docs", (0..100).map(RowId).collect())
16261 .unwrap();
16262 transaction.commit().unwrap();
16263
16264 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 2));
16265 }
16266
16267 #[test]
16268 fn truncate_validation_checks_admin_once_for_all_tables() {
16269 let dir = tempfile::tempdir().unwrap();
16270 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
16271 db.create_table("first", test_schema()).unwrap();
16272 db.create_table("second", test_schema()).unwrap();
16273 let admin = db.resolve_principal("admin").unwrap();
16274 let staging = vec![
16275 (db.table_id("first").unwrap(), Staged::Truncate),
16276 (db.table_id("second").unwrap(), Staged::Truncate),
16277 ];
16278
16279 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16280 db.validate_write_permissions(&staging, Some(&admin), None)
16281 .unwrap();
16282 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
16283 }
16284
16285 #[test]
16286 fn one_table_commit_batches_structural_work() {
16287 let dir = tempfile::tempdir().unwrap();
16288 let db = Database::create(dir.path()).unwrap();
16289 db.create_table("docs", test_schema()).unwrap();
16290 let table_id = db.table_id("docs").unwrap();
16291
16292 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(0));
16293 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
16294 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
16295 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
16296 db.transaction(|transaction| {
16297 for id in 0..100 {
16298 transaction.put("docs", vec![(1, Value::Int64(id))])?;
16299 }
16300 Ok(())
16301 })
16302 .unwrap();
16303
16304 AUTO_INCREMENT_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 2));
16305 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16306 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16307 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
16308
16309 let puts = crate::wal::SharedWal::replay(dir.path())
16310 .unwrap()
16311 .into_iter()
16312 .filter_map(|record| match record.op {
16313 crate::wal::Op::Put { table_id: id, rows } if id == table_id => Some(
16314 bincode::deserialize::<Vec<crate::memtable::Row>>(&rows)
16315 .unwrap()
16316 .len(),
16317 ),
16318 _ => None,
16319 })
16320 .collect::<Vec<_>>();
16321 assert_eq!(puts, [100]);
16322
16323 let row_ids = db
16324 .table("docs")
16325 .unwrap()
16326 .lock()
16327 .visible_rows(db.snapshot().0)
16328 .unwrap()
16329 .into_iter()
16330 .take(2)
16331 .map(|row| row.row_id)
16332 .collect::<Vec<_>>();
16333 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
16334 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
16335 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
16336 db.transaction(|transaction| {
16337 for row_id in row_ids {
16338 transaction.delete("docs", row_id)?;
16339 }
16340 Ok(())
16341 })
16342 .unwrap();
16343 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16344 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16345 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
16346
16347 let deletes = crate::wal::SharedWal::replay(dir.path())
16348 .unwrap()
16349 .into_iter()
16350 .filter_map(|record| match record.op {
16351 crate::wal::Op::Delete {
16352 table_id: id,
16353 row_ids,
16354 } if id == table_id => Some(row_ids.len()),
16355 _ => None,
16356 })
16357 .collect::<Vec<_>>();
16358 assert_eq!(deletes, [2]);
16359 }
16360
16361 fn puts(table_id: u64) -> Vec<(u64, Staged)> {
16362 (0..10_050)
16363 .map(|id| (table_id, Staged::Put(vec![(1, Value::Int64(id))])))
16364 .collect()
16365 }
16366
16367 fn test_schema() -> Schema {
16368 Schema {
16369 columns: vec![ColumnDef {
16370 id: 1,
16371 name: "id".into(),
16372 ty: TypeId::Int64,
16373 flags: crate::schema::ColumnFlags::empty()
16374 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
16375 default_value: None,
16376 }],
16377 ..Schema::default()
16378 }
16379 }
16380}
16381
16382#[cfg(test)]
16383mod cdc_bounds_tests {
16384 use super::*;
16385
16386 #[test]
16387 fn retained_byte_limit_rejects_without_allocating_payload() {
16388 let mut retained = 0;
16389 let error = charge_cdc_bytes(
16390 &mut retained,
16391 CDC_MAX_RETAINED_BYTES.saturating_add(1),
16392 "CDC retained bytes",
16393 )
16394 .unwrap_err();
16395 assert!(matches!(
16396 error,
16397 MongrelError::ResourceLimitExceeded {
16398 resource: "CDC retained bytes",
16399 ..
16400 }
16401 ));
16402 }
16403
16404 #[test]
16405 fn row_json_estimate_accounts_for_byte_array_expansion() {
16406 let row = crate::memtable::Row::new(RowId(1), Epoch(1))
16407 .with_column(1, Value::Bytes(vec![0; 1024]));
16408 assert!(cdc_row_json_bytes(&row) >= 1024 * std::mem::size_of::<serde_json::Value>());
16409 }
16410}
16411
16412#[cfg(test)]
16413mod generation_metrics_tests {
16414 use super::*;
16415 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
16416
16417 #[test]
16418 fn legacy_cow_fallback_is_measured() {
16419 let dir = tempfile::tempdir().unwrap();
16420 let table = Table::create(
16421 dir.path(),
16422 Schema {
16423 columns: vec![ColumnDef {
16424 id: 1,
16425 name: "id".into(),
16426 ty: TypeId::Int64,
16427 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
16428 default_value: None,
16429 }],
16430 ..Schema::default()
16431 },
16432 1,
16433 )
16434 .unwrap();
16435 let handle = TableHandle::from_table(table);
16436 let held = match &handle.inner {
16437 TableHandleInner::CopyOnWrite(slot) => Arc::clone(&slot.read()),
16438 TableHandleInner::Direct(_) => unreachable!(),
16439 };
16440
16441 handle.lock().set_sync_byte_threshold(1);
16442
16443 let stats = handle.generation_stats();
16444 assert_eq!(stats.cow_clone_count, 1);
16445 assert!(stats.estimated_cow_clone_bytes > 0);
16446 drop(held);
16447 }
16448}
16449
16450#[cfg(test)]
16451mod trigger_engine_tests {
16452 use super::*;
16453
16454 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
16455 WriteEvent {
16456 table: "test".into(),
16457 kind: TriggerEvent::Insert,
16458 new: Some(TriggerRowImage {
16459 columns: new_cells.iter().cloned().collect(),
16460 }),
16461 old: Some(TriggerRowImage {
16462 columns: old_cells.iter().cloned().collect(),
16463 }),
16464 changed_columns: Vec::new(),
16465 op_indices: Vec::new(),
16466 put_idx: None,
16467 trigger_stack: Vec::new(),
16468 }
16469 }
16470
16471 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
16472 WriteEvent {
16473 table: "test".into(),
16474 kind: TriggerEvent::Insert,
16475 new: Some(TriggerRowImage {
16476 columns: new_cells.iter().cloned().collect(),
16477 }),
16478 old: None,
16479 changed_columns: Vec::new(),
16480 op_indices: Vec::new(),
16481 put_idx: None,
16482 trigger_stack: Vec::new(),
16483 }
16484 }
16485
16486 #[test]
16487 fn value_order_int64_vs_float64() {
16488 assert_eq!(
16489 value_order(&Value::Int64(5), &Value::Float64(5.0)),
16490 Some(std::cmp::Ordering::Equal)
16491 );
16492 assert_eq!(
16493 value_order(&Value::Int64(5), &Value::Float64(3.0)),
16494 Some(std::cmp::Ordering::Greater)
16495 );
16496 assert_eq!(
16497 value_order(&Value::Int64(2), &Value::Float64(3.0)),
16498 Some(std::cmp::Ordering::Less)
16499 );
16500 }
16501
16502 #[test]
16503 fn value_order_null_returns_none() {
16504 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
16505 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
16506 assert_eq!(value_order(&Value::Null, &Value::Null), None);
16507 }
16508
16509 #[test]
16510 fn value_order_cross_group_returns_none() {
16511 assert_eq!(
16512 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
16513 None
16514 );
16515 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
16516 assert_eq!(
16517 value_order(
16518 &Value::Embedding(vec![1.0, 2.0]),
16519 &Value::Embedding(vec![1.0, 2.0])
16520 ),
16521 None
16522 );
16523 }
16524
16525 #[test]
16526 fn eval_trigger_expr_ranges_and_booleans() {
16527 let expr = TriggerExpr::And {
16528 left: Box::new(TriggerExpr::Gt {
16529 left: TriggerValue::NewColumn(1),
16530 right: TriggerValue::Literal(Value::Int64(0)),
16531 }),
16532 right: Box::new(TriggerExpr::Lte {
16533 left: TriggerValue::NewColumn(1),
16534 right: TriggerValue::Literal(Value::Int64(100)),
16535 }),
16536 };
16537 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
16538 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
16539 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
16540
16541 let or_expr = TriggerExpr::Or {
16542 left: Box::new(TriggerExpr::Lt {
16543 left: TriggerValue::NewColumn(1),
16544 right: TriggerValue::Literal(Value::Int64(0)),
16545 }),
16546 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
16547 TriggerValue::OldColumn(2),
16548 )))),
16549 };
16550 assert!(eval_trigger_expr(
16551 &or_expr,
16552 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
16553 )
16554 .unwrap());
16555 assert!(!eval_trigger_expr(
16556 &or_expr,
16557 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
16558 )
16559 .unwrap());
16560
16561 assert!(eval_trigger_expr(
16562 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
16563 &event_insert(&[])
16564 )
16565 .unwrap());
16566 assert!(!eval_trigger_expr(
16567 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
16568 &event_insert(&[])
16569 )
16570 .unwrap());
16571 assert!(!eval_trigger_expr(
16572 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
16573 &event_insert(&[])
16574 )
16575 .unwrap());
16576 }
16577}