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
55type OpenLeaseId = u64;
56
57static DATABASE_OPEN_WAIT_COUNT: AtomicU64 = AtomicU64::new(0);
58static DATABASE_OPEN_FAILURE_COUNT: AtomicU64 = AtomicU64::new(0);
59
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61pub struct DatabaseOpenMetrics {
62 pub lock_waits: u64,
63 pub failures: u64,
64}
65
66#[derive(Clone, Debug, Eq, Hash, PartialEq)]
67enum DatabaseOpenKey {
68 IntendedPath(PathBuf),
69 FileIdentity(crate::durable_file::DurableFileIdentity),
70}
71
72#[derive(Debug)]
73enum ProcessOpenState {
74 Opening { lease_id: OpenLeaseId },
75 Open { lease_id: OpenLeaseId },
76 Closing { lease_id: OpenLeaseId },
77}
78
79impl ProcessOpenState {
80 fn lease_id(&self) -> OpenLeaseId {
81 match self {
82 Self::Opening { lease_id } | Self::Open { lease_id } | Self::Closing { lease_id } => {
83 *lease_id
84 }
85 }
86 }
87}
88
89#[derive(Default)]
90struct ProcessOpenRegistry {
91 next_lease_id: OpenLeaseId,
92 entries: HashMap<DatabaseOpenKey, ProcessOpenState>,
93}
94
95fn process_open_registry() -> &'static Mutex<ProcessOpenRegistry> {
96 static REGISTRY: std::sync::OnceLock<Mutex<ProcessOpenRegistry>> = std::sync::OnceLock::new();
97 REGISTRY.get_or_init(|| Mutex::new(ProcessOpenRegistry::default()))
98}
99
100fn same_process_locked(path: &Path) -> MongrelError {
101 MongrelError::DatabaseLocked {
102 path: path.to_path_buf(),
103 message: "database is already open in this process; reuse the existing Arc<Database>"
104 .into(),
105 }
106}
107
108struct OpenReservation {
109 lease_id: OpenLeaseId,
110 keys: Vec<DatabaseOpenKey>,
111 committed: bool,
112}
113
114impl OpenReservation {
115 fn acquire(key: DatabaseOpenKey, display_path: &Path) -> Result<Self> {
116 let mut registry = process_open_registry().lock();
117 if registry.entries.contains_key(&key) {
118 DATABASE_OPEN_FAILURE_COUNT.fetch_add(1, Ordering::Relaxed);
119 return Err(same_process_locked(display_path));
120 }
121 registry.next_lease_id = registry.next_lease_id.checked_add(1).ok_or_else(|| {
122 MongrelError::Full("process database-open lease namespace exhausted".into())
123 })?;
124 let lease_id = registry.next_lease_id;
125 registry
126 .entries
127 .insert(key.clone(), ProcessOpenState::Opening { lease_id });
128 Ok(Self {
129 lease_id,
130 keys: vec![key],
131 committed: false,
132 })
133 }
134
135 fn into_lease(
136 mut self,
137 bootstrap_file: std::fs::File,
138 canonical_path: PathBuf,
139 ) -> ExclusiveDatabaseLease {
140 self.committed = true;
141 ExclusiveDatabaseLease {
142 lease_id: self.lease_id,
143 keys: std::mem::take(&mut self.keys),
144 bootstrap_file,
145 legacy_file: None,
146 canonical_path,
147 durable_root: None,
148 owner_pid: std::process::id(),
149 opened: false,
150 }
151 }
152}
153
154impl Drop for OpenReservation {
155 fn drop(&mut self) {
156 if self.committed {
157 return;
158 }
159 DATABASE_OPEN_FAILURE_COUNT.fetch_add(1, Ordering::Relaxed);
160 let mut registry = process_open_registry().lock();
161 for key in &self.keys {
162 if registry
163 .entries
164 .get(key)
165 .is_some_and(|state| state.lease_id() == self.lease_id)
166 {
167 registry.entries.remove(key);
168 }
169 }
170 }
171}
172
173struct ExclusiveDatabaseLease {
174 lease_id: OpenLeaseId,
175 keys: Vec<DatabaseOpenKey>,
176 bootstrap_file: std::fs::File,
177 legacy_file: Option<std::fs::File>,
178 canonical_path: PathBuf,
179 durable_root: Option<Arc<crate::durable_file::DurableRoot>>,
180 owner_pid: u32,
181 opened: bool,
182}
183
184impl ExclusiveDatabaseLease {
185 fn claim_root_identity(&mut self, root: &crate::durable_file::DurableRoot) -> Result<()> {
186 let key = DatabaseOpenKey::FileIdentity(root.file_identity()?);
187 if self.keys.contains(&key) {
188 return Ok(());
189 }
190 let mut registry = process_open_registry().lock();
191 if registry.entries.contains_key(&key) {
192 return Err(same_process_locked(&self.canonical_path));
193 }
194 registry.entries.insert(
195 key.clone(),
196 ProcessOpenState::Opening {
197 lease_id: self.lease_id,
198 },
199 );
200 self.keys.push(key);
201 Ok(())
202 }
203
204 fn mark_open(&mut self) -> Result<()> {
205 let mut registry = process_open_registry().lock();
206 if self.keys.iter().any(|key| {
207 registry
208 .entries
209 .get(key)
210 .is_none_or(|state| state.lease_id() != self.lease_id)
211 }) {
212 return Err(MongrelError::Conflict(
213 "database-open reservation changed during initialization".into(),
214 ));
215 }
216 for key in &self.keys {
217 registry.entries.insert(
218 key.clone(),
219 ProcessOpenState::Open {
220 lease_id: self.lease_id,
221 },
222 );
223 }
224 self.opened = true;
225 Ok(())
226 }
227}
228
229impl Drop for ExclusiveDatabaseLease {
230 fn drop(&mut self) {
231 if std::process::id() != self.owner_pid {
232 return;
233 }
234 if !self.opened {
235 DATABASE_OPEN_FAILURE_COUNT.fetch_add(1, Ordering::Relaxed);
236 }
237 {
238 let mut registry = process_open_registry().lock();
239 for key in &self.keys {
240 if registry
241 .entries
242 .get(key)
243 .is_some_and(|state| state.lease_id() == self.lease_id)
244 {
245 registry.entries.insert(
246 key.clone(),
247 ProcessOpenState::Closing {
248 lease_id: self.lease_id,
249 },
250 );
251 }
252 }
253 }
254 if let Some(file) = &self.legacy_file {
255 let _ = fs2::FileExt::unlock(file);
256 }
257 let _ = fs2::FileExt::unlock(&self.bootstrap_file);
258 let mut registry = process_open_registry().lock();
259 for key in &self.keys {
260 if registry
261 .entries
262 .get(key)
263 .is_some_and(|state| state.lease_id() == self.lease_id)
264 {
265 registry.entries.remove(key);
266 }
267 }
268 }
269}
270
271fn commit_prepare_checkpoint(
272 control: Option<&crate::ExecutionControl>,
273 index: usize,
274) -> Result<()> {
275 if index.is_multiple_of(256) {
276 if let Some(control) = control {
277 control.checkpoint()?;
278 }
279 }
280 Ok(())
281}
282
283fn finish_controlled_commit_attempt(
284 result: Result<Epoch>,
285 after_commit: &mut Option<&mut dyn FnMut(Option<Epoch>) -> Result<()>>,
286) -> Result<Epoch> {
287 let Some(after_commit) = after_commit.as_mut() else {
288 return result;
289 };
290 match result {
291 Ok(epoch) => match (**after_commit)(Some(epoch)) {
292 Ok(()) => Ok(epoch),
293 Err(error) => Err(MongrelError::DurableCommit {
294 epoch: epoch.0,
295 message: error.to_string(),
296 }),
297 },
298 Err(MongrelError::DurableCommit { epoch, message }) => {
299 let callback_error = (**after_commit)(Some(Epoch(epoch))).err();
300 Err(MongrelError::DurableCommit {
301 epoch,
302 message: callback_error
303 .map(|error| format!("{message}; commit callback: {error}"))
304 .unwrap_or(message),
305 })
306 }
307 Err(error) => match (**after_commit)(None) {
308 Ok(()) => Err(error),
309 Err(callback_error) => Err(MongrelError::Other(format!(
310 "{error}; commit callback: {callback_error}"
311 ))),
312 },
313 }
314}
315
316fn current_unix_nanos() -> u64 {
317 std::time::SystemTime::now()
318 .duration_since(std::time::UNIX_EPOCH)
319 .unwrap_or_default()
320 .as_nanos() as u64
321}
322
323#[cfg(feature = "encryption")]
324fn read_encryption_salt(
325 root: &crate::durable_file::DurableRoot,
326) -> Result<[u8; crate::encryption::SALT_LEN]> {
327 let mut file = root
328 .open_regular(Path::new(META_DIR).join(KEYS_FILENAME))
329 .map_err(|error| MongrelError::NotFound(format!("encryption salt file: {error}")))?;
330 let length = file.metadata()?.len();
331 if length != crate::encryption::SALT_LEN as u64 {
332 return Err(MongrelError::Encryption(format!(
333 "invalid encryption salt length: got {length}, expected {}",
334 crate::encryption::SALT_LEN
335 )));
336 }
337 let mut salt = [0_u8; crate::encryption::SALT_LEN];
338 file.read_exact(&mut salt)?;
339 Ok(salt)
340}
341
342fn incremental_aggregate_cache_key(
343 table: &str,
344 conditions: &[crate::query::Condition],
345 column: Option<u16>,
346 agg: crate::engine::NativeAgg,
347 principal: Option<&crate::auth::Principal>,
348 security_version: u64,
349) -> u64 {
350 use std::hash::{Hash, Hasher};
351 let projection = column.as_ref().map(std::slice::from_ref);
352 let query_key = crate::query::canonical_query_key(conditions, projection, security_version);
353 let mut hasher = std::collections::hash_map::DefaultHasher::new();
354 table.hash(&mut hasher);
355 query_key.hash(&mut hasher);
356 match agg {
357 crate::engine::NativeAgg::Count => 0u8,
358 crate::engine::NativeAgg::Sum => 1,
359 crate::engine::NativeAgg::Min => 2,
360 crate::engine::NativeAgg::Max => 3,
361 crate::engine::NativeAgg::Avg => 4,
362 }
363 .hash(&mut hasher);
364 if let Some(principal) = principal {
365 principal.user_id.hash(&mut hasher);
366 principal.created_epoch.hash(&mut hasher);
367 principal.username.hash(&mut hasher);
368 principal.is_admin.hash(&mut hasher);
369 let mut roles = principal.roles.clone();
370 roles.sort_unstable();
371 roles.hash(&mut hasher);
372 }
373 hasher.finish()
374}
375
376fn read_history_retention(
377 root: &crate::durable_file::DurableRoot,
378 current_epoch: Epoch,
379) -> Result<(u64, Epoch)> {
380 const MAX_HISTORY_RETENTION_BYTES: u64 = 128;
381 let file = match root.open_regular(Path::new(META_DIR).join(HISTORY_RETENTION_FILENAME)) {
382 Ok(file) => file,
383 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
384 return Ok((0, current_epoch));
385 }
386 Err(error) => return Err(error.into()),
387 };
388 let length = file.metadata()?.len();
389 if length > MAX_HISTORY_RETENTION_BYTES {
390 return Err(MongrelError::ResourceLimitExceeded {
391 resource: "history retention bytes",
392 requested: usize::try_from(length).unwrap_or(usize::MAX),
393 limit: MAX_HISTORY_RETENTION_BYTES as usize,
394 });
395 }
396 let mut bytes = Vec::with_capacity(length as usize);
397 file.take(MAX_HISTORY_RETENTION_BYTES + 1)
398 .read_to_end(&mut bytes)?;
399 if bytes.len() as u64 != length {
400 return Err(MongrelError::Other(
401 "history retention length changed while reading".into(),
402 ));
403 }
404 let text = std::str::from_utf8(&bytes)
405 .map_err(|error| MongrelError::Other(format!("history retention encoding: {error}")))?;
406 let mut fields = text.split_whitespace();
407 let epochs = fields
408 .next()
409 .ok_or_else(|| MongrelError::Other("history retention file is empty".into()))?
410 .parse::<u64>()
411 .map_err(|error| MongrelError::Other(format!("history retention epochs: {error}")))?;
412 let start = fields
413 .next()
414 .ok_or_else(|| MongrelError::Other("history retention start is missing".into()))?
415 .parse::<u64>()
416 .map_err(|error| MongrelError::Other(format!("history retention start: {error}")))?;
417 if fields.next().is_some() || start > current_epoch.0 {
418 return Err(MongrelError::Other(
419 "history retention file has trailing fields or a future start epoch".into(),
420 ));
421 }
422 Ok((epochs, Epoch(start)))
423}
424
425fn write_history_retention<F>(
426 root: &Path,
427 epochs: u64,
428 start: Epoch,
429 after_publish: F,
430) -> Result<()>
431where
432 F: FnOnce(),
433{
434 let meta = root.join(META_DIR);
435 let path = meta.join(HISTORY_RETENTION_FILENAME);
436 let bytes = format!("{epochs} {}\n", start.0);
437 crate::durable_file::write_atomic_with_after(&path, bytes.as_bytes(), after_publish)?;
438 Ok(())
439}
440
441struct PreparedBackupDestination {
442 parent: crate::durable_file::DurableRoot,
443 destination_name: std::ffi::OsString,
444 destination_path: PathBuf,
445 stage_name: std::ffi::OsString,
446 stage: Option<Box<crate::durable_file::DurableRoot>>,
447}
448
449fn prepare_backup_destination(
450 source: &Path,
451 destination: &Path,
452) -> Result<PreparedBackupDestination> {
453 let destination_name = destination
454 .file_name()
455 .ok_or_else(|| MongrelError::InvalidArgument("invalid backup destination".into()))?
456 .to_os_string();
457 let requested_parent = destination
458 .parent()
459 .filter(|path| !path.as_os_str().is_empty())
460 .unwrap_or_else(|| Path::new("."));
461 crate::durable_file::create_directory_all(requested_parent)?;
462 let parent = crate::durable_file::DurableRoot::open(requested_parent)?;
463 prepare_backup_destination_in(source, &parent, &destination_name)
464}
465
466fn prepare_backup_destination_in(
467 source: &Path,
468 parent: &crate::durable_file::DurableRoot,
469 destination_name: &std::ffi::OsStr,
470) -> Result<PreparedBackupDestination> {
471 let source = source.canonicalize()?;
472 if parent.canonical_path().starts_with(&source) {
473 return Err(MongrelError::InvalidArgument(
474 "backup destination must not be inside the source database".into(),
475 ));
476 }
477 if parent.entry_exists(Path::new(&destination_name))? {
478 return Err(MongrelError::Conflict(format!(
479 "backup destination already exists: {}",
480 parent.canonical_path().join(destination_name).display()
481 )));
482 }
483 let mut stage_name = None;
484 for _ in 0..128 {
485 let mut nonce = [0_u8; 8];
486 crate::encryption::fill_random(&mut nonce)?;
487 let suffix = nonce
488 .iter()
489 .map(|byte| format!("{byte:02x}"))
490 .collect::<String>();
491 let name = std::ffi::OsString::from(format!(
492 ".{}.backup-stage-{}-{suffix}",
493 destination_name.to_string_lossy(),
494 std::process::id()
495 ));
496 match parent.create_directory_new(Path::new(&name)) {
497 Ok(()) => {
498 stage_name = Some(name);
499 break;
500 }
501 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
502 Err(error) => return Err(error.into()),
503 }
504 }
505 let stage_name = stage_name
506 .ok_or_else(|| MongrelError::Conflict("could not allocate backup staging path".into()))?;
507 let stage = parent.open_directory(Path::new(&stage_name))?;
508 Ok(PreparedBackupDestination {
509 destination_path: parent.canonical_path().join(destination_name),
510 destination_name: destination_name.to_os_string(),
511 stage_name,
512 stage: Some(Box::new(stage)),
513 parent: parent.try_clone()?,
514 })
515}
516
517fn copy_backup_boundary(
518 source_root: &Path,
519 destination_root: &crate::durable_file::DurableRoot,
520 deferred_runs: &HashSet<PathBuf>,
521 copied: &mut Vec<PathBuf>,
522 control: Option<&crate::ExecutionControl>,
523) -> Result<()> {
524 let mut visited = 0;
525 crate::durable_file::walk_regular_files_nofollow(
526 source_root,
527 |relative, is_directory| {
528 if visited % 256 == 0 {
529 if let Some(control) = control {
530 control.checkpoint()?;
531 }
532 }
533 visited += 1;
534 if backup_path_excluded(relative) {
535 return Ok(false);
536 }
537 if is_directory {
538 return Ok(true);
539 }
540 if deferred_runs.contains(relative) {
541 return Ok(false);
542 }
543 Ok(!(relative
544 .parent()
545 .and_then(Path::file_name)
546 .is_some_and(|parent| parent == "_runs")
547 && relative
548 .extension()
549 .is_some_and(|extension| extension == "sr")))
550 },
551 |relative| {
552 destination_root.create_directory_all(relative)?;
553 Ok(())
554 },
555 |relative, source| {
556 destination_root.copy_new_from(relative, source)?;
557 copied.push(relative.to_path_buf());
558 Ok(())
559 },
560 )
561}
562
563fn backup_path_excluded(relative: &Path) -> bool {
564 relative == Path::new("_meta/.lock")
565 || relative == Path::new("_meta/replica")
566 || relative == Path::new("_meta/repl_epoch")
567 || relative == Path::new(crate::backup::BACKUP_MANIFEST_PATH)
568 || relative.components().any(|component| {
569 matches!(component, std::path::Component::Normal(name) if name == "_cache" || name == "_txn" || name == "backup-pins")
570 })
571}
572
573#[derive(Debug, Clone)]
574pub enum ExternalTriggerWrite {
575 Insert {
576 table: String,
577 cells: Vec<(u16, Value)>,
578 },
579 UpdateByPk {
580 table: String,
581 pk: Value,
582 cells: Vec<(u16, Value)>,
583 },
584 DeleteByPk {
585 table: String,
586 pk: Value,
587 },
588}
589
590impl ExternalTriggerWrite {
591 fn table(&self) -> &str {
592 match self {
593 Self::Insert { table, .. }
594 | Self::UpdateByPk { table, .. }
595 | Self::DeleteByPk { table, .. } => table,
596 }
597 }
598}
599
600#[derive(Debug, Clone, PartialEq)]
601pub enum ExternalTriggerBaseWrite {
602 Put {
603 table: String,
604 cells: Vec<(u16, Value)>,
605 },
606 Delete {
607 table: String,
608 row_id: RowId,
609 },
610}
611
612#[derive(Debug, Clone, PartialEq)]
613pub struct ExternalTriggerWriteResult {
614 pub state: Vec<u8>,
615 pub base_writes: Vec<ExternalTriggerBaseWrite>,
616}
617
618impl ExternalTriggerWriteResult {
619 pub fn new(state: Vec<u8>) -> Self {
620 Self {
621 state,
622 base_writes: Vec::new(),
623 }
624 }
625}
626
627pub trait ExternalTriggerBridge: Send + Sync {
628 fn apply_trigger_external_write(
629 &self,
630 entry: &ExternalTableEntry,
631 base_state: Vec<u8>,
632 op: ExternalTriggerWrite,
633 ) -> Result<ExternalTriggerWriteResult>;
634}
635
636struct SpilledRun {
638 table_id: u64,
639 run_id: u128,
640 pending_path: PathBuf,
641 final_path: PathBuf,
642 rows: Vec<crate::memtable::Row>,
643 row_count: u64,
644 min_rid: u64,
645 max_rid: u64,
646 content_hash: [u8; 32],
647}
648
649const SPILLED_WAL_PAYLOAD_MAX_BYTES: usize = 24 * 1024 * 1024;
650const SPILLED_WAL_TOTAL_MAX_BYTES: usize = 256 * 1024 * 1024;
651
652fn encode_spilled_row_chunks(
653 rows: &[crate::memtable::Row],
654 total_bytes: &mut usize,
655 total_limit: usize,
656 control: Option<&crate::ExecutionControl>,
657) -> Result<Vec<Vec<u8>>> {
658 let mut output = Vec::new();
659 let mut start = 0;
660 while start < rows.len() {
661 let mut estimated_bytes = std::mem::size_of::<u64>();
665 let mut end = start;
666 while end < rows.len() {
667 if end % 256 == 0 {
668 if let Some(control) = control {
669 control.checkpoint()?;
670 }
671 }
672 let row_bytes =
673 usize::try_from(bincode::serialized_size(&rows[end])?).map_err(|_| {
674 MongrelError::ResourceLimitExceeded {
675 resource: "spilled WAL row bytes",
676 requested: usize::MAX,
677 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
678 }
679 })?;
680 let next_bytes = estimated_bytes.checked_add(row_bytes).ok_or(
681 MongrelError::ResourceLimitExceeded {
682 resource: "spilled WAL row bytes",
683 requested: usize::MAX,
684 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
685 },
686 )?;
687 if next_bytes > SPILLED_WAL_PAYLOAD_MAX_BYTES {
688 break;
689 }
690 estimated_bytes = next_bytes;
691 end += 1;
692 }
693 if end == start {
694 return Err(MongrelError::ResourceLimitExceeded {
695 resource: "spilled WAL row bytes",
696 requested: estimated_bytes.saturating_add(1),
697 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
698 });
699 }
700 let payload = bincode::serialize(&rows[start..end])?;
701 if payload.len() > SPILLED_WAL_PAYLOAD_MAX_BYTES {
702 return Err(MongrelError::ResourceLimitExceeded {
703 resource: "spilled WAL row bytes",
704 requested: payload.len(),
705 limit: SPILLED_WAL_PAYLOAD_MAX_BYTES,
706 });
707 }
708 let requested = total_bytes.checked_add(payload.len()).unwrap_or(usize::MAX);
709 if requested > total_limit {
710 return Err(MongrelError::ResourceLimitExceeded {
711 resource: "spilled WAL transaction bytes",
712 requested,
713 limit: total_limit,
714 });
715 }
716 *total_bytes = requested;
717 output.push(payload);
718 start = end;
719 }
720 Ok(output)
721}
722
723#[cfg(test)]
724mod spilled_wal_encoding_tests {
725 use super::*;
726
727 #[test]
728 fn logical_spill_payload_has_a_total_bound() {
729 let rows = (0..4)
730 .map(|row_id| crate::memtable::Row {
731 row_id: crate::rowid::RowId(row_id),
732 committed_epoch: Epoch::ZERO,
733 columns: [(1, Value::Bytes(vec![0; 64]))].into_iter().collect(),
734 deleted: false,
735 })
736 .collect::<Vec<_>>();
737 let mut total = 0;
738 let error = encode_spilled_row_chunks(&rows, &mut total, 32, None).unwrap_err();
739 assert!(matches!(
740 error,
741 MongrelError::ResourceLimitExceeded {
742 resource: "spilled WAL transaction bytes",
743 ..
744 }
745 ));
746 }
747}
748
749struct PreparedRunLinks {
754 links: Vec<(PathBuf, PathBuf)>,
755 armed: bool,
756}
757
758impl PreparedRunLinks {
759 fn prepare(spilled: &[SpilledRun]) -> Result<Self> {
760 let mut guard = Self {
761 links: Vec::with_capacity(spilled.len()),
762 armed: true,
763 };
764 for run in spilled {
765 crate::durable_file::rename(&run.pending_path, &run.final_path)?;
766 guard
767 .links
768 .push((run.pending_path.clone(), run.final_path.clone()));
769 }
770 Ok(guard)
771 }
772
773 fn disarm(&mut self) {
774 self.armed = false;
775 for (pending, _) in &self.links {
776 if let Some(parent) = pending.parent() {
777 let _ = std::fs::remove_dir_all(parent);
778 }
779 }
780 }
781}
782
783impl Drop for PreparedRunLinks {
784 fn drop(&mut self) {
785 if !self.armed {
786 return;
787 }
788 for (pending, final_path) in self.links.iter().rev() {
789 let _ = std::fs::rename(final_path, pending);
790 }
791 }
792}
793
794struct TableApplyBatch {
795 table_id: u64,
796 handle: TableHandle,
797 ops: Vec<crate::txn::StagedOp>,
798}
799
800#[derive(Debug, Clone)]
801struct TriggerRowImage {
802 columns: HashMap<u16, Value>,
803}
804
805impl TriggerRowImage {
806 fn from_row(row: crate::memtable::Row) -> Self {
807 Self {
808 columns: row.columns,
809 }
810 }
811
812 fn from_cells(cells: &[(u16, Value)]) -> Self {
813 Self {
814 columns: cells.iter().cloned().collect(),
815 }
816 }
817}
818
819#[derive(Debug, Clone)]
820struct WriteEvent {
821 table: String,
822 kind: TriggerEvent,
823 old: Option<TriggerRowImage>,
824 new: Option<TriggerRowImage>,
825 changed_columns: Vec<u16>,
826 op_indices: Vec<usize>,
827 put_idx: Option<usize>,
828 trigger_stack: Vec<String>,
829}
830
831#[derive(Default)]
832struct TriggerExpansion {
833 before: Vec<(u64, crate::txn::Staged)>,
834 before_stacks: Vec<Vec<String>>,
835 before_external: Vec<ExternalTriggerWrite>,
836 after: Vec<(u64, crate::txn::Staged)>,
837 after_stacks: Vec<Vec<String>>,
838 after_external: Vec<ExternalTriggerWrite>,
839 ignored_indices: std::collections::BTreeSet<usize>,
840}
841
842#[derive(Clone, PartialEq)]
843struct TriggerCatalogBinding {
844 triggers: Vec<TriggerEntry>,
845 tables: Vec<(String, u64, u64)>,
846 external_tables: Vec<(String, u64, u64)>,
847}
848
849fn trigger_catalog_binding(catalog: &Catalog) -> Option<TriggerCatalogBinding> {
850 let mut triggers = catalog
851 .triggers
852 .iter()
853 .filter(|entry| entry.trigger.enabled)
854 .cloned()
855 .collect::<Vec<_>>();
856 if triggers.is_empty() {
857 return None;
858 }
859 triggers.sort_by(|left, right| left.trigger.name.cmp(&right.trigger.name));
860 let mut tables = catalog
861 .tables
862 .iter()
863 .filter(|entry| matches!(entry.state, TableState::Live))
864 .map(|entry| (entry.name.clone(), entry.table_id, entry.schema.schema_id))
865 .collect::<Vec<_>>();
866 tables.sort_unstable();
867 let mut external_tables = catalog
868 .external_tables
869 .iter()
870 .map(|entry| {
871 (
872 entry.name.clone(),
873 entry.created_epoch,
874 entry.declared_schema.schema_id,
875 )
876 })
877 .collect::<Vec<_>>();
878 external_tables.sort_unstable();
879 Some(TriggerCatalogBinding {
880 triggers,
881 tables,
882 external_tables,
883 })
884}
885
886struct TriggerProgramOutput<'a> {
887 added: &'a mut Vec<(u64, crate::txn::Staged)>,
888 added_stacks: &'a mut Vec<Vec<String>>,
889 added_external: &'a mut Vec<ExternalTriggerWrite>,
890 ignored_indices: &'a mut std::collections::BTreeSet<usize>,
891}
892
893#[derive(Debug, Clone, Copy, PartialEq, Eq)]
894enum TriggerProgramOutcome {
895 Continue,
896 Ignore,
897}
898
899#[derive(Debug, Clone)]
901pub struct CheckIssue {
902 pub table_id: u64,
903 pub table_name: String,
904 pub severity: String,
905 pub description: String,
906}
907
908#[derive(Debug, Clone)]
910pub struct AuthorizedReadSnapshot {
911 pub table: String,
912 pub table_snapshot: Snapshot,
913 pub data_generation: u64,
914 pub security_version: u64,
915 pub allowed_row_ids: Option<HashSet<RowId>>,
916}
917
918#[derive(Debug, Clone, Copy, PartialEq, Eq)]
920pub struct AuthorizedReadStamp {
921 pub table_id: u64,
922 pub schema_id: u64,
923 pub data_generation: u64,
924 pub security_version: u64,
925 pub snapshot: Snapshot,
926}
927
928type RlsCacheKey = (String, u64, u64, String);
929
930#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
932pub struct RlsCacheStats {
933 pub entries: usize,
934 pub bytes: usize,
935 pub hits: u64,
936 pub misses: u64,
937 pub evictions: u64,
938 pub build_nanos: u64,
939 pub rows_evaluated: u64,
940}
941
942const RLS_CACHE_MAX_BYTES: usize = 64 * 1024 * 1024;
943const CDC_MAX_WAL_RECORDS: usize = 1_000_000;
944const CDC_MAX_WAL_REPLAY_BYTES: usize = 256 * 1024 * 1024;
945const CDC_MAX_EVENTS: usize = 100_000;
946const CDC_MAX_ROWS: usize = 1_000_000;
947const CDC_MAX_INLINE_PAYLOAD_BYTES: usize = 32 * 1024 * 1024;
948const CDC_MAX_RETAINED_BYTES: usize = 256 * 1024 * 1024;
949
950fn charge_cdc_bytes(total: &mut usize, amount: usize, resource: &'static str) -> Result<()> {
951 let requested = total.saturating_add(amount);
952 if requested > CDC_MAX_RETAINED_BYTES {
953 return Err(MongrelError::ResourceLimitExceeded {
954 resource,
955 requested,
956 limit: CDC_MAX_RETAINED_BYTES,
957 });
958 }
959 *total = requested;
960 Ok(())
961}
962
963fn cdc_row_storage_bytes(row: &crate::memtable::Row) -> usize {
964 usize::try_from(row.estimated_bytes())
965 .unwrap_or(usize::MAX)
966 .saturating_add(std::mem::size_of::<crate::memtable::Row>())
967}
968
969fn cdc_row_json_bytes(row: &crate::memtable::Row) -> usize {
970 let value_slot = std::mem::size_of::<serde_json::Value>();
971 row.columns.values().fold(512_usize, |bytes, value| {
972 let values = match value {
973 Value::Bytes(values) => values.len(),
974 Value::Json(values) => values.len(),
975 Value::Embedding(values) => values.len(),
976 _ => 1,
977 };
978 bytes.saturating_add(values.saturating_mul(value_slot))
979 })
980}
981
982fn cdc_rows_json_bytes(rows: &[crate::memtable::Row]) -> usize {
983 rows.iter().fold(0_usize, |bytes, row| {
984 bytes.saturating_add(cdc_row_json_bytes(row))
985 })
986}
987
988#[derive(Default)]
989struct RlsCache {
990 entries: HashMap<RlsCacheKey, (Arc<HashSet<RowId>>, usize)>,
991 lru: VecDeque<RlsCacheKey>,
992 bytes: usize,
993 hits: u64,
994 misses: u64,
995 evictions: u64,
996 build_nanos: u64,
997 rows_evaluated: u64,
998}
999
1000impl RlsCache {
1001 fn get(&mut self, key: &RlsCacheKey) -> Option<Arc<HashSet<RowId>>> {
1002 let value = self.entries.get(key).map(|(value, _)| Arc::clone(value));
1003 if value.is_some() {
1004 self.hits = self.hits.saturating_add(1);
1005 self.touch(key);
1006 } else {
1007 self.misses = self.misses.saturating_add(1);
1008 }
1009 value
1010 }
1011
1012 fn insert(&mut self, key: RlsCacheKey, value: Arc<HashSet<RowId>>) {
1013 let bytes = key
1014 .0
1015 .len()
1016 .saturating_add(key.3.len())
1017 .saturating_add(
1018 value
1019 .capacity()
1020 .saturating_mul(std::mem::size_of::<RowId>() * 3),
1021 )
1022 .saturating_add(std::mem::size_of::<RlsCacheKey>());
1023 if bytes > RLS_CACHE_MAX_BYTES {
1024 return;
1025 }
1026 if let Some((_, old_bytes)) = self.entries.remove(&key) {
1027 self.bytes = self.bytes.saturating_sub(old_bytes);
1028 }
1029 self.lru.retain(|candidate| candidate != &key);
1030 while self.bytes.saturating_add(bytes) > RLS_CACHE_MAX_BYTES {
1031 let Some(oldest) = self.lru.pop_front() else {
1032 break;
1033 };
1034 if let Some((_, old_bytes)) = self.entries.remove(&oldest) {
1035 self.bytes = self.bytes.saturating_sub(old_bytes);
1036 self.evictions = self.evictions.saturating_add(1);
1037 }
1038 }
1039 self.bytes = self.bytes.saturating_add(bytes);
1040 self.lru.push_back(key.clone());
1041 self.entries.insert(key, (value, bytes));
1042 }
1043
1044 fn touch(&mut self, key: &RlsCacheKey) {
1045 self.lru.retain(|candidate| candidate != key);
1046 self.lru.push_back(key.clone());
1047 }
1048
1049 fn stats(&self) -> RlsCacheStats {
1050 RlsCacheStats {
1051 entries: self.entries.len(),
1052 bytes: self.bytes,
1053 hits: self.hits,
1054 misses: self.misses,
1055 evictions: self.evictions,
1056 build_nanos: self.build_nanos,
1057 rows_evaluated: self.rows_evaluated,
1058 }
1059 }
1060}
1061
1062#[derive(Clone)]
1064pub struct TableHandle {
1065 inner: TableHandleInner,
1066 generation_metrics: Arc<TableGenerationMetrics>,
1067}
1068
1069#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1070pub struct TableGenerationStats {
1071 pub active_read_generations: usize,
1072 pub max_live_read_generations: usize,
1073 pub cow_clone_count: u64,
1074 pub cow_clone_nanos: u64,
1075 pub estimated_cow_clone_bytes: u64,
1076 pub writer_wait_nanos: u64,
1077}
1078
1079#[derive(Default)]
1080#[doc(hidden)]
1081pub struct TableGenerationMetrics {
1082 active_read_generations: AtomicUsize,
1083 max_live_read_generations: AtomicUsize,
1084 cow_clone_count: AtomicU64,
1085 cow_clone_nanos: AtomicU64,
1086 estimated_cow_clone_bytes: AtomicU64,
1087 writer_wait_nanos: AtomicU64,
1088}
1089
1090impl TableGenerationMetrics {
1091 fn activate(self: &Arc<Self>, table: Table) -> Arc<TableReadGeneration> {
1092 let active = self.active_read_generations.fetch_add(1, Ordering::Relaxed) + 1;
1093 self.max_live_read_generations
1094 .fetch_max(active, Ordering::Relaxed);
1095 Arc::new(TableReadGeneration {
1096 table,
1097 metrics: Arc::clone(self),
1098 })
1099 }
1100
1101 fn stats(&self) -> TableGenerationStats {
1102 TableGenerationStats {
1103 active_read_generations: self.active_read_generations.load(Ordering::Relaxed),
1104 max_live_read_generations: self.max_live_read_generations.load(Ordering::Relaxed),
1105 cow_clone_count: self.cow_clone_count.load(Ordering::Relaxed),
1106 cow_clone_nanos: self.cow_clone_nanos.load(Ordering::Relaxed),
1107 estimated_cow_clone_bytes: self.estimated_cow_clone_bytes.load(Ordering::Relaxed),
1108 writer_wait_nanos: self.writer_wait_nanos.load(Ordering::Relaxed),
1109 }
1110 }
1111}
1112
1113pub struct TableReadGeneration {
1115 table: Table,
1116 metrics: Arc<TableGenerationMetrics>,
1117}
1118
1119impl std::ops::Deref for TableReadGeneration {
1120 type Target = Table;
1121
1122 fn deref(&self) -> &Self::Target {
1123 &self.table
1124 }
1125}
1126
1127impl Drop for TableReadGeneration {
1128 fn drop(&mut self) {
1129 self.metrics
1130 .active_read_generations
1131 .fetch_sub(1, Ordering::Relaxed);
1132 }
1133}
1134
1135#[derive(Clone)]
1136enum TableHandleInner {
1137 CopyOnWrite(Arc<RwLock<Arc<Table>>>),
1138 Direct(Arc<Mutex<Table>>),
1139}
1140
1141pub enum TableGuard<'a> {
1142 CopyOnWrite {
1143 table: parking_lot::RwLockWriteGuard<'a, Arc<Table>>,
1144 metrics: Arc<TableGenerationMetrics>,
1145 },
1146 Direct {
1147 table: parking_lot::MutexGuard<'a, Table>,
1148 },
1149}
1150
1151impl TableHandle {
1152 fn new(table: Table) -> Self {
1153 Self {
1154 inner: TableHandleInner::CopyOnWrite(Arc::new(RwLock::new(Arc::new(table)))),
1155 generation_metrics: Arc::new(TableGenerationMetrics::default()),
1156 }
1157 }
1158
1159 pub fn from_table(table: Table) -> Self {
1160 Self::new(table)
1161 }
1162
1163 pub fn lock(&self) -> TableGuard<'_> {
1164 let started = std::time::Instant::now();
1165 let guard = match &self.inner {
1166 TableHandleInner::CopyOnWrite(table) => TableGuard::CopyOnWrite {
1167 table: table.write(),
1168 metrics: Arc::clone(&self.generation_metrics),
1169 },
1170 TableHandleInner::Direct(table) => TableGuard::Direct {
1171 table: table.lock(),
1172 },
1173 };
1174 self.generation_metrics.writer_wait_nanos.fetch_add(
1175 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1176 Ordering::Relaxed,
1177 );
1178 guard
1179 }
1180
1181 fn try_lock_for(&self, timeout: std::time::Duration) -> Option<TableGuard<'_>> {
1182 let started = std::time::Instant::now();
1183 let guard = match &self.inner {
1184 TableHandleInner::CopyOnWrite(table) => {
1185 table
1186 .try_write_for(timeout)
1187 .map(|table| TableGuard::CopyOnWrite {
1188 table,
1189 metrics: Arc::clone(&self.generation_metrics),
1190 })
1191 }
1192 TableHandleInner::Direct(table) => table
1193 .try_lock_for(timeout)
1194 .map(|table| TableGuard::Direct { table }),
1195 };
1196 self.generation_metrics.writer_wait_nanos.fetch_add(
1197 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1198 Ordering::Relaxed,
1199 );
1200 guard
1201 }
1202
1203 pub fn ptr_eq(&self, other: &Self) -> bool {
1204 match (&self.inner, &other.inner) {
1205 (TableHandleInner::CopyOnWrite(left), TableHandleInner::CopyOnWrite(right)) => {
1206 Arc::ptr_eq(left, right)
1207 }
1208 (TableHandleInner::Direct(left), TableHandleInner::Direct(right)) => {
1209 Arc::ptr_eq(left, right)
1210 }
1211 _ => false,
1212 }
1213 }
1214
1215 pub fn read_generation_with_context(
1216 &self,
1217 context: Option<&crate::query::AiExecutionContext>,
1218 ) -> Result<(Arc<TableReadGeneration>, Snapshot)> {
1219 let mut table = if let Some(context) = context {
1220 loop {
1221 context.checkpoint()?;
1222 let wait = context
1223 .remaining_duration()
1224 .unwrap_or(std::time::Duration::from_millis(5))
1225 .min(std::time::Duration::from_millis(5));
1226 if let Some(table) = self.try_lock_for(wait) {
1227 break table;
1228 }
1229 }
1230 } else {
1231 self.lock()
1232 };
1233 let snapshot = table.snapshot();
1234 let generation = table.clone_read_generation()?;
1235 Ok((self.generation_metrics.activate(generation), snapshot))
1236 }
1237
1238 pub fn generation_stats(&self) -> TableGenerationStats {
1239 self.generation_metrics.stats()
1240 }
1241}
1242
1243impl From<Arc<Mutex<Table>>> for TableHandle {
1244 fn from(table: Arc<Mutex<Table>>) -> Self {
1245 Self {
1246 inner: TableHandleInner::Direct(table),
1247 generation_metrics: Arc::new(TableGenerationMetrics::default()),
1248 }
1249 }
1250}
1251
1252impl std::ops::Deref for TableGuard<'_> {
1253 type Target = Table;
1254
1255 fn deref(&self) -> &Self::Target {
1256 match self {
1257 Self::CopyOnWrite { table, .. } => table.as_ref(),
1258 Self::Direct { table } => table,
1259 }
1260 }
1261}
1262
1263impl std::ops::DerefMut for TableGuard<'_> {
1264 fn deref_mut(&mut self) -> &mut Self::Target {
1265 match self {
1266 Self::CopyOnWrite { table, metrics } => {
1267 if Arc::strong_count(table) > 1 || Arc::weak_count(table) > 0 {
1268 let estimated_bytes = table.estimated_clone_bytes();
1269 let started = std::time::Instant::now();
1270 let table = Arc::make_mut(table);
1271 metrics.cow_clone_count.fetch_add(1, Ordering::Relaxed);
1272 metrics.cow_clone_nanos.fetch_add(
1273 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
1274 Ordering::Relaxed,
1275 );
1276 metrics
1277 .estimated_cow_clone_bytes
1278 .fetch_add(estimated_bytes, Ordering::Relaxed);
1279 table
1280 } else {
1281 Arc::make_mut(table)
1282 }
1283 }
1284 Self::Direct { table } => table,
1285 }
1286 }
1287}
1288
1289#[derive(Clone, Debug)]
1290pub struct ReadAuthorization {
1291 pub operation: crate::auth::ColumnOperation,
1292 pub columns: Vec<u16>,
1293 pub permissions: Vec<crate::auth::Permission>,
1294}
1295
1296#[derive(Default, Debug)]
1297struct TableWritePermissionNeeds {
1298 insert: bool,
1299 insert_columns: Vec<u16>,
1300 update: bool,
1301 update_columns: Vec<u16>,
1302 delete: bool,
1303 truncate: bool,
1304}
1305
1306#[cfg(test)]
1307thread_local! {
1308 static WRITE_PERMISSION_DECISIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1309 static AUTO_INCREMENT_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1310 static PREBUILD_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1311 static PUBLISH_TABLE_LOCKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1312 static COMMIT_MANIFEST_WRITES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1313 static TABLE_PERMISSION_DECISIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1314}
1315
1316fn summarize_write_permissions(
1317 staging: &[(u64, crate::txn::Staged)],
1318) -> HashMap<u64, TableWritePermissionNeeds> {
1319 use crate::txn::Staged;
1320
1321 let mut needs = HashMap::<u64, TableWritePermissionNeeds>::new();
1322 for (table_id, operation) in staging {
1323 let table = needs.entry(*table_id).or_default();
1324 match operation {
1325 Staged::Put(cells) => {
1326 table.insert = true;
1327 table
1328 .insert_columns
1329 .extend(cells.iter().map(|(column, _)| *column));
1330 }
1331 Staged::Update {
1332 changed_columns, ..
1333 } => {
1334 table.update = true;
1335 table.update_columns.extend(changed_columns);
1336 }
1337 Staged::Delete(_) => table.delete = true,
1338 Staged::Truncate => table.truncate = true,
1339 }
1340 }
1341 for table in needs.values_mut() {
1342 table.insert_columns.sort_unstable();
1343 table.insert_columns.dedup();
1344 table.update_columns.sort_unstable();
1345 table.update_columns.dedup();
1346 }
1347 needs
1348}
1349
1350struct SecurityCoordinator {
1351 gate: RwLock<()>,
1353 version: AtomicU64,
1354}
1355
1356fn security_coordinator(root: &Path, version: u64) -> Arc<SecurityCoordinator> {
1357 static COORDINATORS: std::sync::OnceLock<
1358 Mutex<HashMap<PathBuf, std::sync::Weak<SecurityCoordinator>>>,
1359 > = std::sync::OnceLock::new();
1360
1361 let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
1362 let mut coordinators = COORDINATORS
1363 .get_or_init(|| Mutex::new(HashMap::new()))
1364 .lock();
1365 coordinators.retain(|_, coordinator| coordinator.strong_count() > 0);
1366 if let Some(coordinator) = coordinators.get(&root).and_then(std::sync::Weak::upgrade) {
1367 return coordinator;
1368 }
1369 let coordinator = Arc::new(SecurityCoordinator {
1370 gate: RwLock::new(()),
1371 version: AtomicU64::new(version),
1372 });
1373 coordinators.insert(root, Arc::downgrade(&coordinator));
1374 coordinator
1375}
1376
1377pub fn lock_table_with_context<'a>(
1378 handle: &'a TableHandle,
1379 context: Option<&crate::query::AiExecutionContext>,
1380) -> Result<TableGuard<'a>> {
1381 let Some(context) = context else {
1382 return Ok(handle.lock());
1383 };
1384 loop {
1385 context.checkpoint()?;
1386 let wait = context
1387 .remaining_duration()
1388 .unwrap_or(std::time::Duration::from_millis(5))
1389 .min(std::time::Duration::from_millis(5));
1390 if let Some(guard) = handle.try_lock_for(wait) {
1391 return Ok(guard);
1392 }
1393 }
1394}
1395
1396#[derive(Clone, Debug, Default)]
1402pub struct OpenOptions {
1403 pub lock_timeout_ms: u32,
1419}
1420
1421impl OpenOptions {
1422 pub fn with_lock_timeout_ms(mut self, ms: u32) -> Self {
1425 self.lock_timeout_ms = ms;
1426 self
1427 }
1428}
1429
1430pub struct Database {
1433 root: PathBuf,
1434 durable_root: Arc<crate::durable_file::DurableRoot>,
1435 read_only: bool,
1437 catalog: RwLock<Catalog>,
1438 security_coordinator: Arc<SecurityCoordinator>,
1439 security_catalog_disk_reads: AtomicU64,
1440 rls_cache: Mutex<RlsCache>,
1441 epoch: Arc<EpochAuthority>,
1442 snapshots: Arc<SnapshotRegistry>,
1443 page_cache: Arc<crate::cache::Sharded<crate::cache::PageCache>>,
1444 decoded_cache: Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>,
1445 commit_lock: Arc<Mutex<()>>,
1446 shared_wal: Arc<Mutex<crate::wal::SharedWal>>,
1451 next_txn_id: Arc<Mutex<u64>>,
1455 tables: RwLock<HashMap<u64, TableHandle>>,
1456 kek: Option<Arc<crate::encryption::Kek>>,
1457 ddl_lock: Mutex<()>,
1460 meta_dek: Option<[u8; META_DEK_LEN]>,
1461 spill_threshold: std::sync::atomic::AtomicU64,
1464 conflicts: crate::txn::ConflictIndex,
1467 active_txns: crate::txn::ActiveTxns,
1470 poisoned: Arc<std::sync::atomic::AtomicBool>,
1473 group: Arc<crate::txn::GroupCommit>,
1477 active_spills: Arc<crate::retention::ActiveSpills>,
1480 replication_barrier: parking_lot::RwLock<()>,
1483 replication_wal_retention_segments: AtomicUsize,
1485 backup_pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
1488 #[doc(hidden)]
1492 spill_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1493 #[doc(hidden)]
1495 security_commit_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1496 #[doc(hidden)]
1499 catalog_commit_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1500 #[doc(hidden)]
1503 backup_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1504 replication_hook: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
1505 trigger_recursive: AtomicBool,
1506 trigger_max_depth: AtomicU32,
1507 trigger_max_loop_iterations: AtomicU32,
1508 notify: tokio::sync::broadcast::Sender<ChangeEvent>,
1511 change_wake: tokio::sync::broadcast::Sender<()>,
1514 principal: RwLock<Option<crate::auth::Principal>>,
1524 auth_state: crate::auth_state::AuthState,
1530 _lock: Option<ExclusiveDatabaseLease>,
1532}
1533
1534struct RunPins {
1535 pins: Arc<Mutex<HashMap<(u64, u128), usize>>>,
1536 runs: Vec<(u64, u128)>,
1537}
1538
1539struct BackupFilePins {
1540 root: PathBuf,
1541}
1542
1543struct PendingTableDir {
1544 path: PathBuf,
1545 armed: bool,
1546}
1547
1548impl PendingTableDir {
1549 fn new(path: PathBuf) -> Self {
1550 Self { path, armed: true }
1551 }
1552
1553 fn disarm(&mut self) {
1554 self.armed = false;
1555 }
1556}
1557
1558impl Drop for PendingTableDir {
1559 fn drop(&mut self) {
1560 if self.armed {
1561 let _ = std::fs::remove_dir_all(&self.path);
1562 }
1563 }
1564}
1565
1566impl Drop for BackupFilePins {
1567 fn drop(&mut self) {
1568 let _ = std::fs::remove_dir_all(&self.root);
1569 }
1570}
1571
1572impl Drop for RunPins {
1573 fn drop(&mut self) {
1574 let mut pins = self.pins.lock();
1575 for run in &self.runs {
1576 if let Some(count) = pins.get_mut(run) {
1577 *count -= 1;
1578 if *count == 0 {
1579 pins.remove(run);
1580 }
1581 }
1582 }
1583 }
1584}
1585
1586#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1589pub struct ChangeEvent {
1590 pub id: Option<String>,
1591 pub channel: String,
1592 pub table_id: Option<u64>,
1593 pub table: String,
1594 pub op: String,
1595 pub epoch: u64,
1596 pub txn_id: Option<u64>,
1597 pub message: Option<String>,
1598 pub data: Option<serde_json::Value>,
1599}
1600
1601#[derive(Debug, Clone)]
1602pub struct CdcBatch {
1603 pub events: Vec<ChangeEvent>,
1604 pub current_epoch: u64,
1605 pub earliest_epoch: Option<u64>,
1606 pub gap: bool,
1607}
1608
1609impl std::fmt::Debug for Database {
1616 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1617 let cat = self.catalog.read();
1618 let principal_guard = self.principal.read();
1619 let principal: &str = principal_guard
1620 .as_ref()
1621 .map(|p| p.username.as_str())
1622 .unwrap_or("<none>");
1623 f.debug_struct("Database")
1624 .field("root", &self.root)
1625 .field("db_epoch", &cat.db_epoch)
1626 .field("open_generation", &"sidecar")
1627 .field("tables", &cat.tables.len())
1628 .field("visible_epoch", &self.epoch.visible().0)
1629 .field("encrypted", &self.kek.is_some())
1630 .field("require_auth", &cat.require_auth)
1631 .field("principal", &principal)
1632 .finish()
1633 }
1634}
1635
1636impl Database {
1637 pub fn open_metrics() -> DatabaseOpenMetrics {
1638 DatabaseOpenMetrics {
1639 lock_waits: DATABASE_OPEN_WAIT_COUNT.load(Ordering::Relaxed),
1640 failures: DATABASE_OPEN_FAILURE_COUNT.load(Ordering::Relaxed),
1641 }
1642 }
1643
1644 fn ensure_owner_process(&self) -> Result<()> {
1645 let current_pid = std::process::id();
1646 let owner_pid = self
1647 ._lock
1648 .as_ref()
1649 .map(|lease| lease.owner_pid)
1650 .unwrap_or(current_pid);
1651 if current_pid == owner_pid {
1652 Ok(())
1653 } else {
1654 Err(MongrelError::ForkedProcess {
1655 owner_pid,
1656 current_pid,
1657 })
1658 }
1659 }
1660
1661 pub fn shutdown(self: Arc<Self>) -> Result<()> {
1663 match Arc::try_unwrap(self) {
1664 Ok(database) => {
1665 database.ensure_owner_process()?;
1666 drop(database);
1667 Ok(())
1668 }
1669 Err(database) => Err(MongrelError::DatabaseBusy {
1670 strong_handles: Arc::strong_count(&database),
1671 }),
1672 }
1673 }
1674
1675 fn canonical_lock_target(root: &Path) -> std::io::Result<(PathBuf, PathBuf)> {
1676 if let Ok(canonical) = root.canonicalize() {
1677 let lock_dir = canonical.parent().ok_or_else(|| {
1678 std::io::Error::new(
1679 std::io::ErrorKind::InvalidInput,
1680 "database root must have a parent directory",
1681 )
1682 })?;
1683 return Ok((canonical.clone(), lock_dir.to_path_buf()));
1684 }
1685
1686 let absolute = if root.is_absolute() {
1687 root.to_path_buf()
1688 } else {
1689 std::env::current_dir()?.join(root)
1690 };
1691 let mut cursor = absolute.as_path();
1692 let mut suffix = Vec::new();
1693 while !cursor.exists() {
1694 let name = cursor.file_name().ok_or_else(|| {
1695 std::io::Error::new(
1696 std::io::ErrorKind::NotFound,
1697 format!("no existing ancestor for database root {}", root.display()),
1698 )
1699 })?;
1700 suffix.push(name.to_os_string());
1701 cursor = cursor.parent().ok_or_else(|| {
1702 std::io::Error::new(
1703 std::io::ErrorKind::NotFound,
1704 format!("no existing ancestor for database root {}", root.display()),
1705 )
1706 })?;
1707 }
1708 let lock_dir = cursor.canonicalize()?;
1709 let mut canonical = lock_dir.clone();
1710 for component in suffix.iter().rev() {
1711 canonical.push(component);
1712 }
1713 Ok((canonical, lock_dir))
1714 }
1715
1716 fn acquire_database_lock(root: &Path, timeout_ms: u32) -> Result<ExclusiveDatabaseLease> {
1717 use std::hash::{Hash, Hasher};
1718
1719 let (canonical_path, lock_dir) = Self::canonical_lock_target(root)?;
1720 let reservation =
1721 OpenReservation::acquire(DatabaseOpenKey::IntendedPath(canonical_path.clone()), root)?;
1722
1723 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1724 canonical_path.hash(&mut hasher);
1725 let lock_path = lock_dir.join(format!(".mongreldb-{:016x}.lock", hasher.finish()));
1726 let file = std::fs::OpenOptions::new()
1727 .create(true)
1728 .truncate(false)
1729 .write(true)
1730 .open(lock_path)?;
1731 if let Err(error) = Self::fs_lock_exclusive(&file, timeout_ms) {
1732 return Err(MongrelError::DatabaseLocked {
1733 path: root.to_path_buf(),
1734 message: error.to_string(),
1735 });
1736 }
1737 Ok(reservation.into_lease(file, canonical_path))
1738 }
1739
1740 fn acquire_legacy_database_lock(
1741 lock: &mut ExclusiveDatabaseLease,
1742 root: &Path,
1743 timeout_ms: u32,
1744 ) -> Result<()> {
1745 let durable_root = lock
1746 .durable_root
1747 .as_ref()
1748 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?;
1749 let file = durable_root.open_lock_file(Path::new(META_DIR).join(".lock"))?;
1750 if let Err(error) = Self::fs_lock_exclusive(&file, timeout_ms) {
1751 return Err(MongrelError::DatabaseLocked {
1752 path: root.to_path_buf(),
1753 message: error.to_string(),
1754 });
1755 }
1756 lock.legacy_file = Some(file);
1757 Ok(())
1758 }
1759
1760 fn pin_or_create_database_root(path: &Path) -> Result<crate::durable_file::DurableRoot> {
1761 if path.exists() {
1762 return crate::durable_file::DurableRoot::open(path).map_err(Into::into);
1763 }
1764 let mut ancestor = path;
1765 while !ancestor.exists() {
1766 ancestor = ancestor.parent().ok_or_else(|| {
1767 MongrelError::NotFound(format!(
1768 "no existing ancestor for database root {}",
1769 path.display()
1770 ))
1771 })?;
1772 }
1773 let relative = path.strip_prefix(ancestor).map_err(|error| {
1774 MongrelError::InvalidArgument(format!("invalid database root: {error}"))
1775 })?;
1776 crate::durable_file::DurableRoot::open(ancestor)?
1777 .create_directory_all_pinned(relative)
1778 .map_err(Into::into)
1779 }
1780
1781 fn begin_create(root: impl AsRef<Path>) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
1782 let requested_root = root.as_ref();
1783 let mut lock = Self::acquire_database_lock(requested_root, 0)?;
1784 let root = lock.canonical_path.clone();
1785 Self::reject_existing_database(&root)?;
1786 let durable_root = Arc::new(Self::pin_or_create_database_root(&root)?);
1787 if durable_root.canonical_path() != lock.canonical_path {
1788 return Err(MongrelError::Conflict(
1789 "database root changed while it was being created".into(),
1790 ));
1791 }
1792 lock.claim_root_identity(&durable_root)?;
1793 durable_root.create_directory_all(META_DIR)?;
1794 lock.durable_root = Some(durable_root);
1795 let io_root = lock
1796 .durable_root
1797 .as_ref()
1798 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
1799 .io_path()?;
1800 Self::acquire_legacy_database_lock(&mut lock, &io_root, 0)?;
1801 Self::reject_existing_database(&io_root)?;
1802 Ok((io_root, lock))
1803 }
1804
1805 fn begin_open(
1806 root: impl AsRef<Path>,
1807 lock_timeout_ms: u32,
1808 ) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
1809 let root = root.as_ref();
1810 let canonical_root = root.canonicalize().map_err(|error| {
1811 if error.kind() == std::io::ErrorKind::NotFound {
1812 MongrelError::NotFound(format!("database root {}: {error}", root.display()))
1813 } else {
1814 error.into()
1815 }
1816 })?;
1817 let durable_root = crate::durable_file::DurableRoot::open(&canonical_root)?;
1818 Self::begin_open_durable(durable_root, lock_timeout_ms)
1819 }
1820
1821 fn begin_open_durable(
1822 durable_root: crate::durable_file::DurableRoot,
1823 lock_timeout_ms: u32,
1824 ) -> Result<(PathBuf, ExclusiveDatabaseLease)> {
1825 let io_root = durable_root.io_path()?;
1826 let current_root = io_root.canonicalize()?;
1827 let mut lock = Self::acquire_database_lock(¤t_root, lock_timeout_ms)?;
1828 lock.claim_root_identity(&durable_root)?;
1829 lock.durable_root = Some(Arc::new(durable_root));
1830 let io_root = lock
1831 .durable_root
1832 .as_ref()
1833 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
1834 .io_path()?;
1835 if lock
1836 .durable_root
1837 .as_ref()
1838 .ok_or_else(|| MongrelError::Other("database root descriptor was not pinned".into()))?
1839 .open_directory(META_DIR)
1840 .is_err()
1841 {
1842 return Err(MongrelError::NotFound(format!(
1843 "no database metadata found at {:?}",
1844 current_root
1845 )));
1846 }
1847 Self::acquire_legacy_database_lock(&mut lock, &io_root, lock_timeout_ms)?;
1848 Ok((io_root, lock))
1849 }
1850
1851 pub fn create(root: impl AsRef<Path>) -> Result<Self> {
1853 let (root, lock) = Self::begin_create(root)?;
1854 Self::create_inner(root, None, lock)
1855 }
1856
1857 #[cfg(feature = "encryption")]
1860 pub fn create_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
1861 let (root, lock) = Self::begin_create(root)?;
1862 let salt = crate::encryption::random_salt()?;
1863 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
1864 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1865 Self::create_inner(root, Some(kek), lock)
1866 }
1867
1868 #[cfg(feature = "encryption")]
1871 pub fn create_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
1872 let (root, lock) = Self::begin_create(root)?;
1873 let salt = crate::encryption::random_salt()?;
1874 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
1875 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
1876 Self::create_inner(root, Some(kek), lock)
1877 }
1878
1879 fn create_inner(
1880 root: PathBuf,
1881 kek: Option<Arc<crate::encryption::Kek>>,
1882 lock: ExclusiveDatabaseLease,
1883 ) -> Result<Self> {
1884 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
1885 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
1886 let cat = Catalog::empty();
1887 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
1888 Self::finish_open(root, cat, kek, meta_dek, false, None, None, None, lock)
1889 }
1890
1891 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
1893 Self::open_inner(root, None, None)
1894 }
1895
1896 #[cfg(feature = "encryption")]
1898 pub fn open_encrypted(root: impl AsRef<Path>, passphrase: &str) -> Result<Self> {
1899 let (root, lock) = Self::begin_open(root, 0)?;
1900 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
1901 MongrelError::Other("database root descriptor was not pinned".into())
1902 })?)?;
1903 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1904 Self::open_inner_locked(root, Some(kek), lock)
1905 }
1906
1907 #[cfg(feature = "encryption")]
1910 pub fn open_encrypted_with_options(
1911 root: impl AsRef<Path>,
1912 passphrase: &str,
1913 options: OpenOptions,
1914 ) -> Result<Self> {
1915 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
1916 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
1917 MongrelError::Other("database root descriptor was not pinned".into())
1918 })?)?;
1919 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1920 Self::open_inner_locked(root, Some(kek), lock)
1921 }
1922
1923 #[cfg(feature = "encryption")]
1925 pub fn open_with_key(root: impl AsRef<Path>, key: &[u8]) -> Result<Self> {
1926 let (root, lock) = Self::begin_open(root, 0)?;
1927 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
1928 MongrelError::Other("database root descriptor was not pinned".into())
1929 })?)?;
1930 let kek = Arc::new(crate::encryption::Kek::from_raw_key(key, &salt)?);
1931 Self::open_inner_locked(root, Some(kek), lock)
1932 }
1933
1934 pub fn open_with_credentials(
1946 root: impl AsRef<Path>,
1947 username: &str,
1948 password: &str,
1949 ) -> Result<Self> {
1950 Self::open_inner_with_credentials(root, None, username, password)
1951 }
1952
1953 pub fn open_with_credentials_and_options(
1957 root: impl AsRef<Path>,
1958 username: &str,
1959 password: &str,
1960 options: OpenOptions,
1961 ) -> Result<Self> {
1962 Self::open_inner_with_credentials_and_lock_timeout(
1963 root,
1964 None,
1965 username,
1966 password,
1967 options.lock_timeout_ms,
1968 )
1969 }
1970
1971 #[cfg(feature = "encryption")]
1974 pub fn open_encrypted_with_credentials(
1975 root: impl AsRef<Path>,
1976 passphrase: &str,
1977 username: &str,
1978 password: &str,
1979 ) -> Result<Self> {
1980 let (root, lock) = Self::begin_open(root, 0)?;
1981 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
1982 MongrelError::Other("database root descriptor was not pinned".into())
1983 })?)?;
1984 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
1985 Self::open_inner_with_credentials_locked(root, Some(kek), username, password, lock)
1986 }
1987
1988 #[cfg(feature = "encryption")]
1992 pub fn open_encrypted_with_credentials_and_options(
1993 root: impl AsRef<Path>,
1994 passphrase: &str,
1995 username: &str,
1996 password: &str,
1997 options: OpenOptions,
1998 ) -> Result<Self> {
1999 let (root, lock) = Self::begin_open(root, options.lock_timeout_ms)?;
2000 let salt = read_encryption_salt(lock.durable_root.as_deref().ok_or_else(|| {
2001 MongrelError::Other("database root descriptor was not pinned".into())
2002 })?)?;
2003 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2004 Self::open_inner_with_credentials_locked(root, Some(kek), username, password, lock)
2005 }
2006
2007 pub fn open_with_options(root: impl AsRef<Path>, options: OpenOptions) -> Result<Self> {
2014 Self::open_inner_with_lock_timeout(root, None, None, options.lock_timeout_ms)
2017 }
2018
2019 fn open_inner_with_lock_timeout(
2020 root: impl AsRef<Path>,
2021 kek: Option<Arc<crate::encryption::Kek>>,
2022 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
2023 lock_timeout_ms: u32,
2024 ) -> Result<Self> {
2025 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
2026 Self::open_inner_locked(root, kek, lock)
2027 }
2028
2029 fn open_inner_locked(
2030 root: PathBuf,
2031 kek: Option<Arc<crate::encryption::Kek>>,
2032 lock: ExclusiveDatabaseLease,
2033 ) -> Result<Self> {
2034 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2035 let mut cat = catalog::read_durable(
2036 lock.durable_root.as_deref().ok_or_else(|| {
2037 MongrelError::Other("database root descriptor was not pinned".into())
2038 })?,
2039 meta_dek.as_ref(),
2040 )?
2041 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
2042 let recovery_checkpoint = cat.clone();
2043
2044 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
2047 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
2048 lock.durable_root.as_deref().ok_or_else(|| {
2049 MongrelError::Other("database root descriptor was not pinned".into())
2050 })?,
2051 wal_dek.as_ref(),
2052 )?;
2053 recover_ddl_from_records(
2054 &root,
2055 Some(lock.durable_root.as_deref().ok_or_else(|| {
2056 MongrelError::Other("database root descriptor was not pinned".into())
2057 })?),
2058 &mut cat,
2059 meta_dek.as_ref(),
2060 false,
2061 None,
2062 &recovery_records,
2063 )?;
2064 Self::finish_open(
2065 root,
2066 cat,
2067 kek,
2068 meta_dek,
2069 true,
2070 Some(recovery_checkpoint),
2071 Some(recovery_records),
2072 None,
2073 lock,
2074 )
2075 }
2076
2077 fn open_inner_with_credentials(
2084 root: impl AsRef<Path>,
2085 kek: Option<Arc<crate::encryption::Kek>>,
2086 username: &str,
2087 password: &str,
2088 ) -> Result<Self> {
2089 Self::open_inner_with_credentials_and_lock_timeout(root, kek, username, password, 0)
2090 }
2091
2092 fn open_inner_with_credentials_and_lock_timeout(
2096 root: impl AsRef<Path>,
2097 kek: Option<Arc<crate::encryption::Kek>>,
2098 username: &str,
2099 password: &str,
2100 lock_timeout_ms: u32,
2101 ) -> Result<Self> {
2102 let (root, lock) = Self::begin_open(root, lock_timeout_ms)?;
2103 Self::open_inner_with_credentials_locked(root, kek, username, password, lock)
2104 }
2105
2106 fn open_inner_with_credentials_locked(
2107 root: PathBuf,
2108 kek: Option<Arc<crate::encryption::Kek>>,
2109 username: &str,
2110 password: &str,
2111 lock: ExclusiveDatabaseLease,
2112 ) -> Result<Self> {
2113 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2114 let mut cat = catalog::read_durable(
2115 lock.durable_root.as_deref().ok_or_else(|| {
2116 MongrelError::Other("database root descriptor was not pinned".into())
2117 })?,
2118 meta_dek.as_ref(),
2119 )?
2120 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
2121 let recovery_checkpoint = cat.clone();
2122
2123 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
2126 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
2127 lock.durable_root.as_deref().ok_or_else(|| {
2128 MongrelError::Other("database root descriptor was not pinned".into())
2129 })?,
2130 wal_dek.as_ref(),
2131 )?;
2132 recover_ddl_from_records(
2133 &root,
2134 Some(lock.durable_root.as_deref().ok_or_else(|| {
2135 MongrelError::Other("database root descriptor was not pinned".into())
2136 })?),
2137 &mut cat,
2138 meta_dek.as_ref(),
2139 false,
2140 None,
2141 &recovery_records,
2142 )?;
2143
2144 if !cat.require_auth {
2147 return Err(MongrelError::AuthNotRequired);
2148 }
2149
2150 let user = cat
2155 .users
2156 .iter()
2157 .find(|u| u.username == username)
2158 .filter(|u| !u.password_hash.is_empty())
2159 .ok_or_else(|| MongrelError::InvalidCredentials {
2160 username: username.to_string(),
2161 })?;
2162 let password_ok = crate::auth::verify_password(password, &user.password_hash)
2163 .map_err(MongrelError::Other)?;
2164 if !password_ok {
2165 return Err(MongrelError::InvalidCredentials {
2166 username: username.to_string(),
2167 });
2168 }
2169
2170 let principal =
2172 Self::resolve_principal_from_catalog(&cat, &user.username).ok_or_else(|| {
2173 MongrelError::InvalidCredentials {
2174 username: username.to_string(),
2175 }
2176 })?;
2177
2178 Self::finish_open(
2179 root,
2180 cat,
2181 kek,
2182 meta_dek,
2183 true,
2184 Some(recovery_checkpoint),
2185 Some(recovery_records),
2186 Some(principal),
2187 lock,
2188 )
2189 }
2190
2191 pub fn create_with_credentials(
2201 root: impl AsRef<Path>,
2202 admin_username: &str,
2203 admin_password: &str,
2204 ) -> Result<Self> {
2205 let (root, lock) = Self::begin_create(root)?;
2206 Self::create_inner_with_credentials(root, None, admin_username, admin_password, lock)
2207 }
2208
2209 #[cfg(feature = "encryption")]
2213 pub fn create_encrypted_with_credentials(
2214 root: impl AsRef<Path>,
2215 passphrase: &str,
2216 admin_username: &str,
2217 admin_password: &str,
2218 ) -> Result<Self> {
2219 let (root, lock) = Self::begin_create(root)?;
2220 let salt = crate::encryption::random_salt()?;
2221 crate::durable_file::write_atomic(&root.join(META_DIR).join(KEYS_FILENAME), &salt)?;
2222 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2223 Self::create_inner_with_credentials(root, Some(kek), admin_username, admin_password, lock)
2224 }
2225
2226 fn create_inner_with_credentials(
2227 root: PathBuf,
2228 kek: Option<Arc<crate::encryption::Kek>>,
2229 admin_username: &str,
2230 admin_password: &str,
2231 lock: ExclusiveDatabaseLease,
2232 ) -> Result<Self> {
2233 crate::durable_file::create_directory_all(&root.join(TABLES_DIR))?;
2234 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2235
2236 let password_hash =
2238 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
2239 let mut cat = Catalog::empty();
2240 cat.require_auth = true;
2241 cat.next_user_id = 2;
2242 cat.users.push(crate::auth::UserEntry {
2243 id: 1,
2244 username: admin_username.to_string(),
2245 password_hash,
2246 roles: Vec::new(),
2247 is_admin: true,
2248 created_epoch: 0,
2249 });
2250 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
2251
2252 let admin_principal = crate::auth::Principal {
2255 user_id: 1,
2256 created_epoch: 0,
2257 username: admin_username.to_string(),
2258 is_admin: true,
2259 roles: Vec::new(),
2260 permissions: Vec::new(),
2261 };
2262 Self::finish_open(
2263 root,
2264 cat,
2265 kek,
2266 meta_dek,
2267 false,
2268 None,
2269 None,
2270 Some(admin_principal),
2271 lock,
2272 )
2273 }
2274
2275 fn reject_existing_database(root: &Path) -> Result<()> {
2276 if root.join(catalog::CATALOG_FILENAME).exists() {
2279 return Err(MongrelError::InvalidArgument(format!(
2280 "database already exists at {}; use Database::open() to open it, \
2281 or remove the directory first",
2282 root.display()
2283 )));
2284 }
2285 Ok(())
2286 }
2287
2288 fn open_inner(
2289 root: impl AsRef<Path>,
2290 kek: Option<Arc<crate::encryption::Kek>>,
2291 _meta_dek_override: Option<[u8; META_DEK_LEN]>,
2292 ) -> Result<Self> {
2293 Self::open_inner_with_lock_timeout(root, kek, None, 0)
2294 }
2295
2296 pub(crate) fn open_replica_recovery_durable(
2300 root: &crate::durable_file::DurableRoot,
2301 ) -> Result<Self> {
2302 let (root, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
2303 Self::open_replica_recovery_inner(root, None, lock)
2304 }
2305
2306 #[cfg(feature = "encryption")]
2307 pub(crate) fn open_encrypted_replica_recovery_durable(
2308 root: &crate::durable_file::DurableRoot,
2309 passphrase: &str,
2310 ) -> Result<Self> {
2311 let (root_path, lock) = Self::begin_open_durable(root.try_clone()?, 0)?;
2312 let salt = read_encryption_salt(root)?;
2313 let kek = Arc::new(crate::encryption::Kek::derive(passphrase, &salt)?);
2314 Self::open_replica_recovery_inner(root_path, Some(kek), lock)
2315 }
2316
2317 fn open_replica_recovery_inner(
2318 root: PathBuf,
2319 kek: Option<Arc<crate::encryption::Kek>>,
2320 lock: ExclusiveDatabaseLease,
2321 ) -> Result<Self> {
2322 if !root.join(META_DIR).join("replica").is_file() {
2323 return Err(MongrelError::InvalidArgument(
2324 "recovery auth bypass requires a marked replica staging directory".into(),
2325 ));
2326 }
2327 let meta_dek = crate::encryption::meta_dek_for(kek.as_deref());
2328 let mut cat = catalog::read_durable(
2329 lock.durable_root.as_deref().ok_or_else(|| {
2330 MongrelError::Other("database root descriptor was not pinned".into())
2331 })?,
2332 meta_dek.as_ref(),
2333 )?
2334 .ok_or_else(|| MongrelError::NotFound(format!("no catalog found at {:?}", root)))?;
2335 let recovery_checkpoint = cat.clone();
2336 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
2337 let recovery_records = crate::wal::SharedWal::replay_durable_with_dek(
2338 lock.durable_root.as_deref().ok_or_else(|| {
2339 MongrelError::Other("database root descriptor was not pinned".into())
2340 })?,
2341 wal_dek.as_ref(),
2342 )?;
2343 recover_ddl_from_records(
2344 &root,
2345 Some(lock.durable_root.as_deref().ok_or_else(|| {
2346 MongrelError::Other("database root descriptor was not pinned".into())
2347 })?),
2348 &mut cat,
2349 meta_dek.as_ref(),
2350 false,
2351 None,
2352 &recovery_records,
2353 )?;
2354 let principal = if cat.require_auth {
2355 cat.users
2356 .iter()
2357 .find(|user| user.is_admin)
2358 .and_then(|user| Self::resolve_principal_from_catalog(&cat, &user.username))
2359 .ok_or_else(|| {
2360 MongrelError::Schema(
2361 "authenticated replica catalog has no recoverable admin".into(),
2362 )
2363 })?
2364 .into()
2365 } else {
2366 None
2367 };
2368 Self::finish_open(
2369 root,
2370 cat,
2371 kek,
2372 meta_dek,
2373 true,
2374 Some(recovery_checkpoint),
2375 Some(recovery_records),
2376 principal,
2377 lock,
2378 )
2379 }
2380
2381 fn fs_lock_exclusive(f: &std::fs::File, timeout_ms: u32) -> std::io::Result<()> {
2393 use fs2::FileExt;
2394 if timeout_ms == 0 {
2395 return f.try_lock_exclusive();
2396 }
2397 let deadline =
2399 std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms as u64);
2400 let mut next_sleep = std::time::Duration::from_millis(1);
2401 let mut recorded_wait = false;
2402 loop {
2403 match f.try_lock_exclusive() {
2404 Ok(()) => return Ok(()),
2405 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
2406 if !recorded_wait {
2407 DATABASE_OPEN_WAIT_COUNT.fetch_add(1, Ordering::Relaxed);
2408 recorded_wait = true;
2409 }
2410 let now = std::time::Instant::now();
2411 if now >= deadline {
2412 return Err(std::io::Error::new(
2413 std::io::ErrorKind::WouldBlock,
2414 format!("could not acquire database lock within {timeout_ms}ms"),
2415 ));
2416 }
2417 let remaining = deadline - now;
2418 let sleep = next_sleep.min(remaining);
2419 std::thread::sleep(sleep);
2420 next_sleep = next_sleep
2423 .saturating_mul(10)
2424 .min(std::time::Duration::from_millis(50));
2425 }
2426 Err(e) => return Err(e),
2427 }
2428 }
2429 }
2430
2431 #[allow(clippy::too_many_arguments)]
2432 fn finish_open(
2433 root: PathBuf,
2434 cat: Catalog,
2435 kek: Option<Arc<crate::encryption::Kek>>,
2436 meta_dek: Option<[u8; META_DEK_LEN]>,
2437 existing: bool,
2438 recovery_checkpoint: Option<Catalog>,
2439 recovery_records: Option<Vec<crate::wal::Record>>,
2440 principal: Option<crate::auth::Principal>,
2441 lock: ExclusiveDatabaseLease,
2442 ) -> Result<Self> {
2443 let durable_root = Arc::clone(lock.durable_root.as_ref().ok_or_else(|| {
2444 MongrelError::Other("database root descriptor was not pinned".into())
2445 })?);
2446 let read_only = if existing {
2447 match durable_root.open_regular(Path::new(META_DIR).join("replica")) {
2448 Ok(_) => true,
2449 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
2450 Err(error) => return Err(error.into()),
2451 }
2452 } else {
2453 false
2454 };
2455 let recovered_catalog = cat;
2456 let mut cat = recovered_catalog.clone();
2457 let abandoned = if existing && !read_only {
2458 let abandoned = cat
2459 .tables
2460 .iter()
2461 .filter(|entry| matches!(entry.state, TableState::Building { .. }))
2462 .map(|entry| entry.table_id)
2463 .collect::<Vec<_>>();
2464 for entry in &mut cat.tables {
2465 if abandoned.contains(&entry.table_id) {
2466 entry.state = TableState::Dropped {
2467 at_epoch: cat.db_epoch,
2468 };
2469 }
2470 }
2471 abandoned
2472 } else {
2473 Vec::new()
2474 };
2475 let wal_dek = crate::encryption::wal_dek_for(kek.as_deref());
2476 let recovery_records = match (existing, recovery_records) {
2477 (true, Some(records)) => records,
2478 (true, None) => {
2479 return Err(MongrelError::Other(
2480 "existing open has no validated WAL recovery plan".into(),
2481 ))
2482 }
2483 (false, _) => Vec::new(),
2484 };
2485 let (history_epochs, history_start) =
2486 read_history_retention(&durable_root, Epoch(cat.db_epoch))?;
2487 let open_generation = if existing {
2488 let checkpoint = recovery_checkpoint.as_ref().ok_or_else(|| {
2489 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
2490 })?;
2491 let recovered_table_ids = cat
2492 .tables
2493 .iter()
2494 .filter(|entry| {
2495 checkpoint
2496 .tables
2497 .iter()
2498 .all(|checkpoint| checkpoint.table_id != entry.table_id)
2499 })
2500 .map(|entry| entry.table_id)
2501 .collect::<HashSet<_>>();
2502 let reconciled_table_ids = cat
2503 .tables
2504 .iter()
2505 .filter(|entry| {
2506 checkpoint
2507 .tables
2508 .iter()
2509 .find(|checkpoint| checkpoint.table_id == entry.table_id)
2510 .is_some_and(|checkpoint| {
2511 crate::wal::DdlOp::encode_schema(&checkpoint.schema).ok()
2512 != crate::wal::DdlOp::encode_schema(&entry.schema).ok()
2513 })
2514 })
2515 .map(|entry| entry.table_id)
2516 .collect::<HashSet<_>>();
2517 validate_shared_wal_recovery_plan(
2518 &durable_root,
2519 &cat,
2520 &recovered_table_ids,
2521 &reconciled_table_ids,
2522 meta_dek.as_ref(),
2523 kek.clone(),
2524 &recovery_records,
2525 )?;
2526 let retained_generation = recovery_records
2527 .iter()
2528 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
2529 .map(|record| record.txn_id >> 32)
2530 .max()
2531 .unwrap_or(0);
2532 let head_generation =
2533 crate::wal::SharedWal::durable_open_generation(&durable_root, wal_dek.as_ref())?;
2534 let durable_floor = match head_generation {
2535 Some(head) if retained_generation > head => {
2536 return Err(MongrelError::CorruptWal {
2537 offset: retained_generation,
2538 reason: format!(
2539 "retained transaction generation {retained_generation} exceeds WAL head generation {head}"
2540 ),
2541 })
2542 }
2543 Some(head) => head,
2544 None => retained_generation,
2545 };
2546 let stored = catalog::read_generation(&durable_root)?;
2547 if stored.is_some_and(|generation| generation < durable_floor) {
2548 return Err(MongrelError::Other(format!(
2549 "open-generation {stored:?} precedes durable WAL generation {durable_floor}"
2550 )));
2551 }
2552 let bumped = stored
2553 .unwrap_or(durable_floor)
2554 .max(durable_floor)
2555 .checked_add(1)
2556 .ok_or_else(|| MongrelError::Full("open-generation namespace exhausted".into()))?;
2557 if bumped > u32::MAX as u64 {
2558 return Err(MongrelError::Full(
2559 "open-generation namespace exhausted".into(),
2560 ));
2561 }
2562 bumped
2563 } else {
2564 0
2565 };
2566 let principal = if cat.require_auth {
2567 let supplied = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
2568 Some(
2569 Self::resolve_bound_principal_from_catalog(&cat, supplied)
2570 .ok_or(MongrelError::AuthRequired)?,
2571 )
2572 } else {
2573 principal
2574 };
2575 let mut table_roots = HashMap::<u64, Arc<crate::durable_file::DurableRoot>>::new();
2576 if existing {
2577 for entry in &cat.tables {
2578 if !matches!(entry.state, TableState::Live) {
2579 continue;
2580 }
2581 match durable_root
2582 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))
2583 {
2584 Ok(root) => {
2585 table_roots.insert(entry.table_id, Arc::new(root));
2586 }
2587 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
2588 Err(error) => return Err(error.into()),
2589 }
2590 }
2591 }
2592
2593 if existing {
2597 let mut applied = recovery_checkpoint.ok_or_else(|| {
2598 MongrelError::Other("existing open has no catalog recovery checkpoint".into())
2599 })?;
2600 recover_ddl_from_records(
2601 &root,
2602 Some(&durable_root),
2603 &mut applied,
2604 meta_dek.as_ref(),
2605 true,
2606 Some(&table_roots),
2607 &recovery_records,
2608 )?;
2609 let catalog_value = |catalog: &Catalog| {
2610 serde_json::to_value(catalog)
2611 .map_err(|error| MongrelError::Other(format!("catalog compare: {error}")))
2612 };
2613 if catalog_value(&applied)? != catalog_value(&recovered_catalog)? {
2614 return Err(MongrelError::CorruptWal {
2615 offset: 0,
2616 reason: "validated and applied DDL recovery plans differ".into(),
2617 });
2618 }
2619 if catalog_value(&cat)? != catalog_value(&applied)? {
2620 catalog::write_atomic(&root, &cat, meta_dek.as_ref())?;
2621 }
2622 validate_catalog_table_storage(&durable_root, &cat, meta_dek.as_ref())?;
2623 if !read_only {
2624 sweep_unreferenced_table_dirs(&root, &cat)?;
2625 }
2626 match durable_root.remove_directory_all(Path::new(META_DIR).join("backup-pins")) {
2627 Ok(()) => {}
2628 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
2629 Err(error) => return Err(error.into()),
2630 }
2631 }
2632
2633 let epoch = Arc::new(EpochAuthority::new(cat.db_epoch));
2634 let snapshots = Arc::new(SnapshotRegistry::new());
2635 snapshots.configure_history(history_epochs, history_start);
2636 let page_cache = Arc::new(crate::cache::Sharded::new(
2637 crate::cache::CACHE_SHARDS,
2638 || {
2639 crate::cache::PageCache::new(
2640 crate::engine::PAGE_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
2641 )
2642 },
2643 ));
2644 let decoded_cache = Arc::new(crate::cache::Sharded::new(
2645 crate::cache::CACHE_SHARDS,
2646 || {
2647 crate::cache::DecodedPageCache::new(
2648 crate::engine::DECODED_CACHE_CAPACITY / crate::cache::CACHE_SHARDS as u64,
2649 )
2650 },
2651 ));
2652 let commit_lock = Arc::new(Mutex::new(()));
2653 let shared_wal = Arc::new(Mutex::new(if existing {
2654 crate::wal::SharedWal::open_durable_root_validated(
2655 Arc::clone(&durable_root),
2656 Epoch(cat.db_epoch),
2657 wal_dek.clone(),
2658 Some(&recovery_records),
2659 )?
2660 } else {
2661 crate::wal::SharedWal::create_with_durable_root(
2662 Arc::clone(&durable_root),
2663 Epoch(cat.db_epoch),
2664 wal_dek.clone(),
2665 )?
2666 }));
2667 let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));
2671 let group = Arc::new(crate::txn::GroupCommit::new(
2672 shared_wal.lock().durable_seq(),
2673 ));
2674 let (change_wake, _change_rx) = tokio::sync::broadcast::channel(256);
2675 let txn_ids = Arc::new(Mutex::new(1u64));
2679 let _ = abandoned;
2680
2681 let auth_state = crate::auth_state::AuthState::new(cat.require_auth, principal.clone());
2686 let security_coordinator = security_coordinator(&root, cat.security_version);
2687 let auth_checker: Option<Arc<dyn crate::auth_state::TableAuthChecker>> = Some(Arc::new(
2688 crate::auth_state::DefaultTableAuthChecker::new(auth_state.clone()),
2689 ));
2690
2691 let mut tables: HashMap<u64, TableHandle> = HashMap::new();
2697 for entry in &cat.tables {
2698 if !matches!(entry.state, TableState::Live) {
2699 continue;
2700 }
2701 let table_root = match table_roots.remove(&entry.table_id) {
2702 Some(root) => root,
2703 None => Arc::new(
2704 durable_root
2705 .open_directory(Path::new(TABLES_DIR).join(entry.table_id.to_string()))?,
2706 ),
2707 };
2708 let tdir = table_root.io_path()?;
2709 let ctx = SharedCtx {
2710 root_guard: Some(table_root),
2711 epoch: Arc::clone(&epoch),
2712 page_cache: Arc::clone(&page_cache),
2713 decoded_cache: Arc::clone(&decoded_cache),
2714 snapshots: Arc::clone(&snapshots),
2715 kek: kek.clone(),
2716 commit_lock: Arc::clone(&commit_lock),
2717 shared: Some(crate::engine::SharedWalCtx {
2718 wal: Arc::clone(&shared_wal),
2719 group: Arc::clone(&group),
2720 poisoned: Arc::clone(&poisoned),
2721 txn_ids: Arc::clone(&txn_ids),
2722 change_wake: change_wake.clone(),
2723 }),
2724 table_name: Some(entry.name.clone()),
2725 auth: auth_checker.clone(),
2726 read_only,
2727 };
2728 let t = Table::open_in(&tdir, ctx)?;
2729 tables.insert(entry.table_id, TableHandle::new(t));
2730 }
2731
2732 if existing {
2738 recover_shared_wal(&durable_root, &tables, &cat, &epoch, &recovery_records)?;
2739 reconcile_recovered_table_metadata(&tables, epoch.visible())?;
2740 if read_only {
2741 crate::replication::reconcile_replica_epoch_durable(
2742 &durable_root,
2743 epoch.visible().0,
2744 )?;
2745 }
2746 sweep_pending_txn_dirs(&root, &cat);
2749 }
2750
2751 catalog::write_generation(&durable_root, open_generation)?;
2753 shared_wal.lock().seal_open_generation(open_generation)?;
2754 crate::replication::replication_identity_durable(&durable_root)?;
2755 let next_txn_id = (open_generation << 32) | 1;
2756 *txn_ids.lock() = next_txn_id;
2758 let mut lock = lock;
2759 lock.mark_open()?;
2760
2761 Ok(Self {
2762 root,
2763 durable_root,
2764 read_only,
2765 catalog: RwLock::new(cat),
2766 security_coordinator,
2767 security_catalog_disk_reads: AtomicU64::new(0),
2768 rls_cache: Mutex::new(RlsCache::default()),
2769 epoch,
2770 snapshots,
2771 page_cache,
2772 decoded_cache,
2773 commit_lock,
2774 shared_wal,
2775 next_txn_id: txn_ids,
2776 tables: RwLock::new(tables),
2777 kek,
2778 ddl_lock: Mutex::new(()),
2779 meta_dek,
2780 conflicts: crate::txn::ConflictIndex::new(),
2781 active_txns: crate::txn::ActiveTxns::new(),
2782 poisoned,
2783 group,
2784 spill_threshold: std::sync::atomic::AtomicU64::new(64 * 1024 * 1024),
2785 active_spills: Arc::new(crate::retention::ActiveSpills::new()),
2786 replication_barrier: parking_lot::RwLock::new(()),
2787 replication_wal_retention_segments: AtomicUsize::new(0),
2788 backup_pins: Arc::new(Mutex::new(HashMap::new())),
2789 spill_hook: Mutex::new(None),
2790 security_commit_hook: Mutex::new(None),
2791 catalog_commit_hook: Mutex::new(None),
2792 backup_hook: Mutex::new(None),
2793 replication_hook: Mutex::new(None),
2794 trigger_recursive: AtomicBool::new(TriggerConfig::default().recursive_triggers),
2795 trigger_max_depth: AtomicU32::new(TriggerConfig::default().max_depth),
2796 trigger_max_loop_iterations: AtomicU32::new(
2797 TriggerConfig::default().max_loop_iterations,
2798 ),
2799 notify: {
2800 let (tx, _rx) = tokio::sync::broadcast::channel(256);
2801 tx
2802 },
2803 change_wake,
2804 principal: RwLock::new(principal),
2805 auth_state,
2806 _lock: Some(lock),
2807 })
2808 }
2809
2810 pub fn visible_epoch(&self) -> Epoch {
2812 self.epoch.visible()
2813 }
2814
2815 pub fn catalog_snapshot(&self) -> Catalog {
2817 self.catalog.read().clone()
2818 }
2819
2820 pub fn sql_pragma_i64(&self, key: &str) -> Result<Option<i64>> {
2822 let catalog = self.catalog.read();
2823 match key {
2824 "user_version" => Ok(catalog.user_version),
2825 "application_id" => Ok(catalog.application_id),
2826 _ => Err(MongrelError::InvalidArgument(format!(
2827 "unsupported persistent SQL pragma {key:?}"
2828 ))),
2829 }
2830 }
2831
2832 pub fn set_sql_pragma_i64_with_epoch(&self, key: &str, value: i64) -> Result<Option<Epoch>> {
2835 self.set_sql_pragma_i64_with_epoch_inner(key, value, None)
2836 }
2837
2838 pub fn set_sql_pragma_i64_with_epoch_controlled<F>(
2839 &self,
2840 key: &str,
2841 value: i64,
2842 mut before_commit: F,
2843 ) -> Result<Option<Epoch>>
2844 where
2845 F: FnMut() -> Result<()>,
2846 {
2847 self.set_sql_pragma_i64_with_epoch_inner(key, value, Some(&mut before_commit))
2848 }
2849
2850 fn set_sql_pragma_i64_with_epoch_inner(
2851 &self,
2852 key: &str,
2853 value: i64,
2854 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
2855 ) -> Result<Option<Epoch>> {
2856 use crate::wal::DdlOp;
2857
2858 self.require(&crate::auth::Permission::Ddl)?;
2859 if self.read_only {
2860 return Err(MongrelError::ReadOnlyReplica);
2861 }
2862 if self.poisoned.load(Ordering::Relaxed) {
2863 return Err(MongrelError::Other(
2864 "database poisoned by fsync error".into(),
2865 ));
2866 }
2867 let _ddl = self.ddl_lock.lock();
2868 let _security_write = self.security_write()?;
2869 self.require(&crate::auth::Permission::Ddl)?;
2870 let mut next_catalog = self.catalog.read().clone();
2871 let target = match key {
2872 "user_version" => &mut next_catalog.user_version,
2873 "application_id" => &mut next_catalog.application_id,
2874 _ => {
2875 return Err(MongrelError::InvalidArgument(format!(
2876 "unsupported persistent SQL pragma {key:?}"
2877 )))
2878 }
2879 };
2880 if *target == Some(value) {
2881 return Ok(None);
2882 }
2883 *target = Some(value);
2884
2885 let _commit = self.commit_lock.lock();
2886 let epoch = self.epoch.bump_assigned();
2887 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
2888 let txn_id = self.alloc_txn_id()?;
2889 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
2890 let commit_seq = {
2891 let mut wal = self.shared_wal.lock();
2892 if let Some(before_commit) = before_commit {
2893 before_commit()?;
2894 }
2895 let append: Result<u64> = (|| {
2896 wal.append(
2897 txn_id,
2898 WAL_TABLE_ID,
2899 crate::wal::Op::Ddl(DdlOp::SetSqlPragma {
2900 key: key.to_string(),
2901 value,
2902 }),
2903 )?;
2904 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
2905 wal.append_commit(txn_id, epoch, &[])
2906 })();
2907 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
2908 };
2909 self.await_durable_commit(commit_seq, epoch)?;
2910 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
2911 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
2912 Ok(Some(epoch))
2913 }
2914
2915 pub fn materialized_view(&self, name: &str) -> Option<crate::catalog::MaterializedViewEntry> {
2916 self.catalog
2917 .read()
2918 .materialized_views
2919 .iter()
2920 .find(|definition| definition.name == name)
2921 .cloned()
2922 }
2923
2924 pub fn materialized_views(&self) -> Vec<crate::catalog::MaterializedViewEntry> {
2925 self.catalog.read().materialized_views.clone()
2926 }
2927
2928 pub fn security_catalog(&self) -> crate::security::SecurityCatalog {
2929 self.catalog.read().security.clone()
2930 }
2931
2932 pub fn security_active_for(&self, table: &str) -> bool {
2933 self.catalog.read().security.table_has_security(table)
2934 }
2935
2936 fn refresh_security_catalog_if_stale(&self, expected_version: u64) -> Result<()> {
2937 if self.catalog.read().security_version == expected_version {
2938 return Ok(());
2939 }
2940 self.security_catalog_disk_reads
2941 .fetch_add(1, Ordering::Relaxed);
2942 let fresh = catalog::read_durable(&self.durable_root, self.meta_dek.as_ref())?
2943 .ok_or_else(|| MongrelError::NotFound("catalog vanished during write".into()))?;
2944 let principal = self.principal.read().clone();
2945 let principal = if fresh.require_auth {
2946 principal
2947 .as_ref()
2948 .and_then(|principal| Self::resolve_bound_principal_from_catalog(&fresh, principal))
2949 } else {
2950 principal
2951 };
2952 self.auth_state.set_require_auth(fresh.require_auth);
2953 *self.catalog.write() = fresh;
2954 *self.principal.write() = principal.clone();
2955 self.auth_state.set_principal(principal);
2956 Ok(())
2957 }
2958
2959 fn security_write(&self) -> Result<parking_lot::RwLockWriteGuard<'_, ()>> {
2960 let guard = self.security_coordinator.gate.write();
2961 let version = self.security_coordinator.version.load(Ordering::Acquire);
2962 self.refresh_security_catalog_if_stale(version)?;
2963 Ok(guard)
2964 }
2965
2966 fn publish_catalog_candidate(
2970 &self,
2971 catalog: Catalog,
2972 epoch: Epoch,
2973 epoch_guard: &mut EpochGuard<'_>,
2974 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
2975 ) -> Result<()> {
2976 self.publish_catalog_candidate_with_prelude(
2977 catalog,
2978 epoch,
2979 epoch_guard,
2980 before_publish,
2981 Vec::new(),
2982 )
2983 }
2984
2985 fn publish_catalog_candidate_with_prelude(
2986 &self,
2987 catalog: Catalog,
2988 epoch: Epoch,
2989 epoch_guard: &mut EpochGuard<'_>,
2990 mut before_publish: Option<&mut dyn FnMut() -> Result<()>>,
2991 prelude: Vec<(u64, crate::wal::Op)>,
2992 ) -> Result<()> {
2993 use crate::wal::DdlOp;
2994
2995 if self.read_only {
2996 return Err(MongrelError::ReadOnlyReplica);
2997 }
2998 if self.poisoned.load(Ordering::Relaxed) {
2999 return Err(MongrelError::Other(
3000 "database poisoned by fsync error".into(),
3001 ));
3002 }
3003 if let Some(before_publish) = before_publish.as_mut() {
3004 (**before_publish)()?;
3005 }
3006 if catalog.db_epoch != epoch.0 {
3007 return Err(MongrelError::InvalidArgument(format!(
3008 "catalog epoch {} does not match commit epoch {}",
3009 catalog.db_epoch, epoch.0
3010 )));
3011 }
3012 {
3013 let current = self.catalog.read();
3014 validate_catalog_transition(¤t, &catalog)?;
3015 }
3016 validate_recovered_catalog(&catalog)?;
3017 let catalog_json = DdlOp::encode_catalog(&catalog)?;
3018 let txn_id = self.alloc_txn_id()?;
3019 let commit_seq = {
3020 let mut wal = self.shared_wal.lock();
3021 let append: Result<u64> = (|| {
3022 for (table_id, op) in prelude {
3023 wal.append(txn_id, table_id, op)?;
3024 }
3025 wal.append(
3026 txn_id,
3027 WAL_TABLE_ID,
3028 crate::wal::Op::Ddl(DdlOp::CatalogSnapshot { catalog_json }),
3029 )?;
3030 wal.append_commit(txn_id, epoch, &[])
3031 })();
3032 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
3033 };
3034 self.await_durable_commit(commit_seq, epoch)?;
3035 let checkpoint = self.checkpoint_catalog_after_durable(catalog);
3036 self.finish_durable_publish(epoch, epoch_guard, checkpoint)
3037 }
3038
3039 fn checkpoint_catalog_after_durable(&self, catalog: Catalog) -> Result<()> {
3043 let checkpoint = catalog::write_atomic(&self.root, &catalog, self.meta_dek.as_ref());
3044 let version = catalog.security_version;
3045 let principal = self.principal.read().clone();
3046 let principal = if catalog.require_auth {
3047 principal.as_ref().and_then(|principal| {
3048 Self::resolve_bound_principal_from_catalog(&catalog, principal)
3049 })
3050 } else {
3051 principal
3052 };
3053 *self.catalog.write() = catalog;
3054 self.security_coordinator
3055 .version
3056 .store(version, Ordering::Release);
3057 self.auth_state
3058 .set_require_auth(self.catalog.read().require_auth);
3059 *self.principal.write() = principal.clone();
3060 self.auth_state.set_principal(principal);
3061 checkpoint
3062 }
3063
3064 fn finish_durable_publish(
3065 &self,
3066 epoch: Epoch,
3067 epoch_guard: &mut EpochGuard<'_>,
3068 post_step: Result<()>,
3069 ) -> Result<()> {
3070 self.epoch.publish_in_order(epoch);
3071 epoch_guard.disarm();
3072 match post_step {
3073 Ok(()) => Ok(()),
3074 Err(error) => {
3075 self.poisoned.store(true, Ordering::Relaxed);
3076 Err(MongrelError::DurableCommit {
3077 epoch: epoch.0,
3078 message: error.to_string(),
3079 })
3080 }
3081 }
3082 }
3083
3084 fn await_durable_commit(&self, commit_seq: u64, epoch: Epoch) -> Result<()> {
3088 match self.group.await_durable(&self.shared_wal, commit_seq) {
3089 Ok(()) => Ok(()),
3090 Err(error) => {
3091 self.poisoned.store(true, Ordering::Relaxed);
3092 Err(MongrelError::CommitOutcomeUnknown {
3093 epoch: epoch.0,
3094 message: error.to_string(),
3095 })
3096 }
3097 }
3098 }
3099
3100 fn commit_outcome_unknown(&self, epoch: Epoch, error: impl std::fmt::Display) -> MongrelError {
3101 self.poisoned.store(true, Ordering::Relaxed);
3102 MongrelError::CommitOutcomeUnknown {
3103 epoch: epoch.0,
3104 message: error.to_string(),
3105 }
3106 }
3107
3108 pub fn set_security_catalog(&self, security: crate::security::SecurityCatalog) -> Result<()> {
3110 self.set_security_catalog_as_with_epoch(security, None)
3111 .map(|_| ())
3112 }
3113
3114 pub fn set_security_catalog_as(
3116 &self,
3117 security: crate::security::SecurityCatalog,
3118 principal: Option<&crate::auth::Principal>,
3119 ) -> Result<()> {
3120 self.set_security_catalog_as_with_epoch(security, principal)
3121 .map(|_| ())
3122 }
3123
3124 pub fn set_security_catalog_as_with_epoch(
3126 &self,
3127 security: crate::security::SecurityCatalog,
3128 principal: Option<&crate::auth::Principal>,
3129 ) -> Result<Epoch> {
3130 self.set_security_catalog_as_with_epoch_inner(security, principal, None)
3131 }
3132
3133 pub fn set_security_catalog_as_with_epoch_controlled<F>(
3136 &self,
3137 security: crate::security::SecurityCatalog,
3138 principal: Option<&crate::auth::Principal>,
3139 mut before_commit: F,
3140 ) -> Result<Epoch>
3141 where
3142 F: FnMut() -> Result<()>,
3143 {
3144 self.set_security_catalog_as_with_epoch_inner(security, principal, Some(&mut before_commit))
3145 }
3146
3147 fn set_security_catalog_as_with_epoch_inner(
3148 &self,
3149 security: crate::security::SecurityCatalog,
3150 principal: Option<&crate::auth::Principal>,
3151 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
3152 ) -> Result<Epoch> {
3153 use crate::wal::DdlOp;
3154 use std::sync::atomic::Ordering;
3155
3156 self.require_for(principal, &crate::auth::Permission::Admin)?;
3157 if self.poisoned.load(Ordering::Relaxed) {
3158 return Err(MongrelError::Other(
3159 "database poisoned by fsync error".into(),
3160 ));
3161 }
3162 let _ddl = self.ddl_lock.lock();
3163 let _security_write = self.security_write()?;
3166 self.require_for(principal, &crate::auth::Permission::Admin)?;
3167 let mut next_catalog = self.catalog.read().clone();
3168 validate_security_catalog(&next_catalog, &security)?;
3169 let payload = DdlOp::encode_security(&security)?;
3170 let _commit = self.commit_lock.lock();
3171 let epoch = self.epoch.bump_assigned();
3172 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
3173 let txn_id = self.alloc_txn_id()?;
3174 next_catalog.security = security;
3175 advance_security_version(&mut next_catalog)?;
3176 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
3177 let commit_seq = {
3178 let mut wal = self.shared_wal.lock();
3179 if let Some(before_commit) = before_commit {
3180 before_commit()?;
3181 }
3182 let append: Result<u64> = (|| {
3183 wal.append(
3184 txn_id,
3185 WAL_TABLE_ID,
3186 crate::wal::Op::Ddl(DdlOp::SetSecurityCatalog {
3187 security_json: payload,
3188 }),
3189 )?;
3190 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
3191 wal.append_commit(txn_id, epoch, &[])
3192 })();
3193 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
3194 };
3195 self.await_durable_commit(commit_seq, epoch)?;
3196 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
3197 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
3198 Ok(epoch)
3199 }
3200
3201 pub fn require_for(
3202 &self,
3203 principal: Option<&crate::auth::Principal>,
3204 permission: &crate::auth::Permission,
3205 ) -> Result<()> {
3206 let Some(principal) = principal else {
3207 return self.require(permission);
3208 };
3209 let resolved;
3210 let principal = if self.auth_state.require_auth() || principal.user_id != 0 {
3211 resolved = Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
3212 .ok_or(MongrelError::AuthRequired)?;
3213 &resolved
3214 } else {
3215 principal
3216 };
3217 #[cfg(test)]
3218 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
3219 if principal.has_permission(permission) {
3220 Ok(())
3221 } else {
3222 Err(MongrelError::PermissionDenied {
3223 required: permission.clone(),
3224 principal: principal.username.clone(),
3225 })
3226 }
3227 }
3228
3229 fn require_exact_principal_current(
3233 &self,
3234 principal: Option<&crate::auth::Principal>,
3235 permission: &crate::auth::Permission,
3236 ) -> Result<()> {
3237 let catalog = self.catalog.read();
3238 if !catalog.require_auth {
3239 return Ok(());
3240 }
3241 let supplied = principal.ok_or(MongrelError::AuthRequired)?;
3242 let current = Self::resolve_bound_principal_from_catalog(&catalog, supplied)
3243 .ok_or(MongrelError::AuthRequired)?;
3244 if current.has_permission(permission) {
3245 Ok(())
3246 } else {
3247 Err(MongrelError::PermissionDenied {
3248 required: permission.clone(),
3249 principal: current.username,
3250 })
3251 }
3252 }
3253
3254 pub(crate) fn with_exact_principal_current<T, F>(
3255 &self,
3256 principal: Option<&crate::auth::Principal>,
3257 permission: &crate::auth::Permission,
3258 operation: F,
3259 ) -> Result<T>
3260 where
3261 F: FnOnce() -> Result<T>,
3262 {
3263 let _security = self.security_coordinator.gate.read();
3264 self.require_exact_principal_current(principal, permission)?;
3265 operation()
3266 }
3267
3268 pub fn principal_snapshot(&self) -> Option<crate::auth::Principal> {
3269 self.principal.read().clone()
3270 }
3271
3272 #[cfg(test)]
3273 pub(crate) fn set_cached_principal_for_test(&self, principal: Option<crate::auth::Principal>) {
3274 *self.principal.write() = principal.clone();
3275 self.auth_state.set_principal(principal);
3276 }
3277
3278 pub fn require_columns_for(
3279 &self,
3280 table: &str,
3281 operation: crate::auth::ColumnOperation,
3282 column_ids: &[u16],
3283 principal: Option<&crate::auth::Principal>,
3284 ) -> Result<()> {
3285 if principal.is_none() && !self.auth_state.require_auth() {
3286 return Ok(());
3287 }
3288 let cached = self.principal.read().clone();
3289 let principal = principal.or(cached.as_ref());
3290 let Some(principal) = principal else {
3291 let permission = match operation {
3292 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
3293 table: table.to_string(),
3294 },
3295 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
3296 table: table.to_string(),
3297 },
3298 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
3299 table: table.to_string(),
3300 },
3301 };
3302 return self.require(&permission);
3303 };
3304 let catalog = self.catalog.read();
3305 let resolved;
3306 let principal = if catalog.require_auth || principal.user_id != 0 {
3307 resolved = Self::resolve_bound_principal_from_catalog(&catalog, principal)
3308 .ok_or(MongrelError::AuthRequired)?;
3309 &resolved
3310 } else {
3311 principal
3312 };
3313 let schema = &catalog
3314 .live(table)
3315 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
3316 .schema;
3317 Self::require_columns_for_principal(table, schema, operation, column_ids, principal)
3318 }
3319
3320 fn require_columns_for_principal(
3321 table: &str,
3322 schema: &Schema,
3323 operation: crate::auth::ColumnOperation,
3324 column_ids: &[u16],
3325 principal: &crate::auth::Principal,
3326 ) -> Result<()> {
3327 #[cfg(test)]
3328 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(decisions.get() + 1));
3329 match principal.column_access(table, operation) {
3330 crate::auth::ColumnAccess::All => Ok(()),
3331 crate::auth::ColumnAccess::Columns(allowed) => {
3332 let denied = column_ids.iter().find_map(|column_id| {
3333 schema
3334 .columns
3335 .iter()
3336 .find(|column| column.id == *column_id)
3337 .filter(|column| !allowed.contains(&column.name))
3338 });
3339 if denied.is_none() {
3340 Ok(())
3341 } else {
3342 Err(MongrelError::PermissionDenied {
3343 required: match operation {
3344 crate::auth::ColumnOperation::Select => {
3345 crate::auth::Permission::SelectColumns {
3346 table: table.to_string(),
3347 columns: denied
3348 .into_iter()
3349 .map(|column| column.name.clone())
3350 .collect(),
3351 }
3352 }
3353 crate::auth::ColumnOperation::Insert => {
3354 crate::auth::Permission::InsertColumns {
3355 table: table.to_string(),
3356 columns: denied
3357 .into_iter()
3358 .map(|column| column.name.clone())
3359 .collect(),
3360 }
3361 }
3362 crate::auth::ColumnOperation::Update => {
3363 crate::auth::Permission::UpdateColumns {
3364 table: table.to_string(),
3365 columns: denied
3366 .into_iter()
3367 .map(|column| column.name.clone())
3368 .collect(),
3369 }
3370 }
3371 },
3372 principal: principal.username.clone(),
3373 })
3374 }
3375 }
3376 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
3377 required: match operation {
3378 crate::auth::ColumnOperation::Select => crate::auth::Permission::Select {
3379 table: table.to_string(),
3380 },
3381 crate::auth::ColumnOperation::Insert => crate::auth::Permission::Insert {
3382 table: table.to_string(),
3383 },
3384 crate::auth::ColumnOperation::Update => crate::auth::Permission::Update {
3385 table: table.to_string(),
3386 },
3387 },
3388 principal: principal.username.clone(),
3389 }),
3390 }
3391 }
3392
3393 pub fn select_column_ids_for(
3394 &self,
3395 table: &str,
3396 principal: Option<&crate::auth::Principal>,
3397 ) -> Result<Vec<u16>> {
3398 let catalog = self.catalog.read();
3399 let columns = catalog
3400 .live(table)
3401 .ok_or_else(|| MongrelError::NotFound(format!("table {table:?} not found")))?
3402 .schema
3403 .columns
3404 .iter()
3405 .map(|column| (column.id, column.name.clone()))
3406 .collect::<Vec<_>>();
3407 let principal = self.principal_for_authorized_read(&catalog, principal, false)?;
3408 drop(catalog);
3409 let Some(principal) = principal.as_ref() else {
3410 self.require(&crate::auth::Permission::Select {
3411 table: table.to_string(),
3412 })?;
3413 return Ok(columns.iter().map(|(id, _)| *id).collect());
3414 };
3415 match principal.column_access(table, crate::auth::ColumnOperation::Select) {
3416 crate::auth::ColumnAccess::All => Ok(columns.iter().map(|(id, _)| *id).collect()),
3417 crate::auth::ColumnAccess::Columns(allowed) => Ok(columns
3418 .iter()
3419 .filter(|(_, name)| allowed.contains(name))
3420 .map(|(id, _)| *id)
3421 .collect()),
3422 crate::auth::ColumnAccess::Denied => Err(MongrelError::PermissionDenied {
3423 required: crate::auth::Permission::Select {
3424 table: table.to_string(),
3425 },
3426 principal: principal.username.clone(),
3427 }),
3428 }
3429 }
3430
3431 pub fn secure_rows_for(
3432 &self,
3433 table: &str,
3434 rows: Vec<crate::memtable::Row>,
3435 principal: Option<&crate::auth::Principal>,
3436 ) -> Result<Vec<crate::memtable::Row>> {
3437 self.secure_rows_for_with_context(table, rows, principal, None)
3438 }
3439
3440 pub fn secure_rows_for_with_context(
3441 &self,
3442 table: &str,
3443 rows: Vec<crate::memtable::Row>,
3444 principal: Option<&crate::auth::Principal>,
3445 context: Option<&crate::query::AiExecutionContext>,
3446 ) -> Result<Vec<crate::memtable::Row>> {
3447 let (security, principal) = {
3448 let catalog = self.catalog.read();
3449 (
3450 catalog.security.clone(),
3451 self.principal_for_authorized_read(&catalog, principal, false)?,
3452 )
3453 };
3454 if !security.table_has_security(table) {
3455 return Ok(rows);
3456 }
3457 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
3458 let mut output = Vec::new();
3459 for mut row in rows {
3460 if let Some(context) = context {
3461 context.consume(1)?;
3462 }
3463 if security.row_allowed(
3464 table,
3465 crate::security::PolicyCommand::Select,
3466 &row,
3467 principal,
3468 false,
3469 ) {
3470 security.apply_masks(table, &mut row, principal);
3471 output.push(row);
3472 }
3473 }
3474 Ok(output)
3475 }
3476
3477 pub fn mask_search_hits_for(
3480 &self,
3481 table: &str,
3482 hits: &mut [crate::query::SearchHit],
3483 principal: Option<&crate::auth::Principal>,
3484 ) -> Result<()> {
3485 let (security, principal) = {
3486 let catalog = self.catalog.read();
3487 (
3488 catalog.security.clone(),
3489 self.principal_for_authorized_read(&catalog, principal, false)?,
3490 )
3491 };
3492 if !security.table_has_security(table) {
3493 return Ok(());
3494 }
3495 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
3496 for hit in hits {
3497 security.apply_masks_to_cells(table, &mut hit.cells, principal);
3498 }
3499 Ok(())
3500 }
3501
3502 pub fn mask_rows_for(
3504 &self,
3505 table: &str,
3506 rows: &mut [crate::memtable::Row],
3507 principal: Option<&crate::auth::Principal>,
3508 ) -> Result<()> {
3509 let (security, principal) = {
3510 let catalog = self.catalog.read();
3511 (
3512 catalog.security.clone(),
3513 self.principal_for_authorized_read(&catalog, principal, false)?,
3514 )
3515 };
3516 if !security.table_has_security(table) {
3517 return Ok(());
3518 }
3519 let principal = principal.as_ref().ok_or(MongrelError::AuthRequired)?;
3520 for row in rows {
3521 security.apply_masks(table, row, principal);
3522 }
3523 Ok(())
3524 }
3525
3526 pub fn authorized_candidate_ids_for(
3528 &self,
3529 table: &str,
3530 principal: Option<&crate::auth::Principal>,
3531 ) -> Result<Option<std::collections::HashSet<RowId>>> {
3532 Ok(self
3533 .authorized_read_snapshot(table, principal)?
3534 .allowed_row_ids)
3535 }
3536
3537 fn allowed_row_ids_locked(
3538 &self,
3539 table_name: &str,
3540 table: &Table,
3541 table_snapshot: Snapshot,
3542 security_state: (&crate::security::SecurityCatalog, u64),
3543 principal: Option<&crate::auth::Principal>,
3544 context: Option<&crate::query::AiExecutionContext>,
3545 ) -> Result<Option<Arc<HashSet<RowId>>>> {
3546 let (security, security_version) = security_state;
3547 if !security.rls_enabled(table_name) {
3548 return Ok(None);
3549 }
3550 let authorization_started = std::time::Instant::now();
3551 let principal = principal.ok_or(MongrelError::AuthRequired)?;
3552 let mut roles = principal.roles.clone();
3553 roles.sort_unstable();
3554 let principal_key = format!(
3555 "{}:{}:{}:{}:{roles:?}",
3556 principal.user_id, principal.created_epoch, principal.username, principal.is_admin
3557 );
3558 let cache_key = (
3559 table_name.to_string(),
3560 table.data_generation(),
3561 security_version,
3562 principal_key,
3563 );
3564 if let Some(allowed) = self.rls_cache.lock().get(&cache_key) {
3565 crate::trace::QueryTrace::record(|trace| {
3566 trace.rls_cache_hit = true;
3567 trace.authorization_nanos = trace
3568 .authorization_nanos
3569 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
3570 });
3571 return Ok(Some(allowed));
3572 }
3573 if let Some(context) = context {
3574 context.checkpoint()?;
3575 }
3576 let started = std::time::Instant::now();
3578 let rows = table.visible_rows(table_snapshot)?;
3579 let rows_evaluated = rows.len() as u64;
3580 let mut allowed = HashSet::new();
3581 for chunk in rows.chunks(256) {
3582 if let Some(context) = context {
3583 context.consume(chunk.len())?;
3584 }
3585 allowed.extend(chunk.iter().filter_map(|row| {
3586 security
3587 .row_allowed(
3588 table_name,
3589 crate::security::PolicyCommand::Select,
3590 row,
3591 principal,
3592 false,
3593 )
3594 .then_some(row.row_id)
3595 }));
3596 }
3597 let allowed = Arc::new(allowed);
3598 let mut cache = self.rls_cache.lock();
3599 cache.build_nanos = cache
3600 .build_nanos
3601 .saturating_add(started.elapsed().as_nanos() as u64);
3602 cache.rows_evaluated = cache.rows_evaluated.saturating_add(rows_evaluated);
3603 cache.insert(cache_key, Arc::clone(&allowed));
3604 crate::trace::QueryTrace::record(|trace| {
3605 trace.rls_rows_evaluated = trace
3606 .rls_rows_evaluated
3607 .saturating_add(rows_evaluated as usize);
3608 trace.authorization_nanos = trace
3609 .authorization_nanos
3610 .saturating_add(authorization_started.elapsed().as_nanos() as u64);
3611 });
3612 Ok(Some(allowed))
3613 }
3614
3615 fn principal_for_authorized_read(
3616 &self,
3617 catalog: &Catalog,
3618 principal: Option<&crate::auth::Principal>,
3619 catalog_bound: bool,
3620 ) -> Result<Option<crate::auth::Principal>> {
3621 let principal = principal.cloned().or_else(|| self.principal.read().clone());
3622 let Some(principal) = principal else {
3623 return Ok(None);
3624 };
3625 if catalog.require_auth || catalog_bound || principal.user_id != 0 {
3626 return Self::resolve_bound_principal_from_catalog(catalog, &principal)
3627 .map(Some)
3628 .ok_or(MongrelError::AuthRequired);
3629 }
3630 Ok(Some(principal))
3631 }
3632
3633 pub fn with_authorized_read<T, F>(
3637 &self,
3638 table_name: &str,
3639 principal: Option<&crate::auth::Principal>,
3640 catalog_bound: bool,
3641 read: F,
3642 ) -> Result<T>
3643 where
3644 F: FnMut(
3645 &mut Table,
3646 Snapshot,
3647 Option<&HashSet<RowId>>,
3648 Option<&crate::auth::Principal>,
3649 ) -> Result<T>,
3650 {
3651 self.with_authorized_read_context(
3652 table_name,
3653 principal,
3654 catalog_bound,
3655 None,
3656 None,
3657 None,
3658 read,
3659 )
3660 }
3661
3662 #[allow(clippy::too_many_arguments)]
3663 pub fn with_authorized_read_context<T, F>(
3664 &self,
3665 table_name: &str,
3666 principal: Option<&crate::auth::Principal>,
3667 catalog_bound: bool,
3668 authorization: Option<&ReadAuthorization>,
3669 context: Option<&crate::query::AiExecutionContext>,
3670 snapshot_override: Option<Snapshot>,
3671 read: F,
3672 ) -> Result<T>
3673 where
3674 F: FnMut(
3675 &mut Table,
3676 Snapshot,
3677 Option<&HashSet<RowId>>,
3678 Option<&crate::auth::Principal>,
3679 ) -> Result<T>,
3680 {
3681 self.with_authorized_read_context_stamped(
3682 table_name,
3683 principal,
3684 catalog_bound,
3685 authorization,
3686 context,
3687 snapshot_override,
3688 read,
3689 )
3690 .map(|(result, _)| result)
3691 }
3692
3693 #[allow(clippy::too_many_arguments)]
3694 pub fn with_authorized_read_context_stamped<T, F>(
3695 &self,
3696 table_name: &str,
3697 principal: Option<&crate::auth::Principal>,
3698 catalog_bound: bool,
3699 authorization: Option<&ReadAuthorization>,
3700 context: Option<&crate::query::AiExecutionContext>,
3701 snapshot_override: Option<Snapshot>,
3702 mut read: F,
3703 ) -> Result<(T, AuthorizedReadStamp)>
3704 where
3705 F: FnMut(
3706 &mut Table,
3707 Snapshot,
3708 Option<&HashSet<RowId>>,
3709 Option<&crate::auth::Principal>,
3710 ) -> Result<T>,
3711 {
3712 if principal.is_none() && self.principal.read().is_some() {
3713 self.refresh_principal()?;
3714 }
3715 const RETRIES: usize = 3;
3716 let handle = self.table(table_name)?;
3717 for attempt in 0..RETRIES {
3718 crate::trace::QueryTrace::record(|trace| {
3719 trace.authorization_retries = attempt;
3720 });
3721 let (security, security_version, effective_principal) = {
3722 let catalog = self.catalog.read();
3723 (
3724 catalog.security.clone(),
3725 catalog.security_version,
3726 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
3727 )
3728 };
3729 if let Some(authorization) = authorization {
3730 for permission in &authorization.permissions {
3731 self.require_for(effective_principal.as_ref(), permission)?;
3732 }
3733 self.require_columns_for(
3734 table_name,
3735 authorization.operation,
3736 &authorization.columns,
3737 effective_principal.as_ref(),
3738 )?;
3739 }
3740 let result = {
3741 let mut table = lock_table_with_context(&handle, context)?;
3742 let snapshot = snapshot_override.unwrap_or_else(|| table.snapshot());
3743 let allowed = self.allowed_row_ids_locked(
3744 table_name,
3745 &table,
3746 snapshot,
3747 (&security, security_version),
3748 effective_principal.as_ref(),
3749 context,
3750 )?;
3751 let stamp = AuthorizedReadStamp {
3752 table_id: table.table_id(),
3753 schema_id: table.schema().schema_id,
3754 data_generation: table.data_generation(),
3755 security_version,
3756 snapshot,
3757 };
3758 let result = read(
3759 &mut table,
3760 snapshot,
3761 allowed.as_deref(),
3762 effective_principal.as_ref(),
3763 )?;
3764 (result, stamp)
3765 };
3766 if let Some(context) = context {
3767 context.checkpoint()?;
3768 }
3769 if self.catalog.read().security_version == security_version {
3770 return Ok(result);
3771 }
3772 if attempt + 1 == RETRIES {
3773 return Err(MongrelError::Conflict(
3774 "security policy changed during scored read".into(),
3775 ));
3776 }
3777 }
3778 Err(MongrelError::Conflict(
3779 "authorization retry loop exhausted".into(),
3780 ))
3781 }
3782
3783 fn with_authorized_aggregate_table<T, F>(
3784 &self,
3785 table_name: &str,
3786 columns: &[u16],
3787 principal: Option<&crate::auth::Principal>,
3788 catalog_bound: bool,
3789 allow_table_security: bool,
3790 mut aggregate: F,
3791 ) -> Result<T>
3792 where
3793 F: FnMut(
3794 &mut Table,
3795 Option<&crate::security::CandidateAuthorization<'_>>,
3796 Option<&crate::auth::Principal>,
3797 u64,
3798 ) -> Result<T>,
3799 {
3800 if principal.is_none() && self.principal.read().is_some() {
3801 self.refresh_principal()?;
3802 }
3803 const RETRIES: usize = 3;
3804 let handle = self.table(table_name)?;
3805 for attempt in 0..RETRIES {
3806 let (security, security_version, effective_principal) = {
3807 let catalog = self.catalog.read();
3808 (
3809 catalog.security.clone(),
3810 catalog.security_version,
3811 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
3812 )
3813 };
3814 self.require_columns_for(
3815 table_name,
3816 crate::auth::ColumnOperation::Select,
3817 columns,
3818 effective_principal.as_ref(),
3819 )?;
3820 if !allow_table_security && security.table_has_security(table_name) {
3821 return Err(MongrelError::InvalidArgument(
3822 "incremental aggregate is unsupported while RLS or column masks are active"
3823 .into(),
3824 ));
3825 }
3826 let result = {
3827 let mut table = handle.lock();
3828 let authorization = if security.rls_enabled(table_name) {
3829 Some(crate::security::CandidateAuthorization {
3830 table: table_name,
3831 security: &security,
3832 principal: effective_principal
3833 .as_ref()
3834 .ok_or(MongrelError::AuthRequired)?,
3835 })
3836 } else {
3837 None
3838 };
3839 aggregate(
3840 &mut table,
3841 authorization.as_ref(),
3842 effective_principal.as_ref(),
3843 security_version,
3844 )?
3845 };
3846 if self.catalog.read().security_version == security_version {
3847 return Ok(result);
3848 }
3849 if attempt + 1 == RETRIES {
3850 return Err(MongrelError::Conflict(
3851 "security policy changed during aggregate read".into(),
3852 ));
3853 }
3854 }
3855 Err(MongrelError::Conflict(
3856 "aggregate authorization retry loop exhausted".into(),
3857 ))
3858 }
3859
3860 pub fn with_authorized_scored_read_context<T, F>(
3864 &self,
3865 table_name: &str,
3866 principal: Option<&crate::auth::Principal>,
3867 catalog_bound: bool,
3868 authorization: Option<&ReadAuthorization>,
3869 context: Option<&crate::query::AiExecutionContext>,
3870 mut read: F,
3871 ) -> Result<T>
3872 where
3873 F: FnMut(
3874 &mut Table,
3875 Snapshot,
3876 Option<&crate::security::CandidateAuthorization<'_>>,
3877 Option<&crate::auth::Principal>,
3878 ) -> Result<T>,
3879 {
3880 self.with_authorized_scored_read_context_at(
3881 table_name,
3882 principal,
3883 catalog_bound,
3884 authorization,
3885 context,
3886 None,
3887 |table, snapshot, authorization, principal| {
3888 let mut table = table.clone();
3889 read(&mut table, snapshot, authorization, principal)
3890 },
3891 )
3892 }
3893
3894 #[allow(clippy::too_many_arguments)]
3895 pub fn with_authorized_scored_read_context_at<T, F>(
3896 &self,
3897 table_name: &str,
3898 principal: Option<&crate::auth::Principal>,
3899 catalog_bound: bool,
3900 authorization: Option<&ReadAuthorization>,
3901 context: Option<&crate::query::AiExecutionContext>,
3902 snapshot_override: Option<Snapshot>,
3903 read: F,
3904 ) -> Result<T>
3905 where
3906 F: FnMut(
3907 &Table,
3908 Snapshot,
3909 Option<&crate::security::CandidateAuthorization<'_>>,
3910 Option<&crate::auth::Principal>,
3911 ) -> Result<T>,
3912 {
3913 self.with_authorized_scored_read_context_at_stamped(
3914 table_name,
3915 principal,
3916 catalog_bound,
3917 authorization,
3918 context,
3919 snapshot_override,
3920 read,
3921 )
3922 .map(|(result, _)| result)
3923 }
3924
3925 #[allow(clippy::too_many_arguments)]
3926 pub fn with_authorized_scored_read_context_at_stamped<T, F>(
3927 &self,
3928 table_name: &str,
3929 principal: Option<&crate::auth::Principal>,
3930 catalog_bound: bool,
3931 authorization: Option<&ReadAuthorization>,
3932 context: Option<&crate::query::AiExecutionContext>,
3933 snapshot_override: Option<Snapshot>,
3934 mut read: F,
3935 ) -> Result<(T, AuthorizedReadStamp)>
3936 where
3937 F: FnMut(
3938 &Table,
3939 Snapshot,
3940 Option<&crate::security::CandidateAuthorization<'_>>,
3941 Option<&crate::auth::Principal>,
3942 ) -> Result<T>,
3943 {
3944 if principal.is_none() && self.principal.read().is_some() {
3945 self.refresh_principal()?;
3946 }
3947 const RETRIES: usize = 3;
3948 let handle = self.table(table_name)?;
3949 for attempt in 0..RETRIES {
3950 if let Some(context) = context {
3951 context.checkpoint()?;
3952 }
3953 crate::trace::QueryTrace::record(|trace| {
3954 trace.authorization_retries = attempt;
3955 });
3956 let (security, security_version, effective_principal) = {
3957 let catalog = self.catalog.read();
3958 (
3959 catalog.security.clone(),
3960 catalog.security_version,
3961 self.principal_for_authorized_read(&catalog, principal, catalog_bound)?,
3962 )
3963 };
3964 if let Some(authorization) = authorization {
3965 for permission in &authorization.permissions {
3966 self.require_for(effective_principal.as_ref(), permission)?;
3967 }
3968 self.require_columns_for(
3969 table_name,
3970 authorization.operation,
3971 &authorization.columns,
3972 effective_principal.as_ref(),
3973 )?;
3974 }
3975 let result = {
3976 let (table, snapshot, _snapshot_guard, _run_pins) =
3977 self.scored_read_generation(&handle, context, snapshot_override)?;
3978 let candidate_authorization = if security.rls_enabled(table_name) {
3979 Some(crate::security::CandidateAuthorization {
3980 table: table_name,
3981 security: &security,
3982 principal: effective_principal
3983 .as_ref()
3984 .ok_or(MongrelError::AuthRequired)?,
3985 })
3986 } else {
3987 None
3988 };
3989 let stamp = AuthorizedReadStamp {
3990 table_id: table.table_id(),
3991 schema_id: table.schema().schema_id,
3992 data_generation: table.data_generation(),
3993 security_version,
3994 snapshot,
3995 };
3996 let result = read(
3997 table.as_ref(),
3998 snapshot,
3999 candidate_authorization.as_ref(),
4000 effective_principal.as_ref(),
4001 )?;
4002 (result, stamp)
4003 };
4004 if let Some(context) = context {
4005 context.checkpoint()?;
4006 }
4007 if self.catalog.read().security_version == security_version {
4008 return Ok(result);
4009 }
4010 if attempt + 1 == RETRIES {
4011 return Err(MongrelError::Conflict(
4012 "security policy changed during scored read".into(),
4013 ));
4014 }
4015 }
4016 Err(MongrelError::Conflict(
4017 "scored-read authorization retry loop exhausted".into(),
4018 ))
4019 }
4020
4021 fn scored_read_generation(
4022 &self,
4023 handle: &TableHandle,
4024 context: Option<&crate::query::AiExecutionContext>,
4025 snapshot_override: Option<Snapshot>,
4026 ) -> Result<(
4027 Arc<TableReadGeneration>,
4028 Snapshot,
4029 crate::retention::OwnedSnapshotGuard,
4030 RunPins,
4031 )> {
4032 let mut table = if let Some(context) = context {
4033 loop {
4034 context.checkpoint()?;
4035 let wait = context
4036 .remaining_duration()
4037 .unwrap_or(std::time::Duration::from_millis(5))
4038 .min(std::time::Duration::from_millis(5));
4039 if let Some(table) = handle.try_lock_for(wait) {
4040 break table;
4041 }
4042 }
4043 } else {
4044 handle.lock()
4045 };
4046 let (snapshot, snapshot_guard) = if let Some(snapshot) = snapshot_override {
4047 self.snapshot_at_owned(snapshot.epoch)?
4048 } else {
4049 let snapshot = table.snapshot();
4050 let guard = self.snapshots.register_owned(snapshot.epoch);
4051 (snapshot, guard)
4052 };
4053 let table_id = table.table_id();
4054 let run_keys: Vec<_> = table
4055 .active_run_ids()
4056 .map(|run_id| (table_id, run_id))
4057 .collect();
4058 let generation = handle
4059 .generation_metrics
4060 .activate(table.clone_read_generation()?);
4061 let run_pins = self.pin_runs(&run_keys);
4062 Ok((generation, snapshot, snapshot_guard, run_pins))
4063 }
4064
4065 fn pin_runs(&self, runs: &[(u64, u128)]) -> RunPins {
4066 let mut pins = self.backup_pins.lock();
4067 for run in runs {
4068 *pins.entry(*run).or_insert(0) += 1;
4069 }
4070 drop(pins);
4071 RunPins {
4072 pins: Arc::clone(&self.backup_pins),
4073 runs: runs.to_vec(),
4074 }
4075 }
4076
4077 pub fn query_for_current_principal(
4081 &self,
4082 table_name: &str,
4083 query: &crate::query::Query,
4084 projection: Option<&[u16]>,
4085 ) -> Result<Vec<crate::memtable::Row>> {
4086 let condition_columns = crate::query::condition_columns(&query.conditions);
4087 self.with_authorized_read(
4088 table_name,
4089 None,
4090 true,
4091 |table, snapshot, allowed, principal| {
4092 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
4093 self.require_columns_for(
4094 table_name,
4095 crate::auth::ColumnOperation::Select,
4096 &condition_columns,
4097 principal,
4098 )?;
4099 if let Some(projection) = projection {
4100 self.require_columns_for(
4101 table_name,
4102 crate::auth::ColumnOperation::Select,
4103 projection,
4104 principal,
4105 )?;
4106 }
4107 let mut rows = table.query_at_with_allowed(query, snapshot, allowed)?;
4108 let projection =
4109 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
4110 for row in &mut rows {
4111 row.columns.retain(|column, _| {
4112 allowed_columns.contains(column)
4113 && projection
4114 .as_ref()
4115 .is_none_or(|projection| projection.contains(column))
4116 });
4117 }
4118 self.secure_rows_for(table_name, rows, principal)
4119 },
4120 )
4121 }
4122
4123 pub fn query_for_current_principal_controlled(
4127 &self,
4128 table_name: &str,
4129 query: &crate::query::Query,
4130 projection: Option<&[u16]>,
4131 control: &crate::ExecutionControl,
4132 ) -> Result<Vec<crate::memtable::Row>> {
4133 self.query_for_principal_controlled(table_name, query, projection, None, true, control)
4134 }
4135
4136 fn query_for_principal_controlled(
4137 &self,
4138 table_name: &str,
4139 query: &crate::query::Query,
4140 projection: Option<&[u16]>,
4141 principal: Option<&crate::auth::Principal>,
4142 catalog_bound: bool,
4143 control: &crate::ExecutionControl,
4144 ) -> Result<Vec<crate::memtable::Row>> {
4145 control.checkpoint()?;
4146 let context = crate::query::AiExecutionContext::with_control(
4147 control.clone(),
4148 usize::MAX,
4149 crate::query::MAX_FUSED_CANDIDATES,
4150 );
4151 let condition_columns = crate::query::condition_columns(&query.conditions);
4152 self.with_authorized_read_context(
4153 table_name,
4154 principal,
4155 catalog_bound,
4156 None,
4157 Some(&context),
4158 None,
4159 |table, snapshot, allowed, principal| {
4160 control.checkpoint()?;
4161 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
4162 self.require_columns_for(
4163 table_name,
4164 crate::auth::ColumnOperation::Select,
4165 &condition_columns,
4166 principal,
4167 )?;
4168 if let Some(projection) = projection {
4169 self.require_columns_for(
4170 table_name,
4171 crate::auth::ColumnOperation::Select,
4172 projection,
4173 principal,
4174 )?;
4175 }
4176 let rows =
4177 table.query_at_with_allowed_controlled(query, snapshot, allowed, control)?;
4178 let projection =
4179 projection.map(|columns| columns.iter().copied().collect::<HashSet<_>>());
4180 let mut projected = Vec::with_capacity(rows.len());
4181 for (index, mut row) in rows.into_iter().enumerate() {
4182 if index & 255 == 0 {
4183 control.checkpoint()?;
4184 }
4185 row.columns.retain(|column, _| {
4186 allowed_columns.contains(column)
4187 && projection
4188 .as_ref()
4189 .is_none_or(|projection| projection.contains(column))
4190 });
4191 projected.push(row);
4192 }
4193 self.secure_rows_for_with_context(table_name, projected, principal, Some(&context))
4194 },
4195 )
4196 }
4197
4198 pub fn approx_aggregate_for_current_principal(
4201 &self,
4202 table_name: &str,
4203 conditions: &[crate::query::Condition],
4204 column: Option<u16>,
4205 agg: crate::engine::ApproxAgg,
4206 z: f64,
4207 ) -> Result<Option<crate::engine::ApproxResult>> {
4208 if !z.is_finite() || z <= 0.0 {
4209 return Err(MongrelError::InvalidArgument(
4210 "z must be finite and > 0".into(),
4211 ));
4212 }
4213 let mut columns = crate::query::condition_columns(conditions);
4214 columns.extend(column);
4215 columns.sort_unstable();
4216 columns.dedup();
4217 self.with_authorized_aggregate_table(
4218 table_name,
4219 &columns,
4220 None,
4221 true,
4222 true,
4223 |table, authorization, _, _| {
4224 table.approx_aggregate_with_candidate_authorization(
4225 conditions,
4226 column,
4227 agg,
4228 z,
4229 authorization,
4230 )
4231 },
4232 )
4233 }
4234
4235 pub fn incremental_aggregate_for_current_principal(
4239 &self,
4240 table_name: &str,
4241 conditions: &[crate::query::Condition],
4242 column: Option<u16>,
4243 agg: crate::engine::NativeAgg,
4244 ) -> Result<crate::engine::IncrementalAggResult> {
4245 self.incremental_aggregate_for_principal(table_name, conditions, column, agg, None, true)
4246 }
4247
4248 pub fn incremental_aggregate_for_principal(
4252 &self,
4253 table_name: &str,
4254 conditions: &[crate::query::Condition],
4255 column: Option<u16>,
4256 agg: crate::engine::NativeAgg,
4257 principal: Option<&crate::auth::Principal>,
4258 catalog_bound: bool,
4259 ) -> Result<crate::engine::IncrementalAggResult> {
4260 let mut columns = crate::query::condition_columns(conditions);
4261 columns.extend(column);
4262 columns.sort_unstable();
4263 columns.dedup();
4264 self.with_authorized_aggregate_table(
4265 table_name,
4266 &columns,
4267 principal,
4268 catalog_bound,
4269 false,
4270 |table, _, principal, security_version| {
4271 let cache_key = incremental_aggregate_cache_key(
4272 table_name,
4273 conditions,
4274 column,
4275 agg,
4276 principal,
4277 security_version,
4278 );
4279 table.aggregate_incremental(cache_key, conditions, column, agg)
4280 },
4281 )
4282 }
4283
4284 pub fn get_for_current_principal(
4287 &self,
4288 table_name: &str,
4289 row_id: RowId,
4290 ) -> Result<Option<crate::memtable::Row>> {
4291 self.with_authorized_read(
4292 table_name,
4293 None,
4294 true,
4295 |table, snapshot, allowed, principal| {
4296 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
4297 let Some(row) = table.get(row_id, snapshot) else {
4298 return Ok(None);
4299 };
4300 if allowed.is_some_and(|allowed| !allowed.contains(&row.row_id)) {
4301 return Ok(None);
4302 }
4303 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
4304 if let Some(row) = rows.first_mut() {
4305 row.columns
4306 .retain(|column, _| allowed_columns.contains(column));
4307 }
4308 Ok(rows.pop())
4309 },
4310 )
4311 }
4312
4313 pub fn ann_rerank_for_current_principal(
4316 &self,
4317 table_name: &str,
4318 request: &crate::query::AnnRerankRequest,
4319 ) -> Result<Vec<crate::query::AnnRerankHit>> {
4320 self.with_authorized_scored_read_context_at(
4321 table_name,
4322 None,
4323 true,
4324 Some(&ReadAuthorization {
4325 operation: crate::auth::ColumnOperation::Select,
4326 columns: vec![request.column_id],
4327 permissions: Vec::new(),
4328 }),
4329 None,
4330 None,
4331 |table, snapshot, authorization, principal| {
4332 self.require_columns_for(
4333 table_name,
4334 crate::auth::ColumnOperation::Select,
4335 &[request.column_id],
4336 principal,
4337 )?;
4338 table.ann_rerank_at_with_candidate_authorization_on_generation(
4339 request,
4340 snapshot,
4341 authorization,
4342 None,
4343 )
4344 },
4345 )
4346 }
4347
4348 pub fn authorized_read_snapshot(
4351 &self,
4352 table: &str,
4353 principal: Option<&crate::auth::Principal>,
4354 ) -> Result<AuthorizedReadSnapshot> {
4355 let (security, security_version, effective_principal) = {
4356 let catalog = self.catalog.read();
4357 (
4358 catalog.security.clone(),
4359 catalog.security_version,
4360 self.principal_for_authorized_read(&catalog, principal, false)?,
4361 )
4362 };
4363 let handle = self.table(table)?;
4364 let (table_snapshot, data_generation, allowed_row_ids) = {
4365 let table_handle = handle.lock();
4366 let table_snapshot = table_handle.snapshot();
4367 let data_generation = table_handle.data_generation();
4368 let allowed = self.allowed_row_ids_locked(
4369 table,
4370 &table_handle,
4371 table_snapshot,
4372 (&security, security_version),
4373 effective_principal.as_ref(),
4374 None,
4375 )?;
4376 (
4377 table_snapshot,
4378 data_generation,
4379 allowed.map(|allowed| (*allowed).clone()),
4380 )
4381 };
4382 Ok(AuthorizedReadSnapshot {
4383 table: table.to_string(),
4384 table_snapshot,
4385 data_generation,
4386 security_version,
4387 allowed_row_ids,
4388 })
4389 }
4390
4391 pub fn authorized_read_snapshot_valid(&self, snapshot: &AuthorizedReadSnapshot) -> bool {
4392 if self.catalog.read().security_version != snapshot.security_version {
4393 return false;
4394 }
4395 self.table(&snapshot.table)
4396 .ok()
4397 .is_some_and(|table| table.lock().data_generation() == snapshot.data_generation)
4398 }
4399
4400 pub fn rls_cache_stats(&self) -> RlsCacheStats {
4401 self.rls_cache.lock().stats()
4402 }
4403
4404 pub fn rows_for(
4406 &self,
4407 table: &str,
4408 principal: Option<&crate::auth::Principal>,
4409 ) -> Result<Vec<crate::memtable::Row>> {
4410 if principal.is_none() && self.principal.read().is_some() {
4411 self.refresh_principal()?;
4412 }
4413 let allowed = self.select_column_ids_for(table, principal)?;
4414 let handle = self.table(table)?;
4415 let rows = {
4416 let table = handle.lock();
4417 table.visible_rows(table.snapshot())?
4418 };
4419 let mut rows = self.secure_rows_for(table, rows, principal)?;
4420 for row in &mut rows {
4421 row.columns.retain(|column, _| allowed.contains(column));
4422 }
4423 Ok(rows)
4424 }
4425
4426 pub fn rows_at_epoch_for_current_principal(
4429 &self,
4430 table_name: &str,
4431 snapshot: Snapshot,
4432 ) -> Result<Vec<crate::memtable::Row>> {
4433 self.with_authorized_read_context(
4434 table_name,
4435 None,
4436 true,
4437 Some(&ReadAuthorization {
4438 operation: crate::auth::ColumnOperation::Select,
4439 columns: Vec::new(),
4440 permissions: Vec::new(),
4441 }),
4442 None,
4443 Some(snapshot),
4444 |table, snapshot, allowed, principal| {
4445 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
4446 let mut rows = table.visible_rows(snapshot)?;
4447 if let Some(allowed) = allowed {
4448 rows.retain(|row| allowed.contains(&row.row_id));
4449 }
4450 rows = self.secure_rows_for(table_name, rows, principal)?;
4451 for row in &mut rows {
4452 row.columns
4453 .retain(|column, _| allowed_columns.contains(column));
4454 }
4455 Ok(rows)
4456 },
4457 )
4458 }
4459
4460 pub fn count_for(
4462 &self,
4463 table: &str,
4464 principal: Option<&crate::auth::Principal>,
4465 ) -> Result<u64> {
4466 if principal.is_none() && self.principal.read().is_some() {
4467 self.refresh_principal()?;
4468 }
4469 self.select_column_ids_for(table, principal)?;
4470 if self.security_active_for(table) {
4471 Ok(self.rows_for(table, principal)?.len() as u64)
4472 } else {
4473 Ok(self.table(table)?.lock().count())
4474 }
4475 }
4476
4477 pub fn put_for(
4479 &self,
4480 table: &str,
4481 mut cells: Vec<(u16, crate::memtable::Value)>,
4482 principal: Option<&crate::auth::Principal>,
4483 ) -> Result<RowId> {
4484 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
4485 self.require_columns_for(
4486 table,
4487 crate::auth::ColumnOperation::Insert,
4488 &columns,
4489 principal,
4490 )?;
4491 let handle = self.table(table)?;
4492 let mut table_handle = handle.lock();
4493 table_handle.fill_auto_inc(&mut cells)?;
4494 table_handle.apply_defaults(&mut cells)?;
4495 let mut row = crate::memtable::Row::new(RowId(0), self.epoch.visible());
4496 row.columns.extend(cells.iter().cloned());
4497 self.check_row_policy_for(
4498 table,
4499 crate::security::PolicyCommand::Insert,
4500 &row,
4501 true,
4502 principal,
4503 )?;
4504 table_handle.put(cells)
4505 }
4506
4507 pub fn check_row_policy_for(
4508 &self,
4509 table: &str,
4510 command: crate::security::PolicyCommand,
4511 row: &crate::memtable::Row,
4512 check_new: bool,
4513 principal: Option<&crate::auth::Principal>,
4514 ) -> Result<()> {
4515 let security = self.catalog.read().security.clone();
4516 if !security.rls_enabled(table) {
4517 return Ok(());
4518 }
4519 let cached = self.principal.read().clone();
4520 let principal = principal
4521 .or(cached.as_ref())
4522 .ok_or(MongrelError::AuthRequired)?;
4523 if security.row_allowed(table, command, row, principal, check_new) {
4524 return Ok(());
4525 }
4526 let required = match command {
4527 crate::security::PolicyCommand::Insert => crate::auth::Permission::Insert {
4528 table: table.to_string(),
4529 },
4530 crate::security::PolicyCommand::Update => crate::auth::Permission::Update {
4531 table: table.to_string(),
4532 },
4533 crate::security::PolicyCommand::Select => crate::auth::Permission::Select {
4534 table: table.to_string(),
4535 },
4536 crate::security::PolicyCommand::Delete | crate::security::PolicyCommand::All => {
4537 crate::auth::Permission::Delete {
4538 table: table.to_string(),
4539 }
4540 }
4541 };
4542 Err(MongrelError::PermissionDenied {
4543 required,
4544 principal: principal.username.clone(),
4545 })
4546 }
4547
4548 pub fn set_materialized_view(
4551 &self,
4552 definition: crate::catalog::MaterializedViewEntry,
4553 ) -> Result<()> {
4554 self.set_materialized_view_with_epoch(definition)
4555 .map(|_| ())
4556 }
4557
4558 pub fn set_materialized_view_with_epoch(
4560 &self,
4561 definition: crate::catalog::MaterializedViewEntry,
4562 ) -> Result<Epoch> {
4563 use crate::wal::DdlOp;
4564 use std::sync::atomic::Ordering;
4565
4566 self.require(&crate::auth::Permission::Ddl)?;
4567 if self.poisoned.load(Ordering::Relaxed) {
4568 return Err(MongrelError::Other(
4569 "database poisoned by fsync error".into(),
4570 ));
4571 }
4572 if definition.name.is_empty() || definition.query.trim().is_empty() {
4573 return Err(MongrelError::InvalidArgument(
4574 "materialized view name and query must not be empty".into(),
4575 ));
4576 }
4577
4578 let _ddl = self.ddl_lock.lock();
4579 let _security_write = self.security_write()?;
4580 self.require(&crate::auth::Permission::Ddl)?;
4581 let table_id = self
4582 .catalog
4583 .read()
4584 .live(&definition.name)
4585 .ok_or_else(|| {
4586 MongrelError::NotFound(format!(
4587 "materialized view table {:?} not found",
4588 definition.name
4589 ))
4590 })?
4591 .table_id;
4592 let definition_json = DdlOp::encode_materialized_view(&definition)?;
4593 let _commit = self.commit_lock.lock();
4594 let epoch = self.epoch.bump_assigned();
4595 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
4596 let txn_id = self.alloc_txn_id()?;
4597 let mut next_catalog = self.catalog.read().clone();
4598 if let Some(existing) = next_catalog
4599 .materialized_views
4600 .iter_mut()
4601 .find(|existing| existing.name == definition.name)
4602 {
4603 *existing = definition.clone();
4604 } else {
4605 next_catalog.materialized_views.push(definition.clone());
4606 }
4607 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
4608 let commit_seq = {
4609 let mut wal = self.shared_wal.lock();
4610 let append: Result<u64> = (|| {
4611 wal.append(
4612 txn_id,
4613 table_id,
4614 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
4615 name: definition.name.clone(),
4616 definition_json,
4617 }),
4618 )?;
4619 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
4620 wal.append_commit(txn_id, epoch, &[])
4621 })();
4622 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
4623 };
4624 self.await_durable_commit(commit_seq, epoch)?;
4625
4626 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
4627 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
4628 Ok(epoch)
4629 }
4630
4631 pub fn root(&self) -> &Path {
4633 self.durable_root.canonical_path()
4634 }
4635
4636 pub fn durable_root(&self) -> Arc<crate::durable_file::DurableRoot> {
4639 Arc::clone(&self.durable_root)
4640 }
4641
4642 #[cfg(feature = "encryption")]
4646 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
4647 self.kek
4648 .as_deref()
4649 .map(|kek| kek.derive_subkey(b"mongreldb/server/idempotency/v1"))
4650 }
4651
4652 #[cfg(not(feature = "encryption"))]
4653 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
4654 None
4655 }
4656
4657 pub fn is_read_only_replica(&self) -> bool {
4658 self.read_only
4659 }
4660
4661 pub fn ensure_consistent_read(&self) -> Result<()> {
4665 self.ensure_owner_process()?;
4666 if self.poisoned.load(Ordering::Relaxed) {
4667 return Err(MongrelError::Other(
4668 "database poisoned by post-commit failure; reopen required".into(),
4669 ));
4670 }
4671 Ok(())
4672 }
4673
4674 pub fn set_replication_wal_retention_segments(&self, segments: usize) {
4675 self.replication_wal_retention_segments
4676 .store(segments, std::sync::atomic::Ordering::Relaxed);
4677 }
4678
4679 pub fn replication_snapshot(&self) -> Result<crate::replication::ReplicationSnapshot> {
4684 let admin = crate::auth::Permission::Admin;
4685 self.require(&admin)?;
4686 let operation_principal = self.principal_snapshot();
4687 let _barrier = self.replication_barrier.write();
4688 let _ddl = self.ddl_lock.lock();
4689 let _security = self.security_coordinator.gate.read();
4690 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4691 let mut handles: Vec<_> = self
4692 .tables
4693 .read()
4694 .iter()
4695 .map(|(id, handle)| (*id, handle.clone()))
4696 .collect();
4697 handles.sort_by_key(|(id, _)| *id);
4698 let _table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
4699 let _commit = self.commit_lock.lock();
4700 let mut wal = self.shared_wal.lock();
4701 wal.group_sync()?;
4702 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
4703 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
4704 let epoch = records
4705 .iter()
4706 .filter_map(|record| match &record.op {
4707 crate::wal::Op::TxnCommit { epoch, .. } => Some(*epoch),
4708 _ => None,
4709 })
4710 .max()
4711 .unwrap_or(0)
4712 .max(self.epoch.committed().0);
4713 let files = crate::replication::capture_files(&self.root)?;
4714 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
4715 drop(wal);
4716 Ok(crate::replication::ReplicationSnapshot::new(
4717 source_id, epoch, files,
4718 ))
4719 }
4720
4721 pub fn hot_backup(&self, destination: impl AsRef<Path>) -> Result<crate::backup::BackupReport> {
4729 let control = crate::ExecutionControl::new(None);
4730 self.hot_backup_controlled(destination, &control, || true)
4731 }
4732
4733 pub(crate) fn hot_backup_to_durable_child(
4734 &self,
4735 parent: &crate::durable_file::DurableRoot,
4736 child: &Path,
4737 control: &crate::ExecutionControl,
4738 ) -> Result<crate::backup::BackupReport> {
4739 let mut components = child.components();
4740 if !matches!(components.next(), Some(std::path::Component::Normal(_)))
4741 || components.next().is_some()
4742 {
4743 return Err(MongrelError::InvalidArgument(
4744 "durable backup child must be one relative path component".into(),
4745 ));
4746 }
4747 let destination_name = child.file_name().ok_or_else(|| {
4748 MongrelError::InvalidArgument("durable backup child has no filename".into())
4749 })?;
4750 let prepared = prepare_backup_destination_in(&self.root, parent, destination_name)?;
4751 self.hot_backup_prepared(prepared, control, || true)
4752 }
4753
4754 #[doc(hidden)]
4757 pub fn hot_backup_controlled<F>(
4758 &self,
4759 destination: impl AsRef<Path>,
4760 control: &crate::ExecutionControl,
4761 before_publish: F,
4762 ) -> Result<crate::backup::BackupReport>
4763 where
4764 F: FnOnce() -> bool,
4765 {
4766 let prepared = prepare_backup_destination(&self.root, destination.as_ref())?;
4767 self.hot_backup_prepared(prepared, control, before_publish)
4768 }
4769
4770 fn hot_backup_prepared<F>(
4771 &self,
4772 mut prepared: PreparedBackupDestination,
4773 control: &crate::ExecutionControl,
4774 before_publish: F,
4775 ) -> Result<crate::backup::BackupReport>
4776 where
4777 F: FnOnce() -> bool,
4778 {
4779 let admin = crate::auth::Permission::Admin;
4780 self.require(&admin)?;
4781 let operation_principal = self.principal_snapshot();
4782 control.checkpoint()?;
4783 let destination = prepared.destination_path.clone();
4784 let mut before_publish = Some(before_publish);
4785
4786 let outcome = (|| {
4787 control.checkpoint()?;
4788 let barrier = self.replication_barrier.write();
4789 let ddl = self.ddl_lock.lock();
4790 let security = self.security_coordinator.gate.read();
4791 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4792 let mut handles: Vec<_> = self
4793 .tables
4794 .read()
4795 .iter()
4796 .map(|(id, handle)| (*id, handle.clone()))
4797 .collect();
4798 handles.sort_by_key(|(id, _)| *id);
4799 let table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
4800 let commit = self.commit_lock.lock();
4801 let mut wal = self.shared_wal.lock();
4802 wal.group_sync()?;
4803 let epoch = self.epoch.committed().0;
4804 let boundary_unix_nanos = current_unix_nanos();
4805
4806 let pin_nonce = std::time::SystemTime::now()
4807 .duration_since(std::time::UNIX_EPOCH)
4808 .unwrap_or_default()
4809 .as_nanos();
4810 let file_pin_root = self
4811 .root
4812 .join(META_DIR)
4813 .join("backup-pins")
4814 .join(format!("{}-{pin_nonce}", std::process::id()));
4815 std::fs::create_dir_all(&file_pin_root)?;
4816 let _file_pins = BackupFilePins {
4817 root: file_pin_root.clone(),
4818 };
4819 let mut run_files = Vec::new();
4820 for (index, (table_id, _)) in handles.iter().enumerate() {
4821 if index % 256 == 0 {
4822 control.checkpoint()?;
4823 }
4824 let table = &table_guards[index];
4825 for (run_index, run) in table.run_refs().iter().enumerate() {
4826 if run_index % 256 == 0 {
4827 control.checkpoint()?;
4828 }
4829 let source = table.run_path(run.run_id as u64);
4830 let relative = Path::new(TABLES_DIR)
4831 .join(table_id.to_string())
4832 .join(crate::engine::RUNS_DIR)
4833 .join(format!("r-{}.sr", run.run_id));
4834 let pinned = file_pin_root.join(format!("{table_id}-{}.sr", run.run_id));
4835 if std::fs::hard_link(&source, &pinned).is_err() {
4836 crate::backup::copy_file_synced(&source, &pinned)?;
4837 }
4838 run_files.push(((*table_id, run.run_id), pinned, relative));
4839 }
4840 }
4841 crate::durable_file::sync_directory(&file_pin_root)?;
4842 let run_keys: Vec<_> = run_files.iter().map(|(key, _, _)| *key).collect();
4843 {
4844 let mut pins = self.backup_pins.lock();
4845 for key in &run_keys {
4846 *pins.entry(*key).or_insert(0) += 1;
4847 }
4848 }
4849 let _run_pins = RunPins {
4850 pins: Arc::clone(&self.backup_pins),
4851 runs: run_keys,
4852 };
4853 let deferred: HashSet<_> = run_files
4854 .iter()
4855 .map(|(_, _, relative)| relative.clone())
4856 .collect();
4857 let mut copied = Vec::new();
4858 copy_backup_boundary(
4859 &self.root,
4860 prepared.stage.as_deref().ok_or_else(|| {
4861 MongrelError::Other("backup staging root was already released".into())
4862 })?,
4863 &deferred,
4864 &mut copied,
4865 Some(control),
4866 )?;
4867
4868 drop(wal);
4869 drop(commit);
4870 drop(table_guards);
4871 drop(security);
4872 drop(ddl);
4873 drop(barrier);
4874
4875 if let Some(hook) = self.backup_hook.lock().as_ref() {
4876 hook();
4877 }
4878 for (index, (_, source, relative)) in run_files.into_iter().enumerate() {
4879 if index % 256 == 0 {
4880 control.checkpoint()?;
4881 }
4882 let mut source = crate::durable_file::open_regular_nofollow(&source)?;
4883 prepared
4884 .stage
4885 .as_deref()
4886 .ok_or_else(|| {
4887 MongrelError::Other("backup staging root was already released".into())
4888 })?
4889 .copy_new_from(&relative, &mut source)?;
4890 copied.push(relative);
4891 }
4892
4893 let manifest = crate::backup::BackupManifest::create_controlled_durable(
4894 prepared.stage.as_deref().ok_or_else(|| {
4895 MongrelError::Other("backup staging root was already released".into())
4896 })?,
4897 epoch,
4898 &copied,
4899 control,
4900 )?;
4901 manifest.write_to_durable(prepared.stage.as_deref().ok_or_else(|| {
4902 MongrelError::Other("backup staging root was already released".into())
4903 })?)?;
4904 control.checkpoint()?;
4905 let publish = before_publish.take().ok_or_else(|| {
4906 MongrelError::Other("backup publication callback already consumed".into())
4907 })?;
4908 if !publish() {
4909 return Err(MongrelError::Cancelled);
4910 }
4911 let final_security = self.security_coordinator.gate.read();
4912 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4913 drop(prepared.stage.take().ok_or_else(|| {
4917 MongrelError::Other("backup staging root was already released".into())
4918 })?);
4919 let published = std::cell::Cell::new(false);
4920 if let Err(error) = prepared.parent.rename_directory_new_with_after(
4921 Path::new(&prepared.stage_name),
4922 &prepared.parent,
4923 Path::new(&prepared.destination_name),
4924 || published.set(true),
4925 ) {
4926 if published.get() {
4927 return Err(MongrelError::CommitOutcomeUnknown {
4928 epoch,
4929 message: format!("backup publication was not durable: {error}"),
4930 });
4931 }
4932 return Err(error.into());
4933 }
4934 drop(final_security);
4935 Ok(crate::backup::BackupReport {
4936 destination,
4937 epoch,
4938 boundary_unix_nanos,
4939 files: manifest.files.len(),
4940 bytes: manifest.total_bytes(),
4941 })
4942 })();
4943
4944 if outcome.is_err() {
4945 drop(prepared.stage.take());
4946 let _ = prepared
4947 .parent
4948 .remove_directory_all(Path::new(&prepared.stage_name));
4949 }
4950 outcome
4951 }
4952
4953 pub fn replication_batch_since(
4956 &self,
4957 since_epoch: u64,
4958 ) -> Result<crate::replication::ReplicationBatch> {
4959 use crate::wal::Op;
4960
4961 let admin = crate::auth::Permission::Admin;
4962 self.require(&admin)?;
4963 let operation_principal = self.principal_snapshot();
4964
4965 let mut wal = self.shared_wal.lock();
4966 wal.group_sync()?;
4967 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
4968 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
4969 drop(wal);
4970
4971 let commits: HashMap<u64, u64> = records
4972 .iter()
4973 .filter_map(|record| match &record.op {
4974 Op::TxnCommit { epoch, .. } => Some((record.txn_id, *epoch)),
4975 _ => None,
4976 })
4977 .collect();
4978 let earliest_epoch = commits.values().copied().min();
4979 let current_epoch = commits
4980 .values()
4981 .copied()
4982 .max()
4983 .unwrap_or(0)
4984 .max(self.epoch.committed().0);
4985 let selected: HashSet<u64> = commits
4986 .iter()
4987 .filter_map(|(txn_id, epoch)| (*epoch > since_epoch).then_some(*txn_id))
4988 .collect();
4989 let retention_gap = since_epoch < current_epoch
4990 && since_epoch < crate::replication::replication_wal_floor(&self.root)?;
4991 let spilled = records.iter().any(|record| {
4992 selected.contains(&record.txn_id)
4993 && matches!(
4994 &record.op,
4995 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty()
4996 )
4997 });
4998 let records = records
4999 .into_iter()
5000 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
5001 .filter(|record| selected.contains(&record.txn_id))
5002 .collect();
5003 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
5004 let batch = crate::replication::ReplicationBatch::complete_for_source(
5005 source_id,
5006 since_epoch,
5007 current_epoch,
5008 earliest_epoch,
5009 retention_gap,
5010 spilled,
5011 records,
5012 )?;
5013 if let Some(hook) = self.replication_hook.lock().as_ref() {
5014 hook();
5015 }
5016 let _security = self.security_coordinator.gate.read();
5017 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
5018 Ok(batch)
5019 }
5020
5021 pub fn append_replication_batch(
5025 &self,
5026 batch: &crate::replication::ReplicationBatch,
5027 ) -> Result<u64> {
5028 use crate::wal::Op;
5029
5030 if !self.read_only {
5031 return Err(MongrelError::InvalidArgument(
5032 "replication batches may only target a marked replica".into(),
5033 ));
5034 }
5035 let current = crate::replication::replica_epoch(&self.root)?;
5036 if batch.is_source_bound() {
5037 let source_id = crate::replication::replica_source_id_durable(&self.durable_root)?;
5038 if batch.source_id != source_id {
5039 return Err(MongrelError::Conflict(
5040 "replication batch source does not match follower binding".into(),
5041 ));
5042 }
5043 }
5044 if batch.requires_snapshot {
5045 return Err(MongrelError::Conflict(
5046 "replication snapshot required for this batch".into(),
5047 ));
5048 }
5049 batch.validate_proof()?;
5050 if batch.from_epoch != current {
5051 if batch.from_epoch < current && batch.current_epoch == current {
5052 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
5053 let _wal = self.shared_wal.lock();
5054 let existing: HashSet<(u64, u64)> =
5055 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
5056 .into_iter()
5057 .filter_map(|record| match record.op {
5058 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
5059 _ => None,
5060 })
5061 .collect();
5062 let already_applied = batch.records.iter().all(|record| match &record.op {
5063 Op::TxnCommit { epoch, .. } => existing.contains(&(record.txn_id, *epoch)),
5064 _ => true,
5065 });
5066 if already_applied {
5067 return Ok(current);
5068 }
5069 }
5070 return Err(MongrelError::Conflict(format!(
5071 "replication batch starts at epoch {}, follower is at epoch {current}",
5072 batch.from_epoch
5073 )));
5074 }
5075 if batch.current_epoch < current {
5076 return Err(MongrelError::InvalidArgument(format!(
5077 "replication batch current epoch {} precedes follower epoch {current}",
5078 batch.current_epoch
5079 )));
5080 }
5081 let records = &batch.records;
5082 let mut commits = HashMap::new();
5083 let mut commit_epochs = HashSet::new();
5084 let mut commit_timestamps = HashMap::new();
5085 for record in records {
5086 match &record.op {
5087 Op::TxnCommit { epoch, added_runs } => {
5088 if !added_runs.is_empty() {
5089 return Err(MongrelError::Conflict(
5090 "replication snapshot required for spilled-run transaction".into(),
5091 ));
5092 }
5093 if commits.insert(record.txn_id, *epoch).is_some() {
5094 return Err(MongrelError::InvalidArgument(format!(
5095 "duplicate commit for replication transaction {}",
5096 record.txn_id
5097 )));
5098 }
5099 if *epoch <= current || *epoch > batch.current_epoch {
5100 return Err(MongrelError::InvalidArgument(format!(
5101 "replication commit epoch {epoch} is outside ({current}, {}]",
5102 batch.current_epoch
5103 )));
5104 }
5105 if !commit_epochs.insert(*epoch) {
5106 return Err(MongrelError::InvalidArgument(format!(
5107 "duplicate replication commit epoch {epoch}"
5108 )));
5109 }
5110 }
5111 Op::CommitTimestamp { unix_nanos } => {
5112 commit_timestamps.insert(record.txn_id, *unix_nanos);
5113 }
5114 _ => {}
5115 }
5116 }
5117 for record in records {
5118 if record.txn_id == crate::wal::SYSTEM_TXN_ID
5119 || matches!(&record.op, Op::TxnAbort | Op::Flush { .. })
5120 {
5121 return Err(MongrelError::InvalidArgument(
5122 "replication batch contains a non-committed record".into(),
5123 ));
5124 }
5125 if !commits.contains_key(&record.txn_id) {
5126 return Err(MongrelError::InvalidArgument(format!(
5127 "incomplete replication transaction {}",
5128 record.txn_id
5129 )));
5130 }
5131 }
5132 let target_epoch = commits
5133 .values()
5134 .copied()
5135 .filter(|epoch| *epoch > current)
5136 .max()
5137 .unwrap_or(current);
5138 if target_epoch != batch.current_epoch {
5139 return Err(MongrelError::InvalidArgument(format!(
5140 "replication batch ends at epoch {target_epoch}, expected {}",
5141 batch.current_epoch
5142 )));
5143 }
5144 let mut selected: HashSet<u64> = commits
5145 .iter()
5146 .filter_map(|(txn_id, epoch)| (*epoch > current).then_some(*txn_id))
5147 .collect();
5148 if selected.is_empty() {
5149 return Ok(current);
5150 }
5151 let mut wal = self.shared_wal.lock();
5152 wal.group_sync()?;
5153 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
5154 let existing: HashSet<(u64, u64)> =
5155 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
5156 .into_iter()
5157 .filter_map(|record| match record.op {
5158 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
5159 _ => None,
5160 })
5161 .collect();
5162 selected.retain(|txn_id| {
5163 commits
5164 .get(txn_id)
5165 .is_some_and(|epoch| !existing.contains(&(*txn_id, *epoch)))
5166 });
5167 for record in records {
5168 if !selected.contains(&record.txn_id) {
5169 continue;
5170 }
5171 match &record.op {
5172 Op::TxnCommit { epoch, added_runs } => {
5173 let timestamp = commit_timestamps
5174 .get(&record.txn_id)
5175 .copied()
5176 .unwrap_or_else(current_unix_nanos);
5177 wal.append_commit_at(record.txn_id, Epoch(*epoch), added_runs, timestamp)?;
5178 }
5179 Op::TxnAbort | Op::Flush { .. } | Op::CommitTimestamp { .. } => {}
5180 op => {
5181 wal.append(record.txn_id, 0, op.clone())?;
5182 }
5183 }
5184 }
5185 if !selected.is_empty() {
5186 wal.group_sync()?;
5187 }
5188 drop(wal);
5189
5190 let mut recovered_catalog = self.catalog.read().clone();
5194 if let Err(error) = recover_ddl_from_wal(
5195 &self.root,
5196 Some(&self.durable_root),
5197 &mut recovered_catalog,
5198 self.meta_dek.as_ref(),
5199 wal_dek.as_ref(),
5200 true,
5201 None,
5202 ) {
5203 return Err(MongrelError::DurableCommit {
5204 epoch: target_epoch,
5205 message: format!(
5206 "replication WAL is durable but catalog checkpoint failed: {error}"
5207 ),
5208 });
5209 }
5210 let _security = self.security_coordinator.gate.write();
5211 let old_security_version = self.catalog.read().security_version;
5212 let security_changed = old_security_version != recovered_catalog.security_version
5213 || self.catalog.read().require_auth != recovered_catalog.require_auth;
5214 let require_auth = recovered_catalog.require_auth;
5215 let principal = if security_changed {
5216 None
5217 } else {
5218 self.principal.read().as_ref().and_then(|principal| {
5219 Self::resolve_bound_principal_from_catalog(&recovered_catalog, principal)
5220 })
5221 };
5222 if require_auth {
5223 self.auth_state.set_require_auth(true);
5224 }
5225 self.auth_state.set_principal(principal.clone());
5226 *self.principal.write() = principal;
5227 let security_version = recovered_catalog.security_version;
5228 *self.catalog.write() = recovered_catalog;
5229 self.security_coordinator
5230 .version
5231 .store(security_version, Ordering::Release);
5232 if !require_auth {
5233 self.auth_state.set_require_auth(false);
5234 }
5235 if let Err(error) =
5236 crate::replication::reconcile_replica_epoch_durable(&self.durable_root, target_epoch)
5237 {
5238 return Err(MongrelError::DurableCommit {
5239 epoch: target_epoch,
5240 message: format!(
5241 "replication WAL and catalog are durable but follower watermark failed: {error}"
5242 ),
5243 });
5244 }
5245 Ok(target_epoch)
5246 }
5247
5248 pub fn table_id(&self, name: &str) -> Result<u64> {
5251 let cat = self.catalog.read();
5252 cat.live(name)
5253 .map(|e| e.table_id)
5254 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
5255 }
5256
5257 pub fn table_identity(&self, name: &str) -> Result<(u64, u64)> {
5262 let catalog = self.catalog.read();
5263 catalog
5264 .live(name)
5265 .map(|entry| (entry.table_id, entry.schema.schema_id))
5266 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
5267 }
5268
5269 pub(crate) fn building_table_id(&self, name: &str) -> Result<u64> {
5270 self.catalog
5271 .read()
5272 .building(name)
5273 .map(|entry| entry.table_id)
5274 .ok_or_else(|| MongrelError::NotFound(format!("building table {name:?} not found")))
5275 }
5276
5277 pub fn procedures(&self) -> Vec<StoredProcedure> {
5278 self.catalog
5279 .read()
5280 .procedures
5281 .iter()
5282 .map(|p| p.procedure.clone())
5283 .collect()
5284 }
5285
5286 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
5287 self.catalog
5288 .read()
5289 .procedures
5290 .iter()
5291 .find(|p| p.procedure.name == name)
5292 .map(|p| p.procedure.clone())
5293 }
5294
5295 pub fn create_procedure(&self, procedure: StoredProcedure) -> Result<StoredProcedure> {
5296 self.create_procedure_inner(procedure, None)
5297 }
5298
5299 pub fn create_procedure_controlled<F>(
5300 &self,
5301 procedure: StoredProcedure,
5302 mut before_publish: F,
5303 ) -> Result<StoredProcedure>
5304 where
5305 F: FnMut() -> Result<()>,
5306 {
5307 self.create_procedure_inner(procedure, Some(&mut before_publish))
5308 }
5309
5310 fn create_procedure_inner(
5311 &self,
5312 mut procedure: StoredProcedure,
5313 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5314 ) -> Result<StoredProcedure> {
5315 self.require(&crate::auth::Permission::Ddl)?;
5316 let _g = self.ddl_lock.lock();
5317 let _security_write = self.security_write()?;
5318 self.require(&crate::auth::Permission::Ddl)?;
5319 procedure.validate()?;
5320 self.validate_procedure_references(&procedure)?;
5321 {
5322 let cat = self.catalog.read();
5323 if cat
5324 .procedures
5325 .iter()
5326 .any(|p| p.procedure.name == procedure.name)
5327 {
5328 return Err(MongrelError::InvalidArgument(format!(
5329 "procedure {:?} already exists",
5330 procedure.name
5331 )));
5332 }
5333 }
5334 let commit_lock = Arc::clone(&self.commit_lock);
5335 let _c = commit_lock.lock();
5336 let epoch = self.epoch.bump_assigned();
5337 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5338 procedure.created_epoch = epoch.0;
5339 procedure.updated_epoch = epoch.0;
5340 let mut next_catalog = self.catalog.read().clone();
5341 next_catalog
5342 .procedures
5343 .push(ProcedureEntry::from(procedure.clone()));
5344 next_catalog.db_epoch = epoch.0;
5345 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5346 Ok(procedure)
5347 }
5348
5349 pub fn create_or_replace_procedure(
5350 &self,
5351 procedure: StoredProcedure,
5352 ) -> Result<StoredProcedure> {
5353 self.create_or_replace_procedure_inner(procedure, None)
5354 }
5355
5356 pub fn create_or_replace_procedure_controlled<F>(
5357 &self,
5358 procedure: StoredProcedure,
5359 mut before_publish: F,
5360 ) -> Result<StoredProcedure>
5361 where
5362 F: FnMut() -> Result<()>,
5363 {
5364 self.create_or_replace_procedure_inner(procedure, Some(&mut before_publish))
5365 }
5366
5367 fn create_or_replace_procedure_inner(
5368 &self,
5369 procedure: StoredProcedure,
5370 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5371 ) -> Result<StoredProcedure> {
5372 self.require(&crate::auth::Permission::Ddl)?;
5373 let _g = self.ddl_lock.lock();
5374 let _security_write = self.security_write()?;
5375 self.require(&crate::auth::Permission::Ddl)?;
5376 procedure.validate()?;
5377 self.validate_procedure_references(&procedure)?;
5378 let commit_lock = Arc::clone(&self.commit_lock);
5379 let _c = commit_lock.lock();
5380 let epoch = self.epoch.bump_assigned();
5381 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5382 let mut next_catalog = self.catalog.read().clone();
5383 let replaced = {
5384 let next = match next_catalog
5385 .procedures
5386 .iter()
5387 .position(|p| p.procedure.name == procedure.name)
5388 {
5389 Some(idx) => {
5390 let next = next_catalog.procedures[idx]
5391 .procedure
5392 .replaced(procedure.clone(), epoch.0)?;
5393 next_catalog.procedures[idx] = ProcedureEntry::from(next.clone());
5394 next
5395 }
5396 None => {
5397 let mut next = procedure;
5398 next.created_epoch = epoch.0;
5399 next.updated_epoch = epoch.0;
5400 next_catalog
5401 .procedures
5402 .push(ProcedureEntry::from(next.clone()));
5403 next
5404 }
5405 };
5406 next_catalog.db_epoch = epoch.0;
5407 next
5408 };
5409 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5410 Ok(replaced)
5411 }
5412
5413 pub fn drop_procedure(&self, name: &str) -> Result<()> {
5414 self.drop_procedure_with_epoch(name).map(|_| ())
5415 }
5416
5417 pub fn drop_procedure_with_epoch(&self, name: &str) -> Result<Epoch> {
5418 self.drop_procedure_with_epoch_inner(name, None)
5419 }
5420
5421 pub fn drop_procedure_with_epoch_controlled<F>(
5422 &self,
5423 name: &str,
5424 mut before_publish: F,
5425 ) -> Result<Epoch>
5426 where
5427 F: FnMut() -> Result<()>,
5428 {
5429 self.drop_procedure_with_epoch_inner(name, Some(&mut before_publish))
5430 }
5431
5432 fn drop_procedure_with_epoch_inner(
5433 &self,
5434 name: &str,
5435 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5436 ) -> Result<Epoch> {
5437 self.require(&crate::auth::Permission::Ddl)?;
5438 let _g = self.ddl_lock.lock();
5439 let _security_write = self.security_write()?;
5440 self.require(&crate::auth::Permission::Ddl)?;
5441 let commit_lock = Arc::clone(&self.commit_lock);
5442 let _c = commit_lock.lock();
5443 let epoch = self.epoch.bump_assigned();
5444 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5445 let mut next_catalog = self.catalog.read().clone();
5446 let before = next_catalog.procedures.len();
5447 next_catalog.procedures.retain(|p| p.procedure.name != name);
5448 if next_catalog.procedures.len() == before {
5449 return Err(MongrelError::NotFound(format!(
5450 "procedure {name:?} not found"
5451 )));
5452 }
5453 next_catalog.db_epoch = epoch.0;
5454 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5455 Ok(epoch)
5456 }
5457
5458 pub fn users(&self) -> Vec<crate::auth::UserEntry> {
5463 self.catalog.read().users.clone()
5464 }
5465
5466 pub fn user_identity(&self, username: &str) -> Option<(u64, u64)> {
5469 self.catalog
5470 .read()
5471 .users
5472 .iter()
5473 .find(|user| user.username == username)
5474 .map(|user| (user.id, user.created_epoch))
5475 }
5476
5477 pub fn security_version(&self) -> u64 {
5480 self.catalog.read().security_version
5481 }
5482
5483 pub fn roles(&self) -> Vec<crate::auth::RoleEntry> {
5485 self.catalog.read().roles.clone()
5486 }
5487
5488 pub fn create_user(&self, username: &str, password: &str) -> Result<crate::auth::UserEntry> {
5490 self.require(&crate::auth::Permission::Admin)?;
5491 let hash = crate::auth::hash_password(password).map_err(MongrelError::Other)?;
5492 self.create_user_with_password_hash(username, hash)
5493 }
5494
5495 pub fn create_user_with_password_hash(
5497 &self,
5498 username: &str,
5499 hash: String,
5500 ) -> Result<crate::auth::UserEntry> {
5501 self.create_user_with_password_hash_inner(username, hash, None)
5502 }
5503
5504 pub fn create_user_with_password_hash_controlled<F>(
5505 &self,
5506 username: &str,
5507 hash: String,
5508 mut before_publish: F,
5509 ) -> Result<crate::auth::UserEntry>
5510 where
5511 F: FnMut() -> Result<()>,
5512 {
5513 self.create_user_with_password_hash_inner(username, hash, Some(&mut before_publish))
5514 }
5515
5516 fn create_user_with_password_hash_inner(
5517 &self,
5518 username: &str,
5519 hash: String,
5520 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5521 ) -> Result<crate::auth::UserEntry> {
5522 self.require(&crate::auth::Permission::Admin)?;
5523 let _ddl = self.ddl_lock.lock();
5524 let _security_write = self.security_write()?;
5525 self.require(&crate::auth::Permission::Admin)?;
5526 let _commit = self.commit_lock.lock();
5527 let epoch = self.epoch.bump_assigned();
5528 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5529 let mut next_catalog = self.catalog.read().clone();
5530 if next_catalog.users.iter().any(|u| u.username == username) {
5531 return Err(MongrelError::InvalidArgument(format!(
5532 "user {username:?} already exists"
5533 )));
5534 }
5535 next_catalog.next_user_id = next_catalog.next_user_id.max(1);
5536 let id = next_catalog.next_user_id;
5537 next_catalog.next_user_id = id
5538 .checked_add(1)
5539 .ok_or_else(|| MongrelError::Full("user-id namespace exhausted".into()))?;
5540 let entry = crate::auth::UserEntry {
5541 id,
5542 username: username.into(),
5543 password_hash: hash,
5544 roles: Vec::new(),
5545 is_admin: false,
5546 created_epoch: epoch.0,
5547 };
5548 next_catalog.users.push(entry.clone());
5549 advance_security_version(&mut next_catalog)?;
5550 next_catalog.db_epoch = epoch.0;
5551 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5552 Ok(entry)
5553 }
5554
5555 pub fn drop_user(&self, username: &str) -> Result<()> {
5557 self.drop_user_with_epoch(username).map(|_| ())
5558 }
5559
5560 pub fn drop_user_with_epoch(&self, username: &str) -> Result<Epoch> {
5561 self.drop_user_with_epoch_inner(username, None)
5562 }
5563
5564 pub fn drop_user_with_epoch_controlled<F>(
5565 &self,
5566 username: &str,
5567 mut before_publish: F,
5568 ) -> Result<Epoch>
5569 where
5570 F: FnMut() -> Result<()>,
5571 {
5572 self.drop_user_with_epoch_inner(username, Some(&mut before_publish))
5573 }
5574
5575 fn drop_user_with_epoch_inner(
5576 &self,
5577 username: &str,
5578 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5579 ) -> Result<Epoch> {
5580 self.require(&crate::auth::Permission::Admin)?;
5581 let _ddl = self.ddl_lock.lock();
5582 let _security_write = self.security_write()?;
5583 self.require(&crate::auth::Permission::Admin)?;
5584 let _commit = self.commit_lock.lock();
5585 let epoch = self.epoch.bump_assigned();
5586 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5587 let mut next_catalog = self.catalog.read().clone();
5588 let before = next_catalog.users.len();
5589 next_catalog.users.retain(|u| u.username != username);
5590 if next_catalog.users.len() == before {
5591 return Err(MongrelError::NotFound(format!(
5592 "user {username:?} not found"
5593 )));
5594 }
5595 advance_security_version(&mut next_catalog)?;
5596 next_catalog.db_epoch = epoch.0;
5597 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5598 Ok(epoch)
5599 }
5600
5601 pub fn alter_user_password(&self, username: &str, new_password: &str) -> Result<()> {
5603 self.alter_user_password_with_epoch(username, new_password)
5604 .map(|_| ())
5605 }
5606
5607 pub fn alter_user_password_with_epoch(
5608 &self,
5609 username: &str,
5610 new_password: &str,
5611 ) -> Result<Epoch> {
5612 self.require(&crate::auth::Permission::Admin)?;
5613 let hash = crate::auth::hash_password(new_password).map_err(MongrelError::Other)?;
5614 self.alter_user_password_hash_with_epoch(username, hash)
5615 }
5616
5617 pub fn alter_user_password_hash_with_epoch(
5618 &self,
5619 username: &str,
5620 hash: String,
5621 ) -> Result<Epoch> {
5622 self.alter_user_password_hash_with_epoch_inner(username, hash, None)
5623 }
5624
5625 pub fn alter_user_password_hash_with_epoch_controlled<F>(
5626 &self,
5627 username: &str,
5628 hash: String,
5629 mut before_publish: F,
5630 ) -> Result<Epoch>
5631 where
5632 F: FnMut() -> Result<()>,
5633 {
5634 self.alter_user_password_hash_with_epoch_inner(username, hash, Some(&mut before_publish))
5635 }
5636
5637 fn alter_user_password_hash_with_epoch_inner(
5638 &self,
5639 username: &str,
5640 hash: String,
5641 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5642 ) -> Result<Epoch> {
5643 self.require(&crate::auth::Permission::Admin)?;
5644 let _ddl = self.ddl_lock.lock();
5645 let _security_write = self.security_write()?;
5646 self.require(&crate::auth::Permission::Admin)?;
5647 let _commit = self.commit_lock.lock();
5648 let epoch = self.epoch.bump_assigned();
5649 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
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 user.password_hash = hash;
5657 advance_security_version(&mut next_catalog)?;
5658 next_catalog.db_epoch = epoch.0;
5659 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5660 Ok(epoch)
5661 }
5662
5663 pub fn verify_user(
5666 &self,
5667 username: &str,
5668 password: &str,
5669 ) -> Result<Option<crate::auth::UserEntry>> {
5670 let cat = self.catalog.read();
5671 let Some(user) = cat.users.iter().find(|u| u.username == username) else {
5672 return Ok(None);
5673 };
5674 if user.password_hash.is_empty() {
5675 return Ok(None);
5676 }
5677 let ok = crate::auth::verify_password(password, &user.password_hash)
5678 .map_err(MongrelError::Other)?;
5679 if ok {
5680 Ok(Some(user.clone()))
5681 } else {
5682 Ok(None)
5683 }
5684 }
5685
5686 pub fn authenticate_principal(
5690 &self,
5691 username: &str,
5692 password: &str,
5693 ) -> Result<Option<crate::auth::Principal>> {
5694 self.authenticate_principal_inner(username, password, || {})
5695 }
5696
5697 fn authenticate_principal_inner<F>(
5698 &self,
5699 username: &str,
5700 password: &str,
5701 after_verify: F,
5702 ) -> Result<Option<crate::auth::Principal>>
5703 where
5704 F: FnOnce(),
5705 {
5706 let catalog = self.catalog.read();
5707 let Some(user) = catalog.users.iter().find(|user| user.username == username) else {
5708 return Ok(None);
5709 };
5710 if user.password_hash.is_empty()
5711 || !crate::auth::verify_password(password, &user.password_hash)
5712 .map_err(MongrelError::Other)?
5713 {
5714 return Ok(None);
5715 }
5716 after_verify();
5717 Ok(Self::resolve_user_principal_from_catalog(&catalog, user))
5718 }
5719
5720 pub fn set_user_admin(&self, username: &str, is_admin: bool) -> Result<()> {
5722 self.set_user_admin_with_epoch(username, is_admin)
5723 .map(|_| ())
5724 }
5725
5726 pub fn set_user_admin_with_epoch(
5727 &self,
5728 username: &str,
5729 is_admin: bool,
5730 ) -> Result<Option<Epoch>> {
5731 self.set_user_admin_with_epoch_inner(username, is_admin, None)
5732 }
5733
5734 pub fn set_user_admin_with_epoch_controlled<F>(
5735 &self,
5736 username: &str,
5737 is_admin: bool,
5738 mut before_publish: F,
5739 ) -> Result<Option<Epoch>>
5740 where
5741 F: FnMut() -> Result<()>,
5742 {
5743 self.set_user_admin_with_epoch_inner(username, is_admin, Some(&mut before_publish))
5744 }
5745
5746 fn set_user_admin_with_epoch_inner(
5747 &self,
5748 username: &str,
5749 is_admin: bool,
5750 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5751 ) -> Result<Option<Epoch>> {
5752 self.require(&crate::auth::Permission::Admin)?;
5753 let _ddl = self.ddl_lock.lock();
5754 let _security_write = self.security_write()?;
5755 self.require(&crate::auth::Permission::Admin)?;
5756 let _commit = self.commit_lock.lock();
5757 let mut next_catalog = self.catalog.read().clone();
5758 let user = next_catalog
5759 .users
5760 .iter_mut()
5761 .find(|u| u.username == username)
5762 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5763 if user.is_admin == is_admin {
5764 return Ok(None);
5765 }
5766 user.is_admin = is_admin;
5767 let epoch = self.epoch.bump_assigned();
5768 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5769 advance_security_version(&mut next_catalog)?;
5770 next_catalog.db_epoch = epoch.0;
5771 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5772 Ok(Some(epoch))
5773 }
5774
5775 pub fn create_role(&self, name: &str) -> Result<crate::auth::RoleEntry> {
5777 self.create_role_inner(name, None)
5778 }
5779
5780 pub fn create_role_controlled<F>(
5781 &self,
5782 name: &str,
5783 mut before_publish: F,
5784 ) -> Result<crate::auth::RoleEntry>
5785 where
5786 F: FnMut() -> Result<()>,
5787 {
5788 self.create_role_inner(name, Some(&mut before_publish))
5789 }
5790
5791 fn create_role_inner(
5792 &self,
5793 name: &str,
5794 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5795 ) -> Result<crate::auth::RoleEntry> {
5796 self.require(&crate::auth::Permission::Admin)?;
5797 let _ddl = self.ddl_lock.lock();
5798 let _security_write = self.security_write()?;
5799 self.require(&crate::auth::Permission::Admin)?;
5800 let _commit = self.commit_lock.lock();
5801 let epoch = self.epoch.bump_assigned();
5802 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5803 let mut next_catalog = self.catalog.read().clone();
5804 if next_catalog.roles.iter().any(|r| r.name == name) {
5805 return Err(MongrelError::InvalidArgument(format!(
5806 "role {name:?} already exists"
5807 )));
5808 }
5809 let entry = crate::auth::RoleEntry {
5810 name: name.into(),
5811 permissions: Vec::new(),
5812 created_epoch: epoch.0,
5813 };
5814 next_catalog.roles.push(entry.clone());
5815 advance_security_version(&mut next_catalog)?;
5816 next_catalog.db_epoch = epoch.0;
5817 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5818 Ok(entry)
5819 }
5820
5821 pub fn drop_role(&self, name: &str) -> Result<()> {
5823 self.drop_role_with_epoch(name).map(|_| ())
5824 }
5825
5826 pub fn drop_role_with_epoch(&self, name: &str) -> Result<Epoch> {
5827 self.drop_role_with_epoch_inner(name, None)
5828 }
5829
5830 pub fn drop_role_with_epoch_controlled<F>(
5831 &self,
5832 name: &str,
5833 mut before_publish: F,
5834 ) -> Result<Epoch>
5835 where
5836 F: FnMut() -> Result<()>,
5837 {
5838 self.drop_role_with_epoch_inner(name, Some(&mut before_publish))
5839 }
5840
5841 fn drop_role_with_epoch_inner(
5842 &self,
5843 name: &str,
5844 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5845 ) -> Result<Epoch> {
5846 self.require(&crate::auth::Permission::Admin)?;
5847 let _ddl = self.ddl_lock.lock();
5848 let _security_write = self.security_write()?;
5849 self.require(&crate::auth::Permission::Admin)?;
5850 let _commit = self.commit_lock.lock();
5851 let epoch = self.epoch.bump_assigned();
5852 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5853 let mut next_catalog = self.catalog.read().clone();
5854 let before = next_catalog.roles.len();
5855 next_catalog.roles.retain(|r| r.name != name);
5856 if next_catalog.roles.len() == before {
5857 return Err(MongrelError::NotFound(format!("role {name:?} not found")));
5858 }
5859 for user in &mut next_catalog.users {
5860 user.roles.retain(|r| r != name);
5861 }
5862 advance_security_version(&mut next_catalog)?;
5863 next_catalog.db_epoch = epoch.0;
5864 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5865 Ok(epoch)
5866 }
5867
5868 pub fn grant_role(&self, username: &str, role_name: &str) -> Result<()> {
5870 self.grant_role_with_epoch(username, role_name).map(|_| ())
5871 }
5872
5873 pub fn grant_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
5874 self.grant_role_with_epoch_inner(username, role_name, None)
5875 }
5876
5877 pub fn grant_role_with_epoch_controlled<F>(
5878 &self,
5879 username: &str,
5880 role_name: &str,
5881 mut before_publish: F,
5882 ) -> Result<Option<Epoch>>
5883 where
5884 F: FnMut() -> Result<()>,
5885 {
5886 self.grant_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
5887 }
5888
5889 fn grant_role_with_epoch_inner(
5890 &self,
5891 username: &str,
5892 role_name: &str,
5893 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5894 ) -> Result<Option<Epoch>> {
5895 self.require(&crate::auth::Permission::Admin)?;
5896 let _ddl = self.ddl_lock.lock();
5897 let _security_write = self.security_write()?;
5898 self.require(&crate::auth::Permission::Admin)?;
5899 let _commit = self.commit_lock.lock();
5900 let mut next_catalog = self.catalog.read().clone();
5901 if !next_catalog.roles.iter().any(|r| r.name == role_name) {
5902 return Err(MongrelError::NotFound(format!(
5903 "role {role_name:?} not found"
5904 )));
5905 }
5906 let user = next_catalog
5907 .users
5908 .iter_mut()
5909 .find(|u| u.username == username)
5910 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5911 if user.roles.iter().any(|role| role == role_name) {
5912 return Ok(None);
5913 }
5914 user.roles.push(role_name.into());
5915 let epoch = self.epoch.bump_assigned();
5916 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5917 advance_security_version(&mut next_catalog)?;
5918 next_catalog.db_epoch = epoch.0;
5919 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5920 Ok(Some(epoch))
5921 }
5922
5923 pub fn revoke_role(&self, username: &str, role_name: &str) -> Result<()> {
5925 self.revoke_role_with_epoch(username, role_name).map(|_| ())
5926 }
5927
5928 pub fn revoke_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
5929 self.revoke_role_with_epoch_inner(username, role_name, None)
5930 }
5931
5932 pub fn revoke_role_with_epoch_controlled<F>(
5933 &self,
5934 username: &str,
5935 role_name: &str,
5936 mut before_publish: F,
5937 ) -> Result<Option<Epoch>>
5938 where
5939 F: FnMut() -> Result<()>,
5940 {
5941 self.revoke_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
5942 }
5943
5944 fn revoke_role_with_epoch_inner(
5945 &self,
5946 username: &str,
5947 role_name: &str,
5948 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5949 ) -> Result<Option<Epoch>> {
5950 self.require(&crate::auth::Permission::Admin)?;
5951 let _ddl = self.ddl_lock.lock();
5952 let _security_write = self.security_write()?;
5953 self.require(&crate::auth::Permission::Admin)?;
5954 let _commit = self.commit_lock.lock();
5955 let mut next_catalog = self.catalog.read().clone();
5956 let user = next_catalog
5957 .users
5958 .iter_mut()
5959 .find(|u| u.username == username)
5960 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5961 let before = user.roles.len();
5962 user.roles.retain(|r| r != role_name);
5963 if user.roles.len() == before {
5964 return Ok(None);
5965 }
5966 let epoch = self.epoch.bump_assigned();
5967 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5968 advance_security_version(&mut next_catalog)?;
5969 next_catalog.db_epoch = epoch.0;
5970 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5971 Ok(Some(epoch))
5972 }
5973
5974 pub fn grant_permission(
5976 &self,
5977 role_name: &str,
5978 permission: crate::auth::Permission,
5979 ) -> Result<()> {
5980 self.grant_permission_with_epoch(role_name, permission)
5981 .map(|_| ())
5982 }
5983
5984 pub fn grant_permission_with_epoch(
5985 &self,
5986 role_name: &str,
5987 permission: crate::auth::Permission,
5988 ) -> Result<Option<Epoch>> {
5989 self.grant_permission_with_epoch_inner(role_name, permission, None)
5990 }
5991
5992 pub fn grant_permission_with_epoch_controlled<F>(
5993 &self,
5994 role_name: &str,
5995 permission: crate::auth::Permission,
5996 mut before_publish: F,
5997 ) -> Result<Option<Epoch>>
5998 where
5999 F: FnMut() -> Result<()>,
6000 {
6001 self.grant_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
6002 }
6003
6004 fn grant_permission_with_epoch_inner(
6005 &self,
6006 role_name: &str,
6007 permission: crate::auth::Permission,
6008 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6009 ) -> Result<Option<Epoch>> {
6010 self.require(&crate::auth::Permission::Admin)?;
6011 let _ddl = self.ddl_lock.lock();
6012 let _security_write = self.security_write()?;
6013 self.require(&crate::auth::Permission::Admin)?;
6014 let _commit = self.commit_lock.lock();
6015 let mut next_catalog = self.catalog.read().clone();
6016 let role = next_catalog
6017 .roles
6018 .iter_mut()
6019 .find(|r| r.name == role_name)
6020 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
6021 let before = role.permissions.clone();
6022 merge_permission(&mut role.permissions, permission);
6023 if role.permissions == before {
6024 return Ok(None);
6025 }
6026 let epoch = self.epoch.bump_assigned();
6027 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6028 advance_security_version(&mut next_catalog)?;
6029 next_catalog.db_epoch = epoch.0;
6030 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6031 Ok(Some(epoch))
6032 }
6033
6034 pub fn revoke_permission(
6036 &self,
6037 role_name: &str,
6038 permission: crate::auth::Permission,
6039 ) -> Result<()> {
6040 self.revoke_permission_with_epoch(role_name, permission)
6041 .map(|_| ())
6042 }
6043
6044 pub fn revoke_permission_with_epoch(
6045 &self,
6046 role_name: &str,
6047 permission: crate::auth::Permission,
6048 ) -> Result<Option<Epoch>> {
6049 self.revoke_permission_with_epoch_inner(role_name, permission, None)
6050 }
6051
6052 pub fn revoke_permission_with_epoch_controlled<F>(
6053 &self,
6054 role_name: &str,
6055 permission: crate::auth::Permission,
6056 mut before_publish: F,
6057 ) -> Result<Option<Epoch>>
6058 where
6059 F: FnMut() -> Result<()>,
6060 {
6061 self.revoke_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
6062 }
6063
6064 fn revoke_permission_with_epoch_inner(
6065 &self,
6066 role_name: &str,
6067 permission: crate::auth::Permission,
6068 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6069 ) -> Result<Option<Epoch>> {
6070 self.require(&crate::auth::Permission::Admin)?;
6071 let _ddl = self.ddl_lock.lock();
6072 let _security_write = self.security_write()?;
6073 self.require(&crate::auth::Permission::Admin)?;
6074 let _commit = self.commit_lock.lock();
6075 let mut next_catalog = self.catalog.read().clone();
6076 let role = next_catalog
6077 .roles
6078 .iter_mut()
6079 .find(|r| r.name == role_name)
6080 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
6081 let before = role.permissions.clone();
6082 revoke_permission_from(&mut role.permissions, &permission);
6083 if role.permissions == before {
6084 return Ok(None);
6085 }
6086 let epoch = self.epoch.bump_assigned();
6087 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6088 advance_security_version(&mut next_catalog)?;
6089 next_catalog.db_epoch = epoch.0;
6090 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6091 Ok(Some(epoch))
6092 }
6093
6094 pub fn resolve_principal(&self, username: &str) -> Option<crate::auth::Principal> {
6097 let cat = self.catalog.read();
6098 Self::resolve_principal_from_catalog(&cat, username)
6099 }
6100
6101 pub fn resolve_current_principal(
6104 &self,
6105 principal: &crate::auth::Principal,
6106 ) -> Option<crate::auth::Principal> {
6107 Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
6108 }
6109
6110 fn resolve_principal_from_catalog(
6115 cat: &Catalog,
6116 username: &str,
6117 ) -> Option<crate::auth::Principal> {
6118 let user = cat.users.iter().find(|u| u.username == username)?;
6119 Self::resolve_user_principal_from_catalog(cat, user)
6120 }
6121
6122 fn resolve_bound_principal_from_catalog(
6123 cat: &Catalog,
6124 principal: &crate::auth::Principal,
6125 ) -> Option<crate::auth::Principal> {
6126 let user = cat.users.iter().find(|user| {
6127 user.id == principal.user_id
6128 && user.created_epoch == principal.created_epoch
6129 && user.username == principal.username
6130 })?;
6131 Self::resolve_user_principal_from_catalog(cat, user)
6132 }
6133
6134 fn resolve_user_principal_from_catalog(
6135 cat: &Catalog,
6136 user: &crate::auth::UserEntry,
6137 ) -> Option<crate::auth::Principal> {
6138 let mut permissions = Vec::new();
6139 for role_name in &user.roles {
6140 if let Some(role) = cat.roles.iter().find(|r| &r.name == role_name) {
6141 permissions.extend(role.permissions.iter().cloned());
6142 }
6143 }
6144 Some(crate::auth::Principal {
6145 user_id: user.id,
6146 created_epoch: user.created_epoch,
6147 username: user.username.clone(),
6148 is_admin: user.is_admin,
6149 roles: user.roles.clone(),
6150 permissions,
6151 })
6152 }
6153
6154 pub fn check_permission(&self, username: &str, permission: &crate::auth::Permission) -> bool {
6156 match self.resolve_principal(username) {
6157 Some(p) => p.has_permission(permission),
6158 None => false,
6159 }
6160 }
6161
6162 pub fn require_auth_enabled(&self) -> bool {
6166 self.catalog.read().require_auth
6167 }
6168
6169 pub fn principal(&self) -> Option<crate::auth::Principal> {
6173 self.principal.read().clone()
6174 }
6175
6176 fn table_auth_checker(&self) -> Option<Arc<dyn crate::auth_state::TableAuthChecker>> {
6182 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
6183 self.auth_state.clone(),
6184 )))
6185 }
6186
6187 pub fn refresh_principal(&self) -> Result<()> {
6201 let previous = match self.principal.read().clone() {
6202 Some(principal) => principal,
6203 None => return Ok(()),
6204 };
6205 let observed_version = self.security_coordinator.version.load(Ordering::Acquire);
6206 self.refresh_security_catalog_if_stale(observed_version)?;
6207 let cat = self.catalog.read();
6208 match Self::resolve_bound_principal_from_catalog(&cat, &previous) {
6209 Some(p) => {
6210 *self.principal.write() = Some(p.clone());
6211 self.auth_state.set_principal(Some(p));
6215 Ok(())
6216 }
6217 None => Err(MongrelError::InvalidCredentials {
6218 username: previous.username,
6219 }),
6220 }
6221 }
6222
6223 pub fn security_catalog_disk_read_count(&self) -> u64 {
6226 self.security_catalog_disk_reads.load(Ordering::Relaxed)
6227 }
6228
6229 pub fn enable_auth(&self, admin_username: &str, admin_password: &str) -> Result<()> {
6241 let password_hash =
6242 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
6243 let _ddl = self.ddl_lock.lock();
6244 let _security_write = self.security_write()?;
6245 let _commit = self.commit_lock.lock();
6246 let epoch = self.epoch.bump_assigned();
6247 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6248 let mut next_catalog = self.catalog.read().clone();
6249 if next_catalog.require_auth {
6250 return Err(MongrelError::InvalidArgument(
6251 "database already has require_auth enabled".into(),
6252 ));
6253 }
6254 if next_catalog
6255 .users
6256 .iter()
6257 .any(|u| u.username == admin_username)
6258 {
6259 return Err(MongrelError::InvalidArgument(format!(
6260 "user {admin_username:?} already exists"
6261 )));
6262 }
6263 next_catalog.next_user_id = next_catalog.next_user_id.max(1);
6264 let id = next_catalog.next_user_id;
6265 next_catalog.next_user_id = id
6266 .checked_add(1)
6267 .ok_or_else(|| MongrelError::Full("user-id namespace exhausted".into()))?;
6268 next_catalog.users.push(crate::auth::UserEntry {
6269 id,
6270 username: admin_username.to_string(),
6271 password_hash,
6272 roles: Vec::new(),
6273 is_admin: true,
6274 created_epoch: epoch.0,
6275 });
6276 next_catalog.require_auth = true;
6277 advance_security_version(&mut next_catalog)?;
6278 next_catalog.db_epoch = epoch.0;
6279 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
6280 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
6284 let principal = crate::auth::Principal {
6285 user_id: id,
6286 created_epoch: epoch.0,
6287 username: admin_username.to_string(),
6288 is_admin: true,
6289 roles: Vec::new(),
6290 permissions: Vec::new(),
6291 };
6292 *self.principal.write() = Some(principal.clone());
6293 self.auth_state.set_principal(Some(principal));
6294 }
6295 publish
6296 }
6297
6298 pub fn disable_auth(&self) -> Result<()> {
6315 let _ddl = self.ddl_lock.lock();
6316 let _security_write = self.security_write()?;
6317 let _commit = self.commit_lock.lock();
6318 let epoch = self.epoch.bump_assigned();
6319 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6320 let mut next_catalog = self.catalog.read().clone();
6321 if !next_catalog.require_auth {
6322 return Err(MongrelError::InvalidArgument(
6323 "database does not have require_auth enabled".into(),
6324 ));
6325 }
6326 next_catalog.require_auth = false;
6327 advance_security_version(&mut next_catalog)?;
6328 next_catalog.db_epoch = epoch.0;
6329 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
6330 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
6332 *self.principal.write() = None;
6333 }
6334 publish
6335 }
6336
6337 pub fn require(&self, perm: &crate::auth::Permission) -> Result<()> {
6344 self.ensure_owner_process()?;
6345 if self.read_only && !matches!(perm, crate::auth::Permission::Select { .. }) {
6346 return Err(MongrelError::ReadOnlyReplica);
6347 }
6348 if self.principal.read().is_some() {
6349 self.refresh_principal().map_err(|error| match error {
6350 MongrelError::InvalidCredentials { .. } => MongrelError::AuthRequired,
6351 error => error,
6352 })?;
6353 }
6354 if !self.catalog.read().require_auth {
6355 return Ok(());
6356 }
6357 let guard = self.principal.read();
6358 let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
6359 if p.has_permission(perm) {
6360 Ok(())
6361 } else {
6362 Err(MongrelError::PermissionDenied {
6363 required: perm.clone(),
6364 principal: p.username.clone(),
6365 })
6366 }
6367 }
6368
6369 pub fn require_table(
6374 &self,
6375 table: &str,
6376 perm: crate::auth_state::RequiredPermission,
6377 ) -> Result<()> {
6378 self.require(&perm.into_permission(table))
6379 }
6380
6381 pub fn triggers(&self) -> Vec<StoredTrigger> {
6382 self.catalog
6383 .read()
6384 .triggers
6385 .iter()
6386 .map(|t| t.trigger.clone())
6387 .collect()
6388 }
6389
6390 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
6391 self.catalog
6392 .read()
6393 .triggers
6394 .iter()
6395 .find(|t| t.trigger.name == name)
6396 .map(|t| t.trigger.clone())
6397 }
6398
6399 pub fn create_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
6400 self.create_trigger_inner(trigger, None, None)
6401 }
6402
6403 pub fn create_trigger_controlled<F>(
6404 &self,
6405 trigger: StoredTrigger,
6406 mut before_publish: F,
6407 ) -> Result<StoredTrigger>
6408 where
6409 F: FnMut() -> Result<()>,
6410 {
6411 self.create_trigger_inner(trigger, None, Some(&mut before_publish))
6412 }
6413
6414 pub fn create_trigger_as_controlled<F>(
6415 &self,
6416 trigger: StoredTrigger,
6417 principal: Option<&crate::auth::Principal>,
6418 mut before_publish: F,
6419 ) -> Result<StoredTrigger>
6420 where
6421 F: FnMut() -> Result<()>,
6422 {
6423 self.create_trigger_inner(trigger, principal, Some(&mut before_publish))
6424 }
6425
6426 fn create_trigger_inner(
6427 &self,
6428 mut trigger: StoredTrigger,
6429 principal: Option<&crate::auth::Principal>,
6430 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6431 ) -> Result<StoredTrigger> {
6432 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6433 let _g = self.ddl_lock.lock();
6434 let _security_write = self.security_write()?;
6435 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6436 trigger.validate()?;
6437 self.validate_trigger_references(&trigger)
6438 .map_err(trigger_validation_error)?;
6439 {
6440 let cat = self.catalog.read();
6441 if cat.triggers.iter().any(|t| t.trigger.name == trigger.name) {
6442 return Err(MongrelError::TriggerValidation(format!(
6443 "trigger {:?} already exists",
6444 trigger.name
6445 )));
6446 }
6447 }
6448 let commit_lock = Arc::clone(&self.commit_lock);
6449 let _c = commit_lock.lock();
6450 let epoch = self.epoch.bump_assigned();
6451 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6452 trigger.created_epoch = epoch.0;
6453 trigger.updated_epoch = epoch.0;
6454 let mut next_catalog = self.catalog.read().clone();
6455 next_catalog
6456 .triggers
6457 .push(TriggerEntry::from(trigger.clone()));
6458 next_catalog.db_epoch = epoch.0;
6459 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6460 Ok(trigger)
6461 }
6462
6463 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
6464 self.create_or_replace_trigger_inner(trigger, None, None)
6465 }
6466
6467 pub fn create_or_replace_trigger_controlled<F>(
6468 &self,
6469 trigger: StoredTrigger,
6470 mut before_publish: F,
6471 ) -> Result<StoredTrigger>
6472 where
6473 F: FnMut() -> Result<()>,
6474 {
6475 self.create_or_replace_trigger_inner(trigger, None, Some(&mut before_publish))
6476 }
6477
6478 pub fn create_or_replace_trigger_as_controlled<F>(
6479 &self,
6480 trigger: StoredTrigger,
6481 principal: Option<&crate::auth::Principal>,
6482 mut before_publish: F,
6483 ) -> Result<StoredTrigger>
6484 where
6485 F: FnMut() -> Result<()>,
6486 {
6487 self.create_or_replace_trigger_inner(trigger, principal, Some(&mut before_publish))
6488 }
6489
6490 fn create_or_replace_trigger_inner(
6491 &self,
6492 trigger: StoredTrigger,
6493 principal: Option<&crate::auth::Principal>,
6494 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6495 ) -> Result<StoredTrigger> {
6496 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6497 let _g = self.ddl_lock.lock();
6498 let _security_write = self.security_write()?;
6499 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6500 trigger.validate()?;
6501 self.validate_trigger_references(&trigger)
6502 .map_err(trigger_validation_error)?;
6503 let commit_lock = Arc::clone(&self.commit_lock);
6504 let _c = commit_lock.lock();
6505 let epoch = self.epoch.bump_assigned();
6506 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6507 let mut next_catalog = self.catalog.read().clone();
6508 let replaced = {
6509 let next = match next_catalog
6510 .triggers
6511 .iter()
6512 .position(|t| t.trigger.name == trigger.name)
6513 {
6514 Some(idx) => {
6515 let next = next_catalog.triggers[idx]
6516 .trigger
6517 .replaced(trigger.clone(), epoch.0)?;
6518 next_catalog.triggers[idx] = TriggerEntry::from(next.clone());
6519 next
6520 }
6521 None => {
6522 let mut next = trigger;
6523 next.created_epoch = epoch.0;
6524 next.updated_epoch = epoch.0;
6525 next_catalog.triggers.push(TriggerEntry::from(next.clone()));
6526 next
6527 }
6528 };
6529 next_catalog.db_epoch = epoch.0;
6530 next
6531 };
6532 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6533 Ok(replaced)
6534 }
6535
6536 pub fn drop_trigger(&self, name: &str) -> Result<()> {
6537 self.drop_trigger_with_epoch(name).map(|_| ())
6538 }
6539
6540 pub fn drop_trigger_with_epoch(&self, name: &str) -> Result<Epoch> {
6542 self.drop_triggers_with_epoch(&[name.to_string()])
6543 }
6544
6545 pub fn drop_trigger_with_epoch_controlled<F>(
6546 &self,
6547 name: &str,
6548 before_publish: F,
6549 ) -> Result<Epoch>
6550 where
6551 F: FnMut() -> Result<()>,
6552 {
6553 self.drop_triggers_with_epoch_controlled(&[name.to_string()], before_publish)
6554 }
6555
6556 pub fn drop_triggers_with_epoch(&self, names: &[String]) -> Result<Epoch> {
6558 self.drop_triggers_with_epoch_inner(names, None, None)
6559 }
6560
6561 pub fn drop_triggers_with_epoch_controlled<F>(
6562 &self,
6563 names: &[String],
6564 mut before_publish: F,
6565 ) -> Result<Epoch>
6566 where
6567 F: FnMut() -> Result<()>,
6568 {
6569 self.drop_triggers_with_epoch_inner(names, None, Some(&mut before_publish))
6570 }
6571
6572 pub fn drop_triggers_with_epoch_as_controlled<F>(
6573 &self,
6574 names: &[String],
6575 principal: Option<&crate::auth::Principal>,
6576 mut before_publish: F,
6577 ) -> Result<Epoch>
6578 where
6579 F: FnMut() -> Result<()>,
6580 {
6581 self.drop_triggers_with_epoch_inner(names, principal, Some(&mut before_publish))
6582 }
6583
6584 fn drop_triggers_with_epoch_inner(
6585 &self,
6586 names: &[String],
6587 principal: Option<&crate::auth::Principal>,
6588 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6589 ) -> Result<Epoch> {
6590 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6591 if names.is_empty() {
6592 return Err(MongrelError::InvalidArgument(
6593 "at least one trigger name is required".into(),
6594 ));
6595 }
6596 let _g = self.ddl_lock.lock();
6597 let _security_write = self.security_write()?;
6598 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6599 {
6600 let cat = self.catalog.read();
6601 for name in names {
6602 if !cat.triggers.iter().any(|t| t.trigger.name == *name) {
6603 return Err(MongrelError::NotFound(format!(
6604 "trigger {name:?} not found"
6605 )));
6606 }
6607 }
6608 }
6609 let commit_lock = Arc::clone(&self.commit_lock);
6610 let _c = commit_lock.lock();
6611 let epoch = self.epoch.bump_assigned();
6612 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6613 let mut next_catalog = self.catalog.read().clone();
6614 next_catalog
6615 .triggers
6616 .retain(|trigger| !names.contains(&trigger.trigger.name));
6617 next_catalog.db_epoch = epoch.0;
6618 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6619 Ok(epoch)
6620 }
6621
6622 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
6623 self.catalog.read().external_tables.clone()
6624 }
6625
6626 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
6627 self.catalog
6628 .read()
6629 .external_tables
6630 .iter()
6631 .find(|t| t.name == name)
6632 .cloned()
6633 }
6634
6635 pub fn create_external_table(&self, entry: ExternalTableEntry) -> Result<ExternalTableEntry> {
6636 self.create_external_table_inner(entry, None)
6637 }
6638
6639 pub fn create_external_table_controlled<F>(
6640 &self,
6641 entry: ExternalTableEntry,
6642 mut before_publish: F,
6643 ) -> Result<ExternalTableEntry>
6644 where
6645 F: FnMut() -> Result<()>,
6646 {
6647 self.create_external_table_inner(entry, Some(&mut before_publish))
6648 }
6649
6650 fn create_external_table_inner(
6651 &self,
6652 mut entry: ExternalTableEntry,
6653 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6654 ) -> Result<ExternalTableEntry> {
6655 self.require(&crate::auth::Permission::Ddl)?;
6656 let _g = self.ddl_lock.lock();
6657 let _security_write = self.security_write()?;
6658 self.require(&crate::auth::Permission::Ddl)?;
6659 entry.validate()?;
6660 {
6661 let cat = self.catalog.read();
6662 if cat.live(&entry.name).is_some()
6663 || cat.external_tables.iter().any(|t| t.name == entry.name)
6664 {
6665 return Err(MongrelError::InvalidArgument(format!(
6666 "table {:?} already exists",
6667 entry.name
6668 )));
6669 }
6670 }
6671 let commit_lock = Arc::clone(&self.commit_lock);
6672 let _c = commit_lock.lock();
6673 crate::durable_file::create_directory(&self.root.join(VTAB_DIR))?;
6677 crate::durable_file::remove_directory_all(&self.root.join(VTAB_DIR).join(&entry.name))?;
6678 let epoch = self.epoch.bump_assigned();
6679 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6680 entry.created_epoch = epoch.0;
6681 let mut next_catalog = self.catalog.read().clone();
6682 next_catalog.external_tables.push(entry.clone());
6683 next_catalog.db_epoch = epoch.0;
6684 self.publish_catalog_candidate_with_prelude(
6685 next_catalog,
6686 epoch,
6687 &mut _epoch_guard,
6688 before_publish,
6689 vec![(
6690 EXTERNAL_TABLE_ID,
6691 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
6692 name: entry.name.clone(),
6693 generation_epoch: epoch.0,
6694 }),
6695 )],
6696 )?;
6697 Ok(entry)
6698 }
6699
6700 pub fn drop_external_table(&self, name: &str) -> Result<()> {
6701 self.drop_external_table_with_epoch(name).map(|_| ())
6702 }
6703
6704 pub fn drop_external_table_with_epoch(&self, name: &str) -> Result<Epoch> {
6706 self.drop_external_table_with_epoch_inner(name, None)
6707 }
6708
6709 pub fn drop_external_table_with_epoch_controlled<F>(
6710 &self,
6711 name: &str,
6712 mut before_publish: F,
6713 ) -> Result<Epoch>
6714 where
6715 F: FnMut() -> Result<()>,
6716 {
6717 self.drop_external_table_with_epoch_inner(name, Some(&mut before_publish))
6718 }
6719
6720 fn drop_external_table_with_epoch_inner(
6721 &self,
6722 name: &str,
6723 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6724 ) -> Result<Epoch> {
6725 self.require(&crate::auth::Permission::Ddl)?;
6726 let _g = self.ddl_lock.lock();
6727 let _security_write = self.security_write()?;
6728 self.require(&crate::auth::Permission::Ddl)?;
6729 let commit_lock = Arc::clone(&self.commit_lock);
6730 let _c = commit_lock.lock();
6731 let epoch = self.epoch.bump_assigned();
6732 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6733 let mut next_catalog = self.catalog.read().clone();
6734 let before = next_catalog.external_tables.len();
6735 next_catalog.external_tables.retain(|t| t.name != name);
6736 if next_catalog.external_tables.len() == before {
6737 return Err(MongrelError::NotFound(format!(
6738 "external table {name:?} not found"
6739 )));
6740 }
6741 next_catalog.db_epoch = epoch.0;
6742 self.publish_catalog_candidate_with_prelude(
6743 next_catalog,
6744 epoch,
6745 &mut _epoch_guard,
6746 before_publish,
6747 vec![(
6748 EXTERNAL_TABLE_ID,
6749 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
6750 name: name.to_string(),
6751 generation_epoch: epoch.0,
6752 }),
6753 )],
6754 )?;
6755 let state_dir = self.root.join(VTAB_DIR).join(name);
6756 if let Err(error) = crate::durable_file::remove_directory_all(&state_dir) {
6757 return Err(MongrelError::DurableCommit {
6758 epoch: epoch.0,
6759 message: format!(
6760 "external table was dropped but connector-state cleanup failed: {error}"
6761 ),
6762 });
6763 }
6764 Ok(epoch)
6765 }
6766
6767 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
6768 let txn_id = self.alloc_txn_id()?;
6769 let (principal, catalog_bound) = self.transaction_principal_snapshot();
6770 self.commit_transaction_with_external_states(
6771 txn_id,
6772 self.epoch.visible(),
6773 Vec::new(),
6774 vec![(name.to_string(), state.to_vec())],
6775 Vec::new(),
6776 principal,
6777 catalog_bound,
6778 None,
6779 )
6780 .map(|(epoch, _)| epoch)
6781 }
6782
6783 pub fn trigger_config(&self) -> TriggerConfig {
6784 use std::sync::atomic::Ordering;
6785 TriggerConfig {
6786 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
6787 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
6788 max_loop_iterations: self.trigger_max_loop_iterations.load(Ordering::Relaxed),
6789 }
6790 }
6791
6792 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
6793 use std::sync::atomic::Ordering;
6794 if config.max_depth == 0 {
6795 return Err(MongrelError::InvalidArgument(
6796 "trigger max_depth must be greater than 0".into(),
6797 ));
6798 }
6799 self.trigger_recursive
6800 .store(config.recursive_triggers, Ordering::Relaxed);
6801 self.trigger_max_depth
6802 .store(config.max_depth, Ordering::Relaxed);
6803 self.trigger_max_loop_iterations
6804 .store(config.max_loop_iterations, Ordering::Relaxed);
6805 Ok(())
6806 }
6807
6808 pub fn set_recursive_triggers(&self, recursive: bool) {
6809 use std::sync::atomic::Ordering;
6810 self.trigger_recursive.store(recursive, Ordering::Relaxed);
6811 }
6812
6813 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent> {
6817 self.notify.subscribe()
6818 }
6819
6820 pub fn subscribe_change_commits(&self) -> tokio::sync::broadcast::Receiver<()> {
6821 self.change_wake.subscribe()
6822 }
6823
6824 pub fn change_events_since(&self, last_event_id: Option<&str>) -> Result<CdcBatch> {
6829 let control = crate::ExecutionControl::new(None);
6830 self.change_events_since_controlled(last_event_id, &control)
6831 }
6832
6833 pub fn change_events_since_controlled(
6835 &self,
6836 last_event_id: Option<&str>,
6837 control: &crate::ExecutionControl,
6838 ) -> Result<CdcBatch> {
6839 use crate::wal::Op;
6840
6841 control.checkpoint()?;
6842 let resume = match last_event_id {
6843 Some(id) => {
6844 let (epoch, index) = id.split_once(':').ok_or_else(|| {
6845 MongrelError::InvalidArgument(format!(
6846 "invalid CDC event id {id:?}; expected <epoch>:<index>"
6847 ))
6848 })?;
6849 Some((
6850 epoch.parse::<u64>().map_err(|error| {
6851 MongrelError::InvalidArgument(format!("invalid CDC epoch: {error}"))
6852 })?,
6853 index.parse::<u32>().map_err(|error| {
6854 MongrelError::InvalidArgument(format!("invalid CDC index: {error}"))
6855 })?,
6856 ))
6857 }
6858 None => None,
6859 };
6860
6861 let mut wal = self.shared_wal.lock();
6862 wal.group_sync()?;
6863 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
6864 let records = crate::wal::SharedWal::replay_with_dek_controlled(
6865 &self.root,
6866 wal_dek.as_ref(),
6867 control,
6868 CDC_MAX_WAL_RECORDS,
6869 CDC_MAX_WAL_REPLAY_BYTES,
6870 )?;
6871 drop(wal);
6872 control.checkpoint()?;
6873
6874 let mut commits: HashMap<u64, (u64, Vec<crate::wal::AddedRun>)> = HashMap::new();
6875 let mut spilled_payloads: HashMap<(u64, u64), Vec<&[u8]>> = HashMap::new();
6876 for (index, record) in records.iter().enumerate() {
6877 if index % 256 == 0 {
6878 control.checkpoint()?;
6879 }
6880 if let Op::TxnCommit { epoch, added_runs } = &record.op {
6881 commits.insert(record.txn_id, (*epoch, added_runs.clone()));
6882 }
6883 if let Op::SpilledRows { table_id, rows } = &record.op {
6884 spilled_payloads
6885 .entry((record.txn_id, *table_id))
6886 .or_default()
6887 .push(rows);
6888 }
6889 }
6890 let earliest_epoch = commits.values().map(|(epoch, _)| *epoch).min();
6891 let current_epoch = self.epoch.committed().0;
6892 let retention_floor = crate::replication::replication_wal_floor(&self.root)?;
6893 let gap = resume.is_some_and(|(epoch, _)| {
6894 retention_floor != 0 && epoch <= retention_floor && epoch <= current_epoch
6895 });
6896 if gap {
6897 return Ok(CdcBatch {
6898 events: Vec::new(),
6899 current_epoch,
6900 earliest_epoch,
6901 gap: true,
6902 });
6903 }
6904
6905 let table_names: HashMap<u64, String> = self
6906 .catalog
6907 .read()
6908 .tables
6909 .iter()
6910 .map(|entry| (entry.table_id, entry.name.clone()))
6911 .collect();
6912 let mut before_images: HashMap<(u64, u64, u64), crate::memtable::Row> = HashMap::new();
6913 let mut retained_bytes = 0_usize;
6914 for (index, record) in records.iter().enumerate() {
6915 if index % 256 == 0 {
6916 control.checkpoint()?;
6917 }
6918 if !commits.contains_key(&record.txn_id) {
6919 continue;
6920 }
6921 let Op::BeforeImage {
6922 table_id,
6923 row_id,
6924 row,
6925 } = &record.op
6926 else {
6927 continue;
6928 };
6929 if row.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
6930 return Err(MongrelError::ResourceLimitExceeded {
6931 resource: "CDC before-image bytes",
6932 requested: row.len(),
6933 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
6934 });
6935 }
6936 let before: crate::memtable::Row = bincode::deserialize(row)?;
6937 if before_images.len() >= CDC_MAX_ROWS {
6938 return Err(MongrelError::ResourceLimitExceeded {
6939 resource: "CDC before-image rows",
6940 requested: before_images.len().saturating_add(1),
6941 limit: CDC_MAX_ROWS,
6942 });
6943 }
6944 charge_cdc_bytes(
6945 &mut retained_bytes,
6946 cdc_row_storage_bytes(&before),
6947 "CDC retained bytes",
6948 )?;
6949 before_images.insert((record.txn_id, *table_id, row_id.0), before);
6950 }
6951 let mut operation_indices: HashMap<u64, u32> = HashMap::new();
6952 let mut events = Vec::new();
6953 let mut decoded_rows = before_images.len();
6954 for (record_index, record) in records.iter().enumerate() {
6955 if record_index % 256 == 0 {
6956 control.checkpoint()?;
6957 }
6958 let Some((commit_epoch, _)) = commits.get(&record.txn_id) else {
6959 continue;
6960 };
6961 let event = match &record.op {
6962 Op::Put { table_id, rows } => {
6963 if rows.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
6964 return Err(MongrelError::ResourceLimitExceeded {
6965 resource: "CDC inline row bytes",
6966 requested: rows.len(),
6967 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
6968 });
6969 }
6970 let rows: Vec<crate::memtable::Row> = bincode::deserialize(rows)?;
6971 decoded_rows = decoded_rows.saturating_add(rows.len());
6972 if decoded_rows > CDC_MAX_ROWS {
6973 return Err(MongrelError::ResourceLimitExceeded {
6974 resource: "CDC decoded rows",
6975 requested: decoded_rows,
6976 limit: CDC_MAX_ROWS,
6977 });
6978 }
6979 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(512);
6980 let mut peak_bytes = retained_bytes;
6981 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
6982 let data = serde_json::to_value(rows)
6983 .map_err(|error| MongrelError::Other(format!("CDC JSON: {error}")))?;
6984 Some((*table_id, "put", data, event_bytes))
6985 }
6986 Op::Delete { table_id, row_ids } => {
6987 let before = row_ids
6988 .iter()
6989 .filter_map(|row_id| {
6990 before_images
6991 .get(&(record.txn_id, *table_id, row_id.0))
6992 .cloned()
6993 })
6994 .collect::<Vec<_>>();
6995 let event_bytes = cdc_rows_json_bytes(&before)
6996 .saturating_add(
6997 row_ids
6998 .len()
6999 .saturating_mul(std::mem::size_of::<serde_json::Value>()),
7000 )
7001 .saturating_add(512);
7002 let mut peak_bytes = retained_bytes;
7003 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
7004 Some((
7005 *table_id,
7006 "delete",
7007 serde_json::json!({
7008 "row_ids": row_ids.iter().map(|row_id| row_id.0).collect::<Vec<_>>(),
7009 "before": before,
7010 }),
7011 event_bytes,
7012 ))
7013 }
7014 Op::TruncateTable { table_id } => {
7015 Some((*table_id, "truncate", serde_json::Value::Null, 512))
7016 }
7017 _ => None,
7018 };
7019 if let Some((table_id, op, data, event_bytes)) = event {
7020 let index = operation_indices.entry(record.txn_id).or_insert(0);
7021 let event_position = (*commit_epoch, *index);
7022 *index = index.saturating_add(1);
7023 if resume.is_some_and(|position| event_position <= position) {
7024 continue;
7025 }
7026 if events.len() >= CDC_MAX_EVENTS {
7027 return Err(MongrelError::ResourceLimitExceeded {
7028 resource: "CDC events",
7029 requested: events.len().saturating_add(1),
7030 limit: CDC_MAX_EVENTS,
7031 });
7032 }
7033 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
7034 events.push(ChangeEvent {
7035 id: Some(format!("{}:{}", event_position.0, event_position.1)),
7036 channel: "changes".into(),
7037 table_id: Some(table_id),
7038 table: table_names.get(&table_id).cloned().unwrap_or_default(),
7039 op: op.into(),
7040 epoch: *commit_epoch,
7041 txn_id: Some(record.txn_id),
7042 message: None,
7043 data: Some(data),
7044 });
7045 }
7046 if let Op::TxnCommit { added_runs, .. } = &record.op {
7047 for run in added_runs {
7048 control.checkpoint()?;
7049 let index = operation_indices.entry(record.txn_id).or_insert(0);
7050 let event_position = (*commit_epoch, *index);
7051 *index = index.saturating_add(1);
7052 if resume.is_some_and(|position| event_position <= position) {
7053 continue;
7054 }
7055 let mut rows = if let Some(payloads) =
7056 spilled_payloads.get(&(record.txn_id, run.table_id))
7057 {
7058 let mut rows = Vec::new();
7059 for payload in payloads {
7060 control.checkpoint()?;
7061 if payload.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
7062 return Err(MongrelError::ResourceLimitExceeded {
7063 resource: "CDC spilled row bytes",
7064 requested: payload.len(),
7065 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
7066 });
7067 }
7068 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(payload)?;
7069 if decoded_rows
7070 .saturating_add(rows.len())
7071 .saturating_add(chunk.len())
7072 > CDC_MAX_ROWS
7073 {
7074 return Err(MongrelError::ResourceLimitExceeded {
7075 resource: "CDC decoded rows",
7076 requested: decoded_rows
7077 .saturating_add(rows.len())
7078 .saturating_add(chunk.len()),
7079 limit: CDC_MAX_ROWS,
7080 });
7081 }
7082 rows.extend(chunk);
7083 }
7084 rows
7085 } else {
7086 let Some(handle) = self.tables.read().get(&run.table_id).cloned() else {
7087 return Ok(CdcBatch {
7088 events: Vec::new(),
7089 current_epoch,
7090 earliest_epoch,
7091 gap: true,
7092 });
7093 };
7094 let table = handle.lock();
7095 let mut reader = match table.open_reader(run.run_id) {
7096 Ok(reader) => reader,
7097 Err(_) => {
7098 return Ok(CdcBatch {
7099 events: Vec::new(),
7100 current_epoch,
7101 earliest_epoch,
7102 gap: true,
7103 })
7104 }
7105 };
7106 let remaining = CDC_MAX_ROWS.saturating_sub(decoded_rows);
7107 let rows = reader.all_rows_controlled(control, remaining)?;
7108 drop(reader);
7109 drop(table);
7110 rows
7111 };
7112 for row in &mut rows {
7113 row.committed_epoch = Epoch(*commit_epoch);
7114 }
7115 decoded_rows = decoded_rows.saturating_add(rows.len());
7116 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(768);
7117 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
7118 if events.len() >= CDC_MAX_EVENTS {
7119 return Err(MongrelError::ResourceLimitExceeded {
7120 resource: "CDC events",
7121 requested: events.len().saturating_add(1),
7122 limit: CDC_MAX_EVENTS,
7123 });
7124 }
7125 events.push(ChangeEvent {
7126 id: Some(format!("{}:{}", event_position.0, event_position.1)),
7127 channel: "changes".into(),
7128 table_id: Some(run.table_id),
7129 table: table_names.get(&run.table_id).cloned().unwrap_or_default(),
7130 op: "put_run".into(),
7131 epoch: *commit_epoch,
7132 txn_id: Some(record.txn_id),
7133 message: None,
7134 data: Some(serde_json::json!({
7135 "run_id": run.run_id.to_string(),
7136 "row_count": run.row_count,
7137 "min_row_id": run.min_row_id,
7138 "max_row_id": run.max_row_id,
7139 "rows": rows,
7140 })),
7141 });
7142 }
7143 }
7144 }
7145 control.checkpoint()?;
7146 Ok(CdcBatch {
7147 events,
7148 current_epoch,
7149 earliest_epoch,
7150 gap: false,
7151 })
7152 }
7153
7154 pub fn notify(&self, channel: &str, message: Option<String>) {
7157 let _ = self.notify.send(ChangeEvent {
7158 id: None,
7159 channel: channel.to_string(),
7160 table_id: None,
7161 table: String::new(),
7162 op: "notify".into(),
7163 epoch: self.epoch.visible().0,
7164 txn_id: None,
7165 message,
7166 data: None,
7167 });
7168 }
7169
7170 pub fn call_procedure(
7171 &self,
7172 name: &str,
7173 args: HashMap<String, crate::Value>,
7174 ) -> Result<ProcedureCallResult> {
7175 self.call_procedure_as(name, args, None)
7176 }
7177
7178 pub fn call_procedure_as(
7179 &self,
7180 name: &str,
7181 args: HashMap<String, crate::Value>,
7182 principal: Option<&crate::auth::Principal>,
7183 ) -> Result<ProcedureCallResult> {
7184 let control = crate::ExecutionControl::new(None);
7185 self.call_procedure_as_controlled(name, args, principal, &control, || true)
7186 }
7187
7188 #[doc(hidden)]
7191 pub fn call_procedure_as_bound(
7192 &self,
7193 expected: &StoredProcedure,
7194 args: HashMap<String, crate::Value>,
7195 principal: Option<&crate::auth::Principal>,
7196 ) -> Result<ProcedureCallResult> {
7197 self.require_for(principal, &crate::auth::Permission::All)?;
7198 let procedure = self.procedure(&expected.name).ok_or_else(|| {
7199 MongrelError::NotFound(format!("procedure {:?} not found", expected.name))
7200 })?;
7201 if &procedure != expected {
7202 return Err(MongrelError::Conflict(format!(
7203 "procedure {:?} changed after request authorization",
7204 expected.name
7205 )));
7206 }
7207 let control = crate::ExecutionControl::new(None);
7208 self.execute_procedure_as_controlled(procedure, args, principal, &control, || true)
7209 }
7210
7211 #[doc(hidden)]
7216 pub fn call_procedure_as_controlled<F>(
7217 &self,
7218 name: &str,
7219 args: HashMap<String, crate::Value>,
7220 principal: Option<&crate::auth::Principal>,
7221 control: &crate::ExecutionControl,
7222 before_commit: F,
7223 ) -> Result<ProcedureCallResult>
7224 where
7225 F: FnOnce() -> bool,
7226 {
7227 self.require_for(principal, &crate::auth::Permission::All)?;
7231 let procedure = self
7232 .procedure(name)
7233 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
7234 self.execute_procedure_as_controlled(procedure, args, principal, control, before_commit)
7235 }
7236
7237 fn execute_procedure_as_controlled<F>(
7238 &self,
7239 procedure: StoredProcedure,
7240 args: HashMap<String, crate::Value>,
7241 principal: Option<&crate::auth::Principal>,
7242 control: &crate::ExecutionControl,
7243 before_commit: F,
7244 ) -> Result<ProcedureCallResult>
7245 where
7246 F: FnOnce() -> bool,
7247 {
7248 let args = bind_procedure_args(&procedure, args)?;
7249 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
7250 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
7251 if has_writes {
7252 let mut tx = self.begin_as(principal.cloned());
7253 let run = (|| {
7254 for (step_index, step) in procedure.body.steps.iter().enumerate() {
7255 if step_index % 256 == 0 {
7256 control.checkpoint()?;
7257 }
7258 let output = self.execute_procedure_step(
7259 step,
7260 &args,
7261 &outputs,
7262 Some(&mut tx),
7263 principal,
7264 Some(control),
7265 )?;
7266 outputs.insert(step.id().to_string(), output);
7267 }
7268 control.checkpoint()?;
7269 eval_return_output(&procedure.body.return_value, &args, &outputs)
7270 })();
7271 match run {
7272 Ok(output) => {
7273 control.checkpoint()?;
7274 if !before_commit() {
7275 tx.rollback();
7276 return Err(MongrelError::Cancelled);
7277 }
7278 let epoch = tx.commit()?.0;
7279 Ok(ProcedureCallResult {
7280 epoch: Some(epoch),
7281 output,
7282 })
7283 }
7284 Err(e) => {
7285 tx.rollback();
7286 Err(e)
7287 }
7288 }
7289 } else {
7290 for (step_index, step) in procedure.body.steps.iter().enumerate() {
7291 if step_index % 256 == 0 {
7292 control.checkpoint()?;
7293 }
7294 let output = self.execute_procedure_step(
7295 step,
7296 &args,
7297 &outputs,
7298 None,
7299 principal,
7300 Some(control),
7301 )?;
7302 outputs.insert(step.id().to_string(), output);
7303 }
7304 control.checkpoint()?;
7305 Ok(ProcedureCallResult {
7306 epoch: None,
7307 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
7308 })
7309 }
7310 }
7311
7312 fn execute_procedure_step(
7313 &self,
7314 step: &ProcedureStep,
7315 args: &HashMap<String, crate::Value>,
7316 outputs: &HashMap<String, ProcedureCallOutput>,
7317 tx: Option<&mut crate::txn::Transaction<'_>>,
7318 principal: Option<&crate::auth::Principal>,
7319 control: Option<&crate::ExecutionControl>,
7320 ) -> Result<ProcedureCallOutput> {
7321 if let Some(control) = control {
7322 control.checkpoint()?;
7323 }
7324 match step {
7325 ProcedureStep::NativeQuery {
7326 table,
7327 conditions,
7328 projection,
7329 limit,
7330 ..
7331 } => {
7332 let mut q = crate::Query::new();
7333 for condition in conditions {
7334 q = q.and(eval_condition(condition, args, outputs)?);
7335 }
7336 let fallback_control = crate::ExecutionControl::new(None);
7337 let query_control = control.unwrap_or(&fallback_control);
7338 let mut rows = self.query_for_principal_controlled(
7339 table,
7340 &q,
7341 projection.as_deref(),
7342 principal,
7343 false,
7344 query_control,
7345 )?;
7346 if let Some(limit) = limit {
7347 rows.truncate(*limit);
7348 }
7349 let mut output = Vec::with_capacity(rows.len());
7350 for (row_index, row) in rows.into_iter().enumerate() {
7351 if row_index % 256 == 0 {
7352 if let Some(control) = control {
7353 control.checkpoint()?;
7354 }
7355 }
7356 output.push(ProcedureCallRow {
7357 row_id: Some(row.row_id),
7358 columns: row.columns,
7359 });
7360 }
7361 Ok(ProcedureCallOutput::Rows(output))
7362 }
7363 ProcedureStep::Put {
7364 table,
7365 cells,
7366 returning,
7367 ..
7368 } => {
7369 let tx = tx.ok_or_else(|| {
7370 MongrelError::InvalidArgument(
7371 "write procedure step requires a transaction".into(),
7372 )
7373 })?;
7374 let cells = eval_cells(cells, args, outputs)?;
7375 if *returning {
7376 let out = tx.put_returning(table, cells)?;
7377 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
7378 row_id: None,
7379 columns: out.row.columns.into_iter().collect(),
7380 }))
7381 } else {
7382 tx.put(table, cells)?;
7383 Ok(ProcedureCallOutput::Null)
7384 }
7385 }
7386 ProcedureStep::Upsert {
7387 table,
7388 cells,
7389 update_cells,
7390 returning,
7391 ..
7392 } => {
7393 let tx = tx.ok_or_else(|| {
7394 MongrelError::InvalidArgument(
7395 "write procedure step requires a transaction".into(),
7396 )
7397 })?;
7398 let cells = eval_cells(cells, args, outputs)?;
7399 let action = match update_cells {
7400 Some(update_cells) => {
7401 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
7402 }
7403 None => crate::UpsertAction::DoNothing,
7404 };
7405 let out = tx.upsert(table, cells, action)?;
7406 if *returning {
7407 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
7408 row_id: None,
7409 columns: out.row.columns.into_iter().collect(),
7410 }))
7411 } else {
7412 Ok(ProcedureCallOutput::Null)
7413 }
7414 }
7415 ProcedureStep::DeleteByPk { table, pk, .. } => {
7416 let tx = tx.ok_or_else(|| {
7417 MongrelError::InvalidArgument(
7418 "write procedure step requires a transaction".into(),
7419 )
7420 })?;
7421 let pk = eval_value(pk, args, outputs)?;
7422 let handle = self.table(table)?;
7423 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
7424 MongrelError::NotFound("procedure delete_by_pk target not found".into())
7425 })?;
7426 tx.delete(table, row_id)?;
7427 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
7428 }
7429 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
7430 "DeleteRows procedure step is not supported by the core executor yet".into(),
7431 )),
7432 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
7433 "SqlQuery procedure step must be executed by mongreldb-query".into(),
7434 )),
7435 }
7436 }
7437
7438 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
7439 let cat = self.catalog.read();
7440 for step in &procedure.body.steps {
7441 let Some(table_name) = step.table() else {
7442 continue;
7443 };
7444 let schema = &cat
7445 .live(table_name)
7446 .ok_or_else(|| {
7447 MongrelError::InvalidArgument(format!(
7448 "procedure {:?} references unknown table {table_name:?}",
7449 procedure.name
7450 ))
7451 })?
7452 .schema;
7453 match step {
7454 ProcedureStep::NativeQuery {
7455 conditions,
7456 projection,
7457 ..
7458 } => {
7459 for condition in conditions {
7460 validate_condition_columns(condition, schema)?;
7461 }
7462 if let Some(projection) = projection {
7463 for id in projection {
7464 validate_column_id(*id, schema)?;
7465 }
7466 }
7467 }
7468 ProcedureStep::Put { cells, .. } => {
7469 for cell in cells {
7470 validate_column_id(cell.column_id, schema)?;
7471 }
7472 }
7473 ProcedureStep::Upsert {
7474 cells,
7475 update_cells,
7476 ..
7477 } => {
7478 for cell in cells {
7479 validate_column_id(cell.column_id, schema)?;
7480 }
7481 if let Some(update_cells) = update_cells {
7482 for cell in update_cells {
7483 validate_column_id(cell.column_id, schema)?;
7484 }
7485 }
7486 }
7487 ProcedureStep::DeleteByPk { .. } => {
7488 if schema.primary_key().is_none() {
7489 return Err(MongrelError::InvalidArgument(format!(
7490 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
7491 procedure.name
7492 )));
7493 }
7494 }
7495 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
7496 }
7497 }
7498 Ok(())
7499 }
7500
7501 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
7502 let cat = self.catalog.read();
7503 let target_schema = match &trigger.target {
7504 TriggerTarget::Table(target_name) => cat
7505 .live(target_name)
7506 .ok_or_else(|| {
7507 MongrelError::InvalidArgument(format!(
7508 "trigger {:?} references unknown target table {target_name:?}",
7509 trigger.name
7510 ))
7511 })?
7512 .schema
7513 .clone(),
7514 TriggerTarget::View(_) => Schema {
7515 columns: trigger.target_columns.clone(),
7516 ..Schema::default()
7517 },
7518 };
7519 for col in &trigger.update_of {
7520 if target_schema.column(col).is_none() {
7521 return Err(MongrelError::InvalidArgument(format!(
7522 "trigger {:?} UPDATE OF references unknown column {col:?}",
7523 trigger.name
7524 )));
7525 }
7526 }
7527 if let Some(expr) = &trigger.when {
7528 validate_trigger_expr(expr, &target_schema, trigger.event)?;
7529 }
7530 let mut select_schemas: HashMap<String, &Schema> = HashMap::new();
7531 for step in &trigger.program.steps {
7532 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
7533 {
7534 return Err(MongrelError::InvalidArgument(
7535 "SetNew trigger steps are only valid in BEFORE triggers".into(),
7536 ));
7537 }
7538 validate_trigger_step(
7539 step,
7540 &cat,
7541 &target_schema,
7542 trigger.event,
7543 &mut select_schemas,
7544 )?;
7545 }
7546 Ok(())
7547 }
7548
7549 pub fn begin(&self) -> crate::txn::Transaction<'_> {
7551 self.begin_with_isolation(crate::txn::IsolationLevel::default())
7552 }
7553
7554 fn transaction_principal_snapshot(&self) -> (Option<crate::auth::Principal>, bool) {
7555 let principal = self.principal.read().clone();
7556 let catalog_bound = principal.as_ref().is_some_and(|principal| {
7557 let catalog = self.catalog.read();
7558 catalog.require_auth || principal.user_id != 0
7559 });
7560 (principal, catalog_bound)
7561 }
7562
7563 pub fn begin_as(
7564 &self,
7565 principal: Option<crate::auth::Principal>,
7566 ) -> crate::txn::Transaction<'_> {
7567 let catalog_bound = principal.as_ref().is_some_and(|principal| {
7568 let catalog = self.catalog.read();
7569 catalog.require_auth || principal.user_id != 0
7570 });
7571 let txn_id = self.alloc_txn_id();
7572 let read = Snapshot::at(self.epoch.visible());
7573 crate::txn::Transaction::new(self, txn_id, read).with_principal(principal, catalog_bound)
7574 }
7575
7576 pub fn begin_with_isolation(
7578 &self,
7579 level: crate::txn::IsolationLevel,
7580 ) -> crate::txn::Transaction<'_> {
7581 let txn_id = self.alloc_txn_id();
7582 let epoch = match level {
7583 crate::txn::IsolationLevel::ReadCommitted => self.epoch.visible(),
7584 _ => self.epoch.visible(),
7585 };
7586 let read = Snapshot::at(epoch);
7587 let (principal, catalog_bound) = self.transaction_principal_snapshot();
7588 crate::txn::Transaction::new(self, txn_id, read).with_principal(principal, catalog_bound)
7589 }
7590
7591 pub fn begin_with_external_trigger_bridge<'a>(
7594 &'a self,
7595 bridge: &'a dyn ExternalTriggerBridge,
7596 ) -> crate::txn::Transaction<'a> {
7597 let txn_id = self.alloc_txn_id();
7598 let read = Snapshot::at(self.epoch.visible());
7599 let (principal, catalog_bound) = self.transaction_principal_snapshot();
7600 crate::txn::Transaction::new(self, txn_id, read)
7601 .with_external_trigger_bridge(bridge)
7602 .with_principal(principal, catalog_bound)
7603 }
7604
7605 pub fn begin_with_external_trigger_bridge_as<'a>(
7606 &'a self,
7607 bridge: &'a dyn ExternalTriggerBridge,
7608 principal: Option<crate::auth::Principal>,
7609 ) -> crate::txn::Transaction<'a> {
7610 let catalog_bound = principal.as_ref().is_some_and(|principal| {
7611 let catalog = self.catalog.read();
7612 catalog.require_auth || principal.user_id != 0
7613 });
7614 let txn_id = self.alloc_txn_id();
7615 let read = Snapshot::at(self.epoch.visible());
7616 crate::txn::Transaction::new(self, txn_id, read)
7617 .with_external_trigger_bridge(bridge)
7618 .with_principal(principal, catalog_bound)
7619 }
7620
7621 pub fn transaction<T>(
7623 &self,
7624 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7625 ) -> Result<T> {
7626 let mut tx = self.begin();
7627 match f(&mut tx) {
7628 Ok(out) => {
7629 tx.commit()?;
7630 Ok(out)
7631 }
7632 Err(e) => {
7633 tx.rollback();
7634 Err(e)
7635 }
7636 }
7637 }
7638
7639 pub fn transaction_with_row_ids<T>(
7640 &self,
7641 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7642 ) -> Result<(T, Vec<RowId>)> {
7643 let mut tx = self.begin();
7644 match f(&mut tx) {
7645 Ok(output) => {
7646 let (_, row_ids) = tx.commit_with_row_ids()?;
7647 Ok((output, row_ids))
7648 }
7649 Err(error) => {
7650 tx.rollback();
7651 Err(error)
7652 }
7653 }
7654 }
7655
7656 pub fn transaction_for_current_principal<T>(
7657 &self,
7658 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7659 ) -> Result<T> {
7660 if self.principal.read().is_some() {
7661 self.refresh_principal()?;
7662 }
7663 let mut transaction = self.begin_as(self.principal.read().clone());
7664 match f(&mut transaction) {
7665 Ok(output) => {
7666 transaction.commit()?;
7667 Ok(output)
7668 }
7669 Err(error) => {
7670 transaction.rollback();
7671 Err(error)
7672 }
7673 }
7674 }
7675
7676 pub fn transaction_for_current_principal_with_epoch<T>(
7677 &self,
7678 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7679 ) -> Result<(Epoch, T)> {
7680 if self.principal.read().is_some() {
7681 self.refresh_principal()?;
7682 }
7683 let mut transaction = self.begin_as(self.principal.read().clone());
7684 match f(&mut transaction) {
7685 Ok(output) => {
7686 let epoch = transaction.commit()?;
7687 Ok((epoch, output))
7688 }
7689 Err(error) => {
7690 transaction.rollback();
7691 Err(error)
7692 }
7693 }
7694 }
7695
7696 pub fn transaction_with_row_ids_for_current_principal<T>(
7697 &self,
7698 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7699 ) -> Result<(T, Vec<RowId>)> {
7700 if self.principal.read().is_some() {
7701 self.refresh_principal()?;
7702 }
7703 let mut transaction = self.begin_as(self.principal.read().clone());
7704 match f(&mut transaction) {
7705 Ok(output) => {
7706 let (_, row_ids) = transaction.commit_with_row_ids()?;
7707 Ok((output, row_ids))
7708 }
7709 Err(error) => {
7710 transaction.rollback();
7711 Err(error)
7712 }
7713 }
7714 }
7715
7716 pub fn transaction_with_external_trigger_bridge<'a, T>(
7719 &'a self,
7720 bridge: &'a dyn ExternalTriggerBridge,
7721 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7722 ) -> Result<T> {
7723 let mut tx = self.begin_with_external_trigger_bridge(bridge);
7724 match f(&mut tx) {
7725 Ok(out) => {
7726 tx.commit()?;
7727 Ok(out)
7728 }
7729 Err(e) => {
7730 tx.rollback();
7731 Err(e)
7732 }
7733 }
7734 }
7735
7736 pub fn transaction_with_external_trigger_bridge_as<'a, T>(
7737 &'a self,
7738 bridge: &'a dyn ExternalTriggerBridge,
7739 principal: Option<crate::auth::Principal>,
7740 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7741 ) -> Result<T> {
7742 let mut tx = self.begin_with_external_trigger_bridge_as(bridge, principal);
7743 match f(&mut tx) {
7744 Ok(output) => {
7745 tx.commit()?;
7746 Ok(output)
7747 }
7748 Err(error) => {
7749 tx.rollback();
7750 Err(error)
7751 }
7752 }
7753 }
7754
7755 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
7758 self.active_txns.register(epoch)
7759 }
7760
7761 fn fill_auto_increment_for_staging(
7762 &self,
7763 staging: &mut [(u64, crate::txn::Staged)],
7764 control: Option<&crate::ExecutionControl>,
7765 ) -> Result<()> {
7766 let mut puts_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
7767 for (index, (table_id, staged)) in staging.iter().enumerate() {
7768 commit_prepare_checkpoint(control, index)?;
7769 if matches!(staged, crate::txn::Staged::Put(_)) {
7770 puts_by_table.entry(*table_id).or_default().push(index);
7771 }
7772 }
7773
7774 let tables = self.tables.read();
7775 for (table_index, (table_id, indexes)) in puts_by_table.into_iter().enumerate() {
7776 commit_prepare_checkpoint(control, table_index)?;
7777 if let Some(handle) = tables.get(&table_id) {
7778 #[cfg(test)]
7779 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
7780 let mut t = handle.lock();
7781 for (fill_index, index) in indexes.into_iter().enumerate() {
7782 commit_prepare_checkpoint(control, fill_index)?;
7783 if let crate::txn::Staged::Put(cells) = &mut staging[index].1 {
7784 t.fill_auto_inc(cells)?;
7785 }
7786 }
7787 }
7788 }
7789 Ok(())
7790 }
7791
7792 fn expand_table_triggers(
7793 &self,
7794 staging: &mut Vec<(u64, crate::txn::Staged)>,
7795 read_epoch: Epoch,
7796 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
7797 external_states: &mut Vec<(String, Vec<u8>)>,
7798 control: Option<&crate::ExecutionControl>,
7799 ) -> Result<()> {
7800 commit_prepare_checkpoint(control, 0)?;
7801 let mut external_writes = Vec::new();
7802 let config = self.trigger_config();
7803 if config.recursive_triggers {
7804 let chunk = std::mem::take(staging);
7805 let stacks = vec![Vec::new(); chunk.len()];
7806 *staging = self.expand_trigger_chunk(
7807 chunk,
7808 stacks,
7809 read_epoch,
7810 0,
7811 config.max_depth,
7812 &mut external_writes,
7813 &config,
7814 control,
7815 )?;
7816 self.apply_external_trigger_writes(
7817 external_writes,
7818 external_trigger_bridge,
7819 external_states,
7820 staging,
7821 control,
7822 )?;
7823 return Ok(());
7824 }
7825
7826 let mut expansion =
7827 self.expand_table_triggers_once(staging, read_epoch, None, &config, control)?;
7828 if !expansion.before.is_empty() {
7829 let mut final_staging = expansion.before;
7830 final_staging.extend(filter_ignored_staging(
7831 std::mem::take(staging),
7832 &expansion.ignored_indices,
7833 ));
7834 *staging = final_staging;
7835 } else if !expansion.ignored_indices.is_empty() {
7836 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
7837 }
7838 staging.append(&mut expansion.after);
7839 external_writes.append(&mut expansion.before_external);
7840 external_writes.append(&mut expansion.after_external);
7841 self.apply_external_trigger_writes(
7842 external_writes,
7843 external_trigger_bridge,
7844 external_states,
7845 staging,
7846 control,
7847 )?;
7848 Ok(())
7849 }
7850
7851 #[allow(clippy::too_many_arguments)]
7852 fn expand_trigger_chunk(
7853 &self,
7854 mut chunk: Vec<(u64, crate::txn::Staged)>,
7855 stacks: Vec<Vec<String>>,
7856 read_epoch: Epoch,
7857 depth: u32,
7858 max_depth: u32,
7859 external_writes: &mut Vec<ExternalTriggerWrite>,
7860 config: &TriggerConfig,
7861 control: Option<&crate::ExecutionControl>,
7862 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
7863 if chunk.is_empty() {
7864 return Ok(Vec::new());
7865 }
7866 commit_prepare_checkpoint(control, 0)?;
7867 self.fill_auto_increment_for_staging(&mut chunk, control)?;
7868 let expansion = self.expand_table_triggers_once(
7869 &mut chunk,
7870 read_epoch,
7871 Some(&stacks),
7872 config,
7873 control,
7874 )?;
7875 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
7876 let stack = expansion
7877 .before_stacks
7878 .first()
7879 .or_else(|| expansion.after_stacks.first())
7880 .cloned()
7881 .unwrap_or_default();
7882 return Err(MongrelError::TriggerValidation(format!(
7883 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
7884 Self::format_trigger_stack(&stack)
7885 )));
7886 }
7887
7888 let mut out = Vec::new();
7889 external_writes.extend(expansion.before_external);
7890 out.extend(self.expand_trigger_chunk(
7891 expansion.before,
7892 expansion.before_stacks,
7893 read_epoch,
7894 depth + 1,
7895 max_depth,
7896 external_writes,
7897 config,
7898 control,
7899 )?);
7900 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
7901 external_writes.extend(expansion.after_external);
7902 out.extend(self.expand_trigger_chunk(
7903 expansion.after,
7904 expansion.after_stacks,
7905 read_epoch,
7906 depth + 1,
7907 max_depth,
7908 external_writes,
7909 config,
7910 control,
7911 )?);
7912 Ok(out)
7913 }
7914
7915 fn apply_external_trigger_writes(
7916 &self,
7917 writes: Vec<ExternalTriggerWrite>,
7918 bridge: Option<&dyn ExternalTriggerBridge>,
7919 external_states: &mut Vec<(String, Vec<u8>)>,
7920 staging: &mut Vec<(u64, crate::txn::Staged)>,
7921 control: Option<&crate::ExecutionControl>,
7922 ) -> Result<()> {
7923 if writes.is_empty() {
7924 return Ok(());
7925 }
7926 let bridge = bridge.ok_or_else(|| {
7927 MongrelError::TriggerValidation(
7928 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
7929 )
7930 })?;
7931 for (write_index, write) in writes.into_iter().enumerate() {
7932 commit_prepare_checkpoint(control, write_index)?;
7933 let table = write.table().to_string();
7934 let entry = self.external_table(&table).ok_or_else(|| {
7935 MongrelError::NotFound(format!("external table {table:?} not found"))
7936 })?;
7937 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
7938 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
7939 external_states.push((table, result.state));
7940 for (base_index, base_write) in result.base_writes.into_iter().enumerate() {
7941 commit_prepare_checkpoint(control, base_index)?;
7942 match base_write {
7943 ExternalTriggerBaseWrite::Put { table, cells } => {
7944 let table_id = self.table_id(&table)?;
7945 staging.push((table_id, crate::txn::Staged::Put(cells)));
7946 }
7947 ExternalTriggerBaseWrite::Delete { table, row_id } => {
7948 let table_id = self.table_id(&table)?;
7949 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
7950 }
7951 }
7952 }
7953 }
7954 dedup_external_states_in_place(external_states);
7955 Ok(())
7956 }
7957
7958 fn expand_table_triggers_once(
7959 &self,
7960 staging: &mut Vec<(u64, crate::txn::Staged)>,
7961 read_epoch: Epoch,
7962 trigger_stacks: Option<&[Vec<String>]>,
7963 config: &TriggerConfig,
7964 control: Option<&crate::ExecutionControl>,
7965 ) -> Result<TriggerExpansion> {
7966 commit_prepare_checkpoint(control, 0)?;
7967 let triggers: Vec<StoredTrigger> = self
7968 .catalog
7969 .read()
7970 .triggers
7971 .iter()
7972 .filter(|entry| {
7973 entry.trigger.enabled
7974 && matches!(
7975 entry.trigger.timing,
7976 TriggerTiming::Before | TriggerTiming::After
7977 )
7978 && matches!(entry.trigger.target, TriggerTarget::Table(_))
7979 })
7980 .map(|entry| entry.trigger.clone())
7981 .collect();
7982 if triggers.is_empty() || staging.is_empty() {
7983 return Ok(TriggerExpansion::default());
7984 }
7985
7986 let before_triggers = triggers
7987 .iter()
7988 .filter(|trigger| trigger.timing == TriggerTiming::Before)
7989 .cloned()
7990 .collect::<Vec<_>>();
7991 let after_triggers = triggers
7992 .iter()
7993 .filter(|trigger| trigger.timing == TriggerTiming::After)
7994 .cloned()
7995 .collect::<Vec<_>>();
7996
7997 let mut before_added = Vec::new();
7998 let mut before_stacks = Vec::new();
7999 let mut before_external = Vec::new();
8000 let mut ignored_indices = std::collections::BTreeSet::new();
8001 if !before_triggers.is_empty() {
8002 let before_events =
8003 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?;
8004 let mut out = TriggerProgramOutput {
8005 added: &mut before_added,
8006 added_stacks: &mut before_stacks,
8007 added_external: &mut before_external,
8008 ignored_indices: &mut ignored_indices,
8009 };
8010 self.execute_triggers_for_events(
8011 &before_triggers,
8012 &before_events,
8013 Some(staging),
8014 &mut out,
8015 config,
8016 read_epoch,
8017 control,
8018 )?;
8019 }
8020
8021 let after_events = if after_triggers.is_empty() {
8022 Vec::new()
8023 } else {
8024 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?
8025 .into_iter()
8026 .filter(|event| {
8027 !event
8028 .op_indices
8029 .iter()
8030 .any(|idx| ignored_indices.contains(idx))
8031 })
8032 .collect()
8033 };
8034
8035 let mut after_added = Vec::new();
8036 let mut after_stacks = Vec::new();
8037 let mut after_external = Vec::new();
8038 let mut out = TriggerProgramOutput {
8039 added: &mut after_added,
8040 added_stacks: &mut after_stacks,
8041 added_external: &mut after_external,
8042 ignored_indices: &mut ignored_indices,
8043 };
8044 self.execute_triggers_for_events(
8045 &after_triggers,
8046 &after_events,
8047 None,
8048 &mut out,
8049 config,
8050 read_epoch,
8051 control,
8052 )?;
8053 Ok(TriggerExpansion {
8054 before: before_added,
8055 before_stacks,
8056 before_external,
8057 after: after_added,
8058 after_stacks,
8059 after_external,
8060 ignored_indices,
8061 })
8062 }
8063
8064 #[allow(clippy::too_many_arguments)]
8065 fn execute_triggers_for_events(
8066 &self,
8067 triggers: &[StoredTrigger],
8068 events: &[WriteEvent],
8069 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
8070 out: &mut TriggerProgramOutput<'_>,
8071 config: &TriggerConfig,
8072 read_epoch: Epoch,
8073 control: Option<&crate::ExecutionControl>,
8074 ) -> Result<()> {
8075 let mut checkpoint_index = 0_usize;
8076 for event in events {
8077 for trigger in triggers {
8078 commit_prepare_checkpoint(control, checkpoint_index)?;
8079 checkpoint_index += 1;
8080 if event
8081 .op_indices
8082 .iter()
8083 .any(|idx| out.ignored_indices.contains(idx))
8084 {
8085 break;
8086 }
8087 let matches = {
8088 let cat = self.catalog.read();
8089 trigger_matches_event(trigger, event, &cat)?
8090 };
8091 if !matches {
8092 continue;
8093 }
8094 if let Some(when) = &trigger.when {
8095 if !eval_trigger_expr(when, event)? {
8096 continue;
8097 }
8098 }
8099 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
8100 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
8101 return Err(MongrelError::TriggerValidation(format!(
8102 "trigger recursion cycle detected; trigger stack: {}",
8103 Self::format_trigger_stack(&trigger_stack)
8104 )));
8105 }
8106 let outcome = match staging.as_mut() {
8107 Some(staging) => self.execute_trigger_program(
8108 trigger,
8109 event,
8110 Some(&mut **staging),
8111 out,
8112 &trigger_stack,
8113 config,
8114 read_epoch,
8115 control,
8116 )?,
8117 None => self.execute_trigger_program(
8118 trigger,
8119 event,
8120 None,
8121 out,
8122 &trigger_stack,
8123 config,
8124 read_epoch,
8125 control,
8126 )?,
8127 };
8128 if outcome == TriggerProgramOutcome::Ignore {
8129 out.ignored_indices.extend(event.op_indices.iter().copied());
8130 break;
8131 }
8132 }
8133 }
8134 Ok(())
8135 }
8136
8137 fn trigger_events_for_staging(
8138 &self,
8139 staging: &[(u64, crate::txn::Staged)],
8140 read_epoch: Epoch,
8141 trigger_stacks: Option<&[Vec<String>]>,
8142 control: Option<&crate::ExecutionControl>,
8143 ) -> Result<Vec<WriteEvent>> {
8144 use crate::txn::Staged;
8145 use std::collections::{HashMap, VecDeque};
8146
8147 let snapshot = Snapshot::at(read_epoch);
8148 let cat = self.catalog.read();
8149 let mut table_names = HashMap::new();
8150 let mut table_schemas = HashMap::new();
8151 for entry in cat
8152 .tables
8153 .iter()
8154 .filter(|entry| matches!(entry.state, TableState::Live))
8155 {
8156 table_names.insert(entry.table_id, entry.name.clone());
8157 table_schemas.insert(entry.table_id, entry.schema.clone());
8158 }
8159 drop(cat);
8160
8161 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
8162 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
8163 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
8164
8165 for (idx, (table_id, staged)) in staging.iter().enumerate() {
8166 commit_prepare_checkpoint(control, idx)?;
8167 let Some(schema) = table_schemas.get(table_id) else {
8168 continue;
8169 };
8170 let Some(pk) = schema.primary_key() else {
8171 continue;
8172 };
8173 match staged {
8174 Staged::Delete(row_id) => {
8175 let handle = self.table_by_id(*table_id)?;
8176 let Some(row) = handle.lock().get(*row_id, snapshot) else {
8177 continue;
8178 };
8179 let Some(pk_value) = row.columns.get(&pk.id) else {
8180 continue;
8181 };
8182 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
8183 delete_by_key
8184 .entry((*table_id, pk_value.encode_key()))
8185 .or_default()
8186 .push_back(idx);
8187 }
8188 Staged::Put(cells) => {
8189 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
8190 put_by_key
8191 .entry((*table_id, value.encode_key()))
8192 .or_default()
8193 .push_back(idx);
8194 }
8195 }
8196 Staged::Update { row_id, .. } => {
8197 let handle = self.table_by_id(*table_id)?;
8198 let row = handle.lock().get(*row_id, snapshot);
8199 if let Some(row) = row {
8200 old_rows.insert(idx, TriggerRowImage::from_row(row));
8201 }
8202 }
8203 Staged::Truncate => {}
8204 }
8205 }
8206
8207 let mut paired_delete = std::collections::HashSet::new();
8208 let mut paired_put = std::collections::HashSet::new();
8209 let mut events = Vec::new();
8210
8211 for (pair_index, (key, deletes)) in delete_by_key.iter_mut().enumerate() {
8212 commit_prepare_checkpoint(control, pair_index)?;
8213 let Some(puts) = put_by_key.get_mut(key) else {
8214 continue;
8215 };
8216 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
8217 paired_delete.insert(delete_idx);
8218 paired_put.insert(put_idx);
8219 let (table_id, _) = &staging[put_idx];
8220 let Some(table_name) = table_names.get(table_id).cloned() else {
8221 continue;
8222 };
8223 let old = old_rows.get(&delete_idx).cloned();
8224 let new = match &staging[put_idx].1 {
8225 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
8226 _ => None,
8227 };
8228 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
8229 events.push(WriteEvent {
8230 table: table_name,
8231 kind: TriggerEvent::Update,
8232 old,
8233 new,
8234 changed_columns,
8235 op_indices: vec![delete_idx, put_idx],
8236 put_idx: Some(put_idx),
8237 trigger_stack: Self::trigger_stack_for_indices(
8238 trigger_stacks,
8239 &[delete_idx, put_idx],
8240 ),
8241 });
8242 }
8243 }
8244
8245 for (idx, (table_id, staged)) in staging.iter().enumerate() {
8246 commit_prepare_checkpoint(control, idx)?;
8247 let Some(table_name) = table_names.get(table_id).cloned() else {
8248 continue;
8249 };
8250 match staged {
8251 Staged::Put(cells) if !paired_put.contains(&idx) => {
8252 let new = Some(TriggerRowImage::from_cells(cells));
8253 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
8254 events.push(WriteEvent {
8255 table: table_name,
8256 kind: TriggerEvent::Insert,
8257 old: None,
8258 new,
8259 changed_columns,
8260 op_indices: vec![idx],
8261 put_idx: Some(idx),
8262 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
8263 });
8264 }
8265 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
8266 let old = match old_rows.get(&idx).cloned() {
8267 Some(old) => Some(old),
8268 None => {
8269 let handle = self.table_by_id(*table_id)?;
8270 let row = handle.lock().get(*row_id, snapshot);
8271 row.map(TriggerRowImage::from_row)
8272 }
8273 };
8274 let Some(old) = old else {
8275 continue;
8276 };
8277 let changed_columns = old.columns.keys().copied().collect();
8278 events.push(WriteEvent {
8279 table: table_name,
8280 kind: TriggerEvent::Delete,
8281 old: Some(old),
8282 new: None,
8283 changed_columns,
8284 op_indices: vec![idx],
8285 put_idx: None,
8286 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
8287 });
8288 }
8289 Staged::Update { new_row: cells, .. } => {
8290 let old = old_rows.get(&idx).cloned();
8291 let new = Some(TriggerRowImage::from_cells(cells));
8292 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
8293 events.push(WriteEvent {
8294 table: table_name,
8295 kind: TriggerEvent::Update,
8296 old,
8297 new,
8298 changed_columns,
8299 op_indices: vec![idx],
8300 put_idx: Some(idx),
8301 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
8302 });
8303 }
8304 Staged::Truncate => {}
8305 _ => {}
8306 }
8307 }
8308
8309 Ok(events)
8310 }
8311
8312 #[allow(clippy::too_many_arguments)]
8313 fn execute_trigger_program(
8314 &self,
8315 trigger: &StoredTrigger,
8316 event: &WriteEvent,
8317 staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
8318 out: &mut TriggerProgramOutput<'_>,
8319 trigger_stack: &[String],
8320 config: &TriggerConfig,
8321 read_epoch: Epoch,
8322 control: Option<&crate::ExecutionControl>,
8323 ) -> Result<TriggerProgramOutcome> {
8324 let mut event = event.clone();
8325 let mut select_results: HashMap<String, Vec<TriggerRowImage>> = HashMap::new();
8326 self.execute_trigger_steps(
8327 trigger,
8328 &trigger.program.steps,
8329 &mut event,
8330 staging,
8331 out,
8332 trigger_stack,
8333 config,
8334 &mut select_results,
8335 0,
8336 None,
8337 read_epoch,
8338 control,
8339 )
8340 }
8341
8342 #[allow(clippy::too_many_arguments)]
8343 fn execute_trigger_steps(
8344 &self,
8345 trigger: &StoredTrigger,
8346 steps: &[TriggerStep],
8347 event: &mut WriteEvent,
8348 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
8349 out: &mut TriggerProgramOutput<'_>,
8350 trigger_stack: &[String],
8351 config: &TriggerConfig,
8352 select_results: &mut HashMap<String, Vec<TriggerRowImage>>,
8353 depth: u32,
8354 selected: Option<&TriggerRowImage>,
8355 read_epoch: Epoch,
8356 control: Option<&crate::ExecutionControl>,
8357 ) -> Result<TriggerProgramOutcome> {
8358 let _ = depth;
8359 for (step_index, step) in steps.iter().enumerate() {
8360 commit_prepare_checkpoint(control, step_index)?;
8361 match step {
8362 TriggerStep::SetNew { cells } => {
8363 if trigger.timing != TriggerTiming::Before {
8364 return Err(MongrelError::InvalidArgument(
8365 "SetNew trigger step is only valid in BEFORE triggers".into(),
8366 ));
8367 }
8368 let put_idx = event.put_idx.ok_or_else(|| {
8369 MongrelError::InvalidArgument(
8370 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
8371 )
8372 })?;
8373 let staging = staging.as_deref_mut().ok_or_else(|| {
8374 MongrelError::InvalidArgument(
8375 "SetNew trigger step requires mutable trigger staging".into(),
8376 )
8377 })?;
8378 let mut update_changed_columns = None;
8379 let row_cells = match staging.get_mut(put_idx).map(|(_, op)| op) {
8380 Some(crate::txn::Staged::Put(cells)) => cells,
8381 Some(crate::txn::Staged::Update {
8382 new_row,
8383 changed_columns,
8384 ..
8385 }) => {
8386 update_changed_columns = Some(changed_columns);
8387 new_row
8388 }
8389 _ => {
8390 return Err(MongrelError::InvalidArgument(
8391 "SetNew trigger step target row is not mutable".into(),
8392 ))
8393 }
8394 };
8395 for (column_id, value) in eval_trigger_cells(cells, event, selected)? {
8396 row_cells.retain(|(id, _)| *id != column_id);
8397 row_cells.push((column_id, value.clone()));
8398 if let Some(changed_columns) = &mut update_changed_columns {
8399 changed_columns.push(column_id);
8400 }
8401 if let Some(new) = &mut event.new {
8402 new.columns.insert(column_id, value);
8403 }
8404 }
8405 row_cells.sort_by_key(|(id, _)| *id);
8406 if let Some(changed_columns) = update_changed_columns {
8407 changed_columns.sort_unstable();
8408 changed_columns.dedup();
8409 }
8410 }
8411 TriggerStep::Insert { table, cells } => {
8412 let cells = eval_trigger_cells(cells, event, selected)?;
8413 if let Ok(table_id) = self.table_id(table) {
8414 out.added.push((table_id, crate::txn::Staged::Put(cells)));
8415 out.added_stacks.push(trigger_stack.to_vec());
8416 } else if self.external_table(table).is_some() {
8417 out.added_external.push(ExternalTriggerWrite::Insert {
8418 table: table.clone(),
8419 cells,
8420 });
8421 } else {
8422 return Err(MongrelError::NotFound(format!(
8423 "trigger {:?} insert target {table:?} not found",
8424 trigger.name
8425 )));
8426 }
8427 }
8428 TriggerStep::UpdateByPk { table, pk, cells } => {
8429 let pk = eval_trigger_value(pk, event, selected)?;
8430 let cells = eval_trigger_cells(cells, event, selected)?;
8431 if self.external_table(table).is_some() {
8432 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
8433 table: table.clone(),
8434 pk,
8435 cells,
8436 });
8437 } else {
8438 let row_id = self
8439 .table(table)?
8440 .lock()
8441 .lookup_pk(&pk.encode_key())
8442 .ok_or_else(|| {
8443 MongrelError::NotFound(format!(
8444 "trigger {:?} update target not found",
8445 trigger.name
8446 ))
8447 })?;
8448 let handle = self.table(table)?;
8449 let snapshot = Snapshot::at(self.epoch.visible());
8450 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
8451 MongrelError::NotFound(format!(
8452 "trigger {:?} update target not visible",
8453 trigger.name
8454 ))
8455 })?;
8456 let mut changed_columns = cells
8457 .iter()
8458 .map(|(column_id, _)| *column_id)
8459 .collect::<Vec<_>>();
8460 changed_columns.sort_unstable();
8461 changed_columns.dedup();
8462 let mut merged = old.columns;
8463 for (column_id, value) in cells {
8464 merged.insert(column_id, value);
8465 }
8466 out.added.push((
8467 self.table_id(table)?,
8468 crate::txn::Staged::Update {
8469 row_id,
8470 new_row: merged.into_iter().collect(),
8471 changed_columns,
8472 },
8473 ));
8474 out.added_stacks.push(trigger_stack.to_vec());
8475 }
8476 }
8477 TriggerStep::DeleteByPk { table, pk } => {
8478 let pk = eval_trigger_value(pk, event, selected)?;
8479 if self.external_table(table).is_some() {
8480 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
8481 table: table.clone(),
8482 pk,
8483 });
8484 } else {
8485 let row_id = self
8486 .table(table)?
8487 .lock()
8488 .lookup_pk(&pk.encode_key())
8489 .ok_or_else(|| {
8490 MongrelError::NotFound(format!(
8491 "trigger {:?} delete target not found",
8492 trigger.name
8493 ))
8494 })?;
8495 out.added
8496 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
8497 out.added_stacks.push(trigger_stack.to_vec());
8498 }
8499 }
8500 TriggerStep::Select {
8501 id,
8502 table,
8503 conditions,
8504 } => {
8505 let schema = self.table(table)?.lock().schema().clone();
8506 let snapshot = Snapshot::at(read_epoch);
8507 let handle = self.table(table)?;
8508 let rows = match control {
8509 Some(control) => {
8510 handle.lock().visible_rows_controlled(snapshot, control)?
8511 }
8512 None => handle.lock().visible_rows(snapshot)?,
8513 };
8514 let mut matched = Vec::new();
8515 for (row_index, row) in rows.into_iter().enumerate() {
8516 commit_prepare_checkpoint(control, row_index)?;
8517 let image = TriggerRowImage::from_row(row);
8518 let passes = conditions
8519 .iter()
8520 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
8521 .collect::<Result<Vec<_>>>()?
8522 .into_iter()
8523 .all(|b| b);
8524 if passes {
8525 matched.push(image);
8526 }
8527 }
8528 if let Some(pk) = schema.primary_key() {
8529 matched.sort_by(|a, b| {
8530 let av = a.columns.get(&pk.id).unwrap_or(&Value::Null);
8531 let bv = b.columns.get(&pk.id).unwrap_or(&Value::Null);
8532 value_order(av, bv).unwrap_or(std::cmp::Ordering::Equal)
8533 });
8534 }
8535 select_results.insert(id.clone(), matched);
8536 }
8537 TriggerStep::Foreach { id, steps } => {
8538 let rows = select_results.get(id).ok_or_else(|| {
8539 MongrelError::InvalidArgument(format!(
8540 "trigger {:?} foreach references unknown select id {id:?}",
8541 trigger.name
8542 ))
8543 })?;
8544 if rows.len() > config.max_loop_iterations as usize {
8545 return Err(MongrelError::InvalidArgument(format!(
8546 "trigger {:?} foreach exceeded max_loop_iterations ({})",
8547 trigger.name, config.max_loop_iterations
8548 )));
8549 }
8550 for (row_index, row) in rows.clone().into_iter().enumerate() {
8551 commit_prepare_checkpoint(control, row_index)?;
8552 let result = self.execute_trigger_steps(
8553 trigger,
8554 steps,
8555 event,
8556 staging.as_deref_mut(),
8557 out,
8558 trigger_stack,
8559 config,
8560 select_results,
8561 depth + 1,
8562 Some(&row),
8563 read_epoch,
8564 control,
8565 )?;
8566 if result == TriggerProgramOutcome::Ignore {
8567 return Ok(TriggerProgramOutcome::Ignore);
8568 }
8569 }
8570 }
8571 TriggerStep::DeleteWhere { table, conditions } => {
8572 let schema = self.table(table)?.lock().schema().clone();
8573 let snapshot = Snapshot::at(read_epoch);
8574 let handle = self.table(table)?;
8575 let rows = match control {
8576 Some(control) => {
8577 handle.lock().visible_rows_controlled(snapshot, control)?
8578 }
8579 None => handle.lock().visible_rows(snapshot)?,
8580 };
8581 let table_id = self.table_id(table)?;
8582 let mut to_delete = Vec::new();
8583 for (row_index, row) in rows.into_iter().enumerate() {
8584 commit_prepare_checkpoint(control, row_index)?;
8585 let image = TriggerRowImage::from_row(row.clone());
8586 let passes = conditions
8587 .iter()
8588 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
8589 .collect::<Result<Vec<_>>>()?
8590 .into_iter()
8591 .all(|b| b);
8592 if passes {
8593 to_delete.push((table_id, row.row_id));
8594 }
8595 }
8596 for (row_index, (table_id, row_id)) in to_delete.into_iter().enumerate() {
8597 commit_prepare_checkpoint(control, row_index)?;
8598 out.added
8599 .push((table_id, crate::txn::Staged::Delete(row_id)));
8600 out.added_stacks.push(trigger_stack.to_vec());
8601 }
8602 }
8603 TriggerStep::UpdateWhere {
8604 table,
8605 conditions,
8606 cells,
8607 } => {
8608 let schema = self.table(table)?.lock().schema().clone();
8609 let snapshot = Snapshot::at(read_epoch);
8610 let handle = self.table(table)?;
8611 let rows = match control {
8612 Some(control) => {
8613 handle.lock().visible_rows_controlled(snapshot, control)?
8614 }
8615 None => handle.lock().visible_rows(snapshot)?,
8616 };
8617 let table_id = self.table_id(table)?;
8618 let mut changed_columns =
8619 cells.iter().map(|cell| cell.column_id).collect::<Vec<_>>();
8620 changed_columns.sort_unstable();
8621 changed_columns.dedup();
8622 let mut to_update = Vec::new();
8623 for (row_index, row) in rows.into_iter().enumerate() {
8624 commit_prepare_checkpoint(control, row_index)?;
8625 let image = TriggerRowImage::from_row(row.clone());
8626 let passes = conditions
8627 .iter()
8628 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
8629 .collect::<Result<Vec<_>>>()?
8630 .into_iter()
8631 .all(|b| b);
8632 if passes {
8633 let new_cells = cells
8634 .iter()
8635 .map(|cell| {
8636 Ok((
8637 cell.column_id,
8638 eval_trigger_value(&cell.value, event, Some(&image))?,
8639 ))
8640 })
8641 .collect::<Result<Vec<_>>>()?;
8642 let mut merged = row.columns.clone();
8643 for (column_id, value) in new_cells {
8644 merged.insert(column_id, value);
8645 }
8646 to_update.push((table_id, row.row_id, merged));
8647 }
8648 }
8649 for (row_index, (table_id, row_id, merged)) in to_update.into_iter().enumerate()
8650 {
8651 commit_prepare_checkpoint(control, row_index)?;
8652 out.added.push((
8653 table_id,
8654 crate::txn::Staged::Update {
8655 row_id,
8656 new_row: merged.into_iter().collect(),
8657 changed_columns: changed_columns.clone(),
8658 },
8659 ));
8660 out.added_stacks.push(trigger_stack.to_vec());
8661 }
8662 }
8663 TriggerStep::Raise { action, message } => match action {
8664 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
8665 TriggerRaiseAction::Abort
8666 | TriggerRaiseAction::Fail
8667 | TriggerRaiseAction::Rollback => {
8668 let message = eval_trigger_value(message, event, selected)?;
8669 return Err(MongrelError::TriggerValidation(format!(
8670 "trigger {:?} raised: {}; trigger stack: {}",
8671 trigger.name,
8672 trigger_message(message),
8673 Self::format_trigger_stack(trigger_stack)
8674 )));
8675 }
8676 },
8677 }
8678 }
8679 Ok(TriggerProgramOutcome::Continue)
8680 }
8681
8682 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
8683 let Some(stacks) = stacks else {
8684 return Vec::new();
8685 };
8686 let mut out = Vec::new();
8687 for idx in indices {
8688 let Some(stack) = stacks.get(*idx) else {
8689 continue;
8690 };
8691 for name in stack {
8692 if !out.iter().any(|existing| existing == name) {
8693 out.push(name.clone());
8694 }
8695 }
8696 }
8697 out
8698 }
8699
8700 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
8701 let mut out = stack.to_vec();
8702 out.push(trigger_name.to_string());
8703 out
8704 }
8705
8706 fn format_trigger_stack(stack: &[String]) -> String {
8707 if stack.is_empty() {
8708 "<root>".into()
8709 } else {
8710 stack.join(" -> ")
8711 }
8712 }
8713
8714 fn validate_constraints(
8729 &self,
8730 staging: &mut Vec<(u64, crate::txn::Staged)>,
8731 read_epoch: Epoch,
8732 control: Option<&crate::ExecutionControl>,
8733 ) -> Result<()> {
8734 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
8735 use crate::memtable::Row;
8736 use crate::txn::Staged;
8737 use std::collections::HashSet;
8738
8739 commit_prepare_checkpoint(control, 0)?;
8740 let snapshot = Snapshot::at(read_epoch);
8741 let cat = self.catalog.read();
8742
8743 let live: Vec<(u64, &str, &crate::schema::Schema)> = cat
8745 .tables
8746 .iter()
8747 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
8748 .map(|e| (e.table_id, e.name.as_str(), &e.schema))
8749 .collect();
8750
8751 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
8753 if !any_constraints {
8754 return Ok(());
8755 }
8756
8757 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
8759 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
8760 if let Some(r) = rows_cache.get(&table_id) {
8761 return Ok(r.clone());
8762 }
8763 let handle = self.table_by_id(table_id)?;
8764 let rows = match control {
8765 Some(control) => handle.lock().visible_rows_controlled(snapshot, control)?,
8766 None => handle.lock().visible_rows(snapshot)?,
8767 };
8768 rows_cache.insert(table_id, rows.clone());
8769 Ok(rows)
8770 };
8771
8772 let mut processed_updates = HashSet::new();
8777 type PendingUpdate = (usize, u64, crate::rowid::RowId, Vec<(u16, Value)>);
8778 let mut update_pass = 0_usize;
8779 loop {
8780 commit_prepare_checkpoint(control, update_pass)?;
8781 update_pass += 1;
8782 let updates: Vec<PendingUpdate> = staging
8783 .iter()
8784 .enumerate()
8785 .filter_map(|(index, (table_id, op))| match op {
8786 Staged::Update {
8787 row_id,
8788 new_row: cells,
8789 ..
8790 } if !processed_updates.contains(&index) => {
8791 Some((index, *table_id, *row_id, cells.clone()))
8792 }
8793 _ => None,
8794 })
8795 .collect();
8796 if updates.is_empty() {
8797 break;
8798 }
8799 let mut new_ops = Vec::new();
8800 for (update_index, (index, table_id, row_id, new_cells)) in
8801 updates.into_iter().enumerate()
8802 {
8803 commit_prepare_checkpoint(control, update_index)?;
8804 processed_updates.insert(index);
8805 let Some(tname) = live
8806 .iter()
8807 .find(|(id, _, _)| *id == table_id)
8808 .map(|(_, name, _)| *name)
8809 else {
8810 continue;
8811 };
8812 let Some(old_row) = self.table_by_id(table_id)?.lock().get(row_id, snapshot) else {
8813 continue;
8814 };
8815 let new_map: HashMap<u16, Value> = new_cells.iter().cloned().collect();
8816 for (child_id, _child_name, child_schema) in &live {
8817 for fk in &child_schema.constraints.foreign_keys {
8818 if fk.ref_table != tname {
8819 continue;
8820 }
8821 let Some(old_key) = encode_composite_key(&fk.ref_columns, &old_row.columns)
8822 else {
8823 continue;
8824 };
8825 if encode_composite_key(&fk.ref_columns, &new_map).as_deref()
8826 == Some(old_key.as_slice())
8827 {
8828 continue;
8829 }
8830 if fk.on_update == FkAction::Restrict {
8831 continue;
8832 }
8833 let child_rows = load_rows(*child_id)?;
8834 for (child_index, child) in child_rows.into_iter().enumerate() {
8835 commit_prepare_checkpoint(control, child_index)?;
8836 if encode_composite_key(&fk.columns, &child.columns).as_deref()
8837 != Some(old_key.as_slice())
8838 {
8839 continue;
8840 }
8841 if staging.iter().any(|(id, op)| {
8842 *id == *child_id
8843 && matches!(op, Staged::Delete(id) if *id == child.row_id)
8844 }) {
8845 continue;
8846 }
8847 let mut cells: Vec<(u16, Value)> = child
8848 .columns
8849 .iter()
8850 .map(|(column_id, value)| (*column_id, value.clone()))
8851 .collect();
8852 for (child_column, parent_column) in
8853 fk.columns.iter().zip(&fk.ref_columns)
8854 {
8855 cells.retain(|(column_id, _)| column_id != child_column);
8856 let value = match fk.on_update {
8857 FkAction::Cascade => {
8858 new_map.get(parent_column).cloned().unwrap_or(Value::Null)
8859 }
8860 FkAction::SetNull => Value::Null,
8861 FkAction::Restrict => {
8862 return Err(MongrelError::Other(
8863 "restricted foreign-key update reached cascade preparation"
8864 .into(),
8865 ));
8866 }
8867 };
8868 cells.push((*child_column, value));
8869 }
8870 cells.sort_by_key(|(column_id, _)| *column_id);
8871 if let Some(existing_index) = staging.iter().position(|(id, op)| {
8872 *id == *child_id
8873 && matches!(op, Staged::Update { row_id, .. } if *row_id == child.row_id)
8874 }) {
8875 if let Staged::Update {
8876 new_row: existing,
8877 changed_columns,
8878 ..
8879 } = &mut staging[existing_index].1 {
8880 changed_columns.extend(fk.columns.iter().copied());
8881 changed_columns.sort_unstable();
8882 changed_columns.dedup();
8883 if *existing != cells {
8884 *existing = cells;
8885 processed_updates.remove(&existing_index);
8886 }
8887 }
8888 } else {
8889 new_ops.push((
8890 *child_id,
8891 Staged::Update {
8892 row_id: child.row_id,
8893 new_row: cells,
8894 changed_columns: fk.columns.clone(),
8895 },
8896 ));
8897 }
8898 }
8899 }
8900 }
8901 }
8902 staging.extend(new_ops);
8903 }
8904
8905 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
8910 let mut cascade_pass = 0_usize;
8911 loop {
8912 commit_prepare_checkpoint(control, cascade_pass)?;
8913 cascade_pass += 1;
8914 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
8915 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
8916 .iter()
8917 .filter_map(|(t, op)| match op {
8918 Staged::Delete(rid) => Some((*t, *rid)),
8919 _ => None,
8920 })
8921 .collect();
8922 for (delete_index, (table_id, rid)) in deletes.into_iter().enumerate() {
8923 commit_prepare_checkpoint(control, delete_index)?;
8924 if !cascaded.insert((table_id, rid.0)) {
8925 continue;
8926 }
8927 let Some(tname) = live
8928 .iter()
8929 .find(|(t, _, _)| *t == table_id)
8930 .map(|(_, n, _)| *n)
8931 else {
8932 continue;
8933 };
8934 let parent_handle = self.table_by_id(table_id)?;
8935 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
8936 continue;
8937 };
8938 for (child_id, _child_name, child_schema) in &live {
8939 for fk in &child_schema.constraints.foreign_keys {
8940 if fk.ref_table != tname {
8941 continue;
8942 }
8943 let Some(parent_key) =
8944 encode_composite_key(&fk.ref_columns, &parent_row.columns)
8945 else {
8946 continue;
8947 };
8948 let key_preserved = staging.iter().any(|(t, op)| {
8957 if *t != table_id {
8958 return false;
8959 }
8960 let Staged::Put(cells) = op else {
8961 return false;
8962 };
8963 let map: HashMap<u16, crate::memtable::Value> =
8964 cells.iter().cloned().collect();
8965 encode_composite_key(&fk.ref_columns, &map).as_deref()
8966 == Some(parent_key.as_slice())
8967 });
8968 if key_preserved {
8969 continue;
8970 }
8971 match fk.on_delete {
8972 FkAction::Restrict => continue,
8973 FkAction::Cascade => {
8974 let child_rows = load_rows(*child_id)?;
8975 for (child_index, cr) in child_rows.iter().enumerate() {
8976 commit_prepare_checkpoint(control, child_index)?;
8977 if !cascaded.contains(&(*child_id, cr.row_id.0))
8978 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
8979 == Some(parent_key.as_slice())
8980 {
8981 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
8982 }
8983 }
8984 }
8985 FkAction::SetNull => {
8986 let child_rows = load_rows(*child_id)?;
8987 for (child_index, cr) in child_rows.iter().enumerate() {
8988 commit_prepare_checkpoint(control, child_index)?;
8989 if !cascaded.contains(&(*child_id, cr.row_id.0))
8990 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
8991 == Some(parent_key.as_slice())
8992 {
8993 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
8996 .columns
8997 .iter()
8998 .map(|(k, v)| (*k, v.clone()))
8999 .collect();
9000 for cid in &fk.columns {
9001 cells.retain(|(k, _)| k != cid);
9002 cells.push((*cid, crate::memtable::Value::Null));
9003 }
9004 new_ops.push((
9005 *child_id,
9006 Staged::Update {
9007 row_id: cr.row_id,
9008 new_row: cells,
9009 changed_columns: fk.columns.clone(),
9010 },
9011 ));
9012 }
9013 }
9014 }
9015 }
9016 }
9017 }
9018 }
9019 if new_ops.is_empty() {
9020 break;
9021 }
9022 staging.extend(new_ops);
9023 }
9024
9025 let staged_deletes: HashSet<(u64, u64)> = staging
9029 .iter()
9030 .filter_map(|(t, op)| match op {
9031 Staged::Delete(rid) | Staged::Update { row_id: rid, .. } => Some((*t, rid.0)),
9032 _ => None,
9033 })
9034 .collect();
9035
9036 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
9038
9039 for (operation_index, (table_id, op)) in staging.iter().enumerate() {
9041 commit_prepare_checkpoint(control, operation_index)?;
9042 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id).copied()
9043 else {
9044 continue;
9045 };
9046 let cells_map: HashMap<u16, crate::memtable::Value>;
9047 match op {
9048 Staged::Put(cells) | Staged::Update { new_row: cells, .. } => {
9049 cells_map = cells.iter().cloned().collect();
9050
9051 if !schema.constraints.checks.is_empty() {
9053 validate_checks(&schema.constraints.checks, &cells_map)?;
9054 }
9055
9056 for uc in &schema.constraints.uniques {
9058 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
9059 continue; };
9061 let marker = (*table_id, uc.id, key.clone());
9062 if !seen_unique.insert(marker) {
9063 return Err(MongrelError::Conflict(format!(
9064 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
9065 uc.name
9066 )));
9067 }
9068 let rows = load_rows(*table_id)?;
9069 for (row_index, r) in rows.iter().enumerate() {
9070 commit_prepare_checkpoint(control, row_index)?;
9071 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
9074 continue;
9075 }
9076 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
9077 if theirs == key {
9078 return Err(MongrelError::Conflict(format!(
9079 "UNIQUE constraint '{}' on table '{tname}' violated",
9080 uc.name
9081 )));
9082 }
9083 }
9084 }
9085 }
9086
9087 for fk in &schema.constraints.foreign_keys {
9089 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
9090 continue; };
9092 let Some(parent_id) = cat
9093 .tables
9094 .iter()
9095 .find(|t| t.name == fk.ref_table)
9096 .map(|t| t.table_id)
9097 else {
9098 return Err(MongrelError::InvalidArgument(format!(
9099 "FOREIGN KEY '{}' references unknown table '{}'",
9100 fk.name, fk.ref_table
9101 )));
9102 };
9103 let parent_rows = load_rows(parent_id)?;
9104 let mut found = false;
9105 for (row_index, r) in parent_rows.iter().enumerate() {
9106 commit_prepare_checkpoint(control, row_index)?;
9107 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
9108 continue;
9109 }
9110 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
9111 if pkey == child_key {
9112 found = true;
9113 break;
9114 }
9115 }
9116 }
9117 if !found {
9124 for (staged_index, (st_table, st_op)) in staging.iter().enumerate() {
9125 commit_prepare_checkpoint(control, staged_index)?;
9126 if *st_table != parent_id {
9127 continue;
9128 }
9129 if let Staged::Put(pcells)
9130 | Staged::Update {
9131 new_row: pcells, ..
9132 } = st_op
9133 {
9134 let pmap: HashMap<u16, crate::memtable::Value> =
9135 pcells.iter().cloned().collect();
9136 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &pmap)
9137 {
9138 if pkey == child_key {
9139 found = true;
9140 break;
9141 }
9142 }
9143 }
9144 }
9145 }
9146 if !found {
9147 return Err(MongrelError::Conflict(format!(
9148 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
9149 fk.name, fk.ref_table
9150 )));
9151 }
9152 }
9153
9154 if let Staged::Update { row_id, .. } = op {
9159 let parent_handle = self.table_by_id(*table_id)?;
9160 let Some(old_parent) = parent_handle.lock().get(*row_id, snapshot) else {
9161 continue;
9162 };
9163 for (child_id, child_name, child_schema) in &live {
9164 for fk in &child_schema.constraints.foreign_keys {
9165 if fk.ref_table != tname || fk.on_update != FkAction::Restrict {
9166 continue;
9167 }
9168 let Some(old_key) =
9169 encode_composite_key(&fk.ref_columns, &old_parent.columns)
9170 else {
9171 continue;
9172 };
9173 if encode_composite_key(&fk.ref_columns, &cells_map).as_deref()
9174 == Some(old_key.as_slice())
9175 {
9176 continue;
9177 }
9178 for (child_index, child) in
9179 load_rows(*child_id)?.into_iter().enumerate()
9180 {
9181 commit_prepare_checkpoint(control, child_index)?;
9182 if encode_composite_key(&fk.columns, &child.columns).as_deref()
9183 != Some(old_key.as_slice())
9184 {
9185 continue;
9186 }
9187 let replacement = staging.iter().find_map(|(id, op)| {
9188 if *id != *child_id {
9189 return None;
9190 }
9191 match op {
9192 Staged::Delete(id) if *id == child.row_id => Some(None),
9193 Staged::Update {
9194 row_id,
9195 new_row: cells,
9196 ..
9197 } if *row_id == child.row_id => {
9198 let map: HashMap<u16, Value> =
9199 cells.iter().cloned().collect();
9200 Some(encode_composite_key(&fk.columns, &map))
9201 }
9202 _ => None,
9203 }
9204 });
9205 if replacement.is_some_and(|key| {
9206 key.as_deref() != Some(old_key.as_slice())
9207 }) {
9208 continue;
9209 }
9210 return Err(MongrelError::Conflict(format!(
9211 "FOREIGN KEY '{}' on table '{child_name}' restricts update (parent key referenced)",
9212 fk.name
9213 )));
9214 }
9215 }
9216 }
9217 }
9218 }
9219 Staged::Delete(rid) => {
9220 let parent_handle = self.table_by_id(*table_id)?;
9224 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
9225 continue;
9226 };
9227 for (child_id, child_name, child_schema) in &live {
9228 for fk in &child_schema.constraints.foreign_keys {
9229 if fk.ref_table != tname || fk.on_delete != FkAction::Restrict {
9230 continue;
9231 }
9232 let Some(parent_key) =
9233 encode_composite_key(&fk.ref_columns, &parent_row.columns)
9234 else {
9235 continue;
9236 };
9237 let child_rows = load_rows(*child_id)?;
9238 for (row_index, r) in child_rows.iter().enumerate() {
9239 commit_prepare_checkpoint(control, row_index)?;
9240 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
9243 continue;
9244 }
9245 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
9246 if ck == parent_key {
9247 return Err(MongrelError::Conflict(format!(
9248 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
9249 fk.name
9250 )));
9251 }
9252 }
9253 }
9254 }
9255 }
9256 }
9257 Staged::Truncate => {
9258 for (child_id, child_name, child_schema) in &live {
9262 for fk in &child_schema.constraints.foreign_keys {
9263 if fk.ref_table != tname {
9264 continue;
9265 }
9266 let child_rows = load_rows(*child_id)?;
9267 if child_rows
9268 .iter()
9269 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
9270 {
9271 return Err(MongrelError::Conflict(format!(
9272 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
9273 fk.name
9274 )));
9275 }
9276 }
9277 }
9278 }
9279 }
9280 }
9281 Ok(())
9282 }
9283
9284 fn validate_write_permissions(
9285 &self,
9286 staging: &[(u64, crate::txn::Staged)],
9287 principal: Option<&crate::auth::Principal>,
9288 control: Option<&crate::ExecutionControl>,
9289 ) -> Result<()> {
9290 commit_prepare_checkpoint(control, 0)?;
9291 if principal.is_none() && !self.auth_state.require_auth() {
9292 return Ok(());
9293 }
9294 let principal = principal.ok_or(MongrelError::AuthRequired)?;
9295 let needs = summarize_write_permissions(staging);
9296 let catalog = self.catalog.read();
9297
9298 if needs.values().any(|need| need.truncate) {
9299 self.require_for(Some(principal), &crate::auth::Permission::Admin)?;
9300 }
9301 for (need_index, (table_id, need)) in needs.into_iter().enumerate() {
9302 commit_prepare_checkpoint(control, need_index)?;
9303 let entry = catalog
9304 .tables
9305 .iter()
9306 .find(|entry| {
9307 entry.table_id == table_id
9308 && matches!(entry.state, TableState::Live | TableState::Building { .. })
9309 })
9310 .ok_or_else(|| {
9311 MongrelError::NotFound(format!(
9312 "live table {table_id} not found during write validation"
9313 ))
9314 })?;
9315 if matches!(entry.state, TableState::Building { .. }) {
9316 self.require_for(Some(principal), &crate::auth::Permission::Ddl)?;
9317 continue;
9318 }
9319 if need.insert {
9320 Self::require_columns_for_principal(
9321 &entry.name,
9322 &entry.schema,
9323 crate::auth::ColumnOperation::Insert,
9324 &need.insert_columns,
9325 principal,
9326 )?;
9327 }
9328 if need.update {
9329 Self::require_columns_for_principal(
9330 &entry.name,
9331 &entry.schema,
9332 crate::auth::ColumnOperation::Update,
9333 &need.update_columns,
9334 principal,
9335 )?;
9336 }
9337 if need.delete {
9338 self.require_for(
9339 Some(principal),
9340 &crate::auth::Permission::Delete {
9341 table: entry.name.clone(),
9342 },
9343 )?;
9344 }
9345 }
9346 Ok(())
9347 }
9348
9349 fn validate_security_writes(
9350 &self,
9351 staging: &[(u64, crate::txn::Staged)],
9352 read_epoch: Epoch,
9353 explicit_principal: Option<&crate::auth::Principal>,
9354 control: Option<&crate::ExecutionControl>,
9355 ) -> Result<()> {
9356 commit_prepare_checkpoint(control, 0)?;
9357 use crate::security::PolicyCommand;
9358 use crate::txn::Staged;
9359
9360 let catalog = self.catalog.read();
9361 if catalog.security.rls_tables.is_empty() {
9362 return Ok(());
9363 }
9364 let security = catalog.security.clone();
9365 let table_names = catalog
9366 .tables
9367 .iter()
9368 .filter(|entry| matches!(entry.state, TableState::Live))
9369 .map(|entry| (entry.table_id, entry.name.clone()))
9370 .collect::<HashMap<_, _>>();
9371 drop(catalog);
9372 if !staging.iter().any(|(table_id, _)| {
9373 table_names
9374 .get(table_id)
9375 .is_some_and(|table| security.rls_enabled(table))
9376 }) {
9377 return Ok(());
9378 }
9379 let principal = explicit_principal.ok_or(MongrelError::AuthRequired)?;
9380
9381 for (operation_index, (table_id, operation)) in staging.iter().enumerate() {
9382 commit_prepare_checkpoint(control, operation_index)?;
9383 let Some(table) = table_names.get(table_id) else {
9384 continue;
9385 };
9386 if !security.rls_enabled(table) || principal.is_admin {
9387 continue;
9388 }
9389 let denied = |command| MongrelError::PermissionDenied {
9390 required: match command {
9391 PolicyCommand::Insert => crate::auth::Permission::Insert {
9392 table: table.clone(),
9393 },
9394 PolicyCommand::Update => crate::auth::Permission::Update {
9395 table: table.clone(),
9396 },
9397 PolicyCommand::Delete | PolicyCommand::All | PolicyCommand::Select => {
9398 crate::auth::Permission::Delete {
9399 table: table.clone(),
9400 }
9401 }
9402 },
9403 principal: principal.username.clone(),
9404 };
9405 match operation {
9406 Staged::Put(cells) => {
9407 let mut row = crate::memtable::Row::new(RowId(0), Epoch(read_epoch.0));
9408 row.columns.extend(cells.iter().cloned());
9409 if !security.row_allowed(table, PolicyCommand::Insert, &row, principal, true) {
9410 return Err(denied(PolicyCommand::Insert));
9411 }
9412 }
9413 Staged::Update {
9414 row_id,
9415 new_row: cells,
9416 ..
9417 } => {
9418 let old = self
9419 .table_by_id(*table_id)?
9420 .lock()
9421 .get(*row_id, Snapshot::at(read_epoch))
9422 .ok_or_else(|| {
9423 MongrelError::NotFound(format!("row {} not found", row_id.0))
9424 })?;
9425 if !security.row_allowed(table, PolicyCommand::Update, &old, principal, false) {
9426 return Err(denied(PolicyCommand::Update));
9427 }
9428 let mut new = crate::memtable::Row::new(*row_id, Epoch(read_epoch.0));
9429 new.columns.extend(cells.iter().cloned());
9430 if !security.row_allowed(table, PolicyCommand::Update, &new, principal, true) {
9431 return Err(denied(PolicyCommand::Update));
9432 }
9433 }
9434 Staged::Delete(row_id) => {
9435 let old = self
9436 .table_by_id(*table_id)?
9437 .lock()
9438 .get(*row_id, Snapshot::at(read_epoch))
9439 .ok_or_else(|| {
9440 MongrelError::NotFound(format!("row {} not found", row_id.0))
9441 })?;
9442 if !security.row_allowed(table, PolicyCommand::Delete, &old, principal, false) {
9443 return Err(denied(PolicyCommand::Delete));
9444 }
9445 }
9446 Staged::Truncate => return Err(denied(PolicyCommand::Delete)),
9447 }
9448 }
9449 Ok(())
9450 }
9451
9452 #[allow(clippy::too_many_arguments)]
9459 pub(crate) fn commit_transaction_with_external_states(
9460 &self,
9461 txn_id: u64,
9462 read_epoch: Epoch,
9463 staging: Vec<(u64, crate::txn::Staged)>,
9464 external_states: Vec<(String, Vec<u8>)>,
9465 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
9466 security_principal: Option<crate::auth::Principal>,
9467 principal_catalog_bound: bool,
9468 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9469 ) -> Result<(Epoch, Vec<RowId>)> {
9470 self.commit_transaction_with_external_states_inner(
9471 txn_id,
9472 read_epoch,
9473 staging,
9474 external_states,
9475 materialized_view_updates,
9476 security_principal,
9477 principal_catalog_bound,
9478 external_trigger_bridge,
9479 None,
9480 None,
9481 )
9482 }
9483
9484 #[allow(clippy::too_many_arguments)]
9485 pub(crate) fn commit_transaction_with_external_states_controlled(
9486 &self,
9487 txn_id: u64,
9488 read_epoch: Epoch,
9489 staging: Vec<(u64, crate::txn::Staged)>,
9490 external_states: Vec<(String, Vec<u8>)>,
9491 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
9492 security_principal: Option<crate::auth::Principal>,
9493 principal_catalog_bound: bool,
9494 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9495 control: &crate::ExecutionControl,
9496 before_commit: &mut dyn FnMut() -> Result<()>,
9497 ) -> Result<(Epoch, Vec<RowId>)> {
9498 self.commit_transaction_with_external_states_inner(
9499 txn_id,
9500 read_epoch,
9501 staging,
9502 external_states,
9503 materialized_view_updates,
9504 security_principal,
9505 principal_catalog_bound,
9506 external_trigger_bridge,
9507 Some(control),
9508 Some(before_commit),
9509 )
9510 }
9511
9512 #[allow(clippy::too_many_arguments)]
9513 fn commit_transaction_with_external_states_inner(
9514 &self,
9515 txn_id: u64,
9516 read_epoch: Epoch,
9517 mut staging: Vec<(u64, crate::txn::Staged)>,
9518 external_states: Vec<(String, Vec<u8>)>,
9519 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
9520 mut security_principal: Option<crate::auth::Principal>,
9521 principal_catalog_bound: bool,
9522 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9523 control: Option<&crate::ExecutionControl>,
9524 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
9525 ) -> Result<(Epoch, Vec<RowId>)> {
9526 use crate::memtable::Row;
9527 use crate::txn::{Staged, StagedOp, WriteKey};
9528 use crate::wal::Op;
9529 use std::collections::hash_map::DefaultHasher;
9530 use std::hash::{Hash, Hasher};
9531 use std::sync::atomic::Ordering;
9532
9533 if txn_id == crate::wal::SYSTEM_TXN_ID {
9534 return Err(MongrelError::Full(
9535 "per-open transaction id namespace exhausted; reopen the database".into(),
9536 ));
9537 }
9538 if self.read_only {
9539 return Err(MongrelError::ReadOnlyReplica);
9540 }
9541 commit_prepare_checkpoint(control, 0)?;
9542 let observed_security_version = self.security_coordinator.version.load(Ordering::Acquire);
9543 self.refresh_security_catalog_if_stale(observed_security_version)?;
9544 let trigger_binding = trigger_catalog_binding(&self.catalog.read());
9545 if self.auth_state.require_auth() && security_principal.is_none() {
9546 return Err(MongrelError::AuthRequired);
9547 }
9548 {
9549 let catalog = self.catalog.read();
9550 if catalog.require_auth
9551 || principal_catalog_bound
9552 || security_principal
9553 .as_ref()
9554 .is_some_and(|principal| principal.user_id != 0)
9555 {
9556 let principal = security_principal
9557 .as_ref()
9558 .ok_or(MongrelError::AuthRequired)?;
9559 security_principal =
9560 Self::resolve_bound_principal_from_catalog(&catalog, principal);
9561 if security_principal.is_none() {
9562 return Err(MongrelError::AuthRequired);
9563 }
9564 }
9565 }
9566 let _replication_guard = self.replication_barrier.read();
9567 if self.poisoned.load(Ordering::Relaxed) {
9568 return Err(MongrelError::Other(
9569 "database poisoned by fsync error".into(),
9570 ));
9571 }
9572 let mut external_states = dedup_external_states(external_states);
9573 if !external_states.is_empty() {
9574 let cat = self.catalog.read();
9575 for (name, _) in &external_states {
9576 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
9577 return Err(MongrelError::NotFound(format!(
9578 "external table {name:?} not found"
9579 )));
9580 }
9581 }
9582 }
9583 let prepared_materialized_views = {
9584 let mut deduplicated = HashMap::new();
9585 for (definition_index, definition) in materialized_view_updates.into_iter().enumerate()
9586 {
9587 commit_prepare_checkpoint(control, definition_index)?;
9588 if definition.name.is_empty() || definition.query.trim().is_empty() {
9589 return Err(MongrelError::InvalidArgument(
9590 "materialized view name and query must not be empty".into(),
9591 ));
9592 }
9593 deduplicated.insert(definition.name.clone(), definition);
9594 }
9595 let catalog = self.catalog.read();
9596 let mut prepared = Vec::with_capacity(deduplicated.len());
9597 for (definition_index, definition) in deduplicated.into_values().enumerate() {
9598 commit_prepare_checkpoint(control, definition_index)?;
9599 let table_id = catalog
9600 .live(&definition.name)
9601 .ok_or_else(|| {
9602 MongrelError::NotFound(format!(
9603 "materialized view table {:?} not found",
9604 definition.name
9605 ))
9606 })?
9607 .table_id;
9608 prepared.push((table_id, definition));
9609 }
9610 prepared.sort_by(|left, right| left.1.name.cmp(&right.1.name));
9611 prepared
9612 };
9613
9614 self.fill_auto_increment_for_staging(&mut staging, control)?;
9617 self.expand_table_triggers(
9618 &mut staging,
9619 read_epoch,
9620 external_trigger_bridge,
9621 &mut external_states,
9622 control,
9623 )?;
9624 self.fill_auto_increment_for_staging(&mut staging, control)?;
9625 external_states = dedup_external_states(external_states);
9626 let expected_external_generations = {
9627 let catalog = self.catalog.read();
9628 let mut generations = HashMap::with_capacity(external_states.len());
9629 for (name, _) in &external_states {
9630 let entry = catalog
9631 .external_tables
9632 .iter()
9633 .find(|entry| entry.name == *name)
9634 .ok_or_else(|| {
9635 MongrelError::NotFound(format!("external table {name:?} not found"))
9636 })?;
9637 generations.insert(name.clone(), entry.created_epoch);
9638 }
9639 generations
9640 };
9641
9642 self.validate_constraints(&mut staging, read_epoch, control)?;
9647 self.validate_write_permissions(&staging, security_principal.as_ref(), control)?;
9648 self.validate_security_writes(&staging, read_epoch, security_principal.as_ref(), control)?;
9649 let mut normalized = Vec::with_capacity(staging.len() * 2);
9650 for (staged_index, (table_id, op)) in staging.into_iter().enumerate() {
9651 commit_prepare_checkpoint(control, staged_index)?;
9652 match op {
9653 crate::txn::Staged::Update {
9654 row_id,
9655 new_row: cells,
9656 ..
9657 } => {
9658 normalized.push((table_id, crate::txn::Staged::Delete(row_id)));
9659 normalized.push((table_id, crate::txn::Staged::Put(cells)));
9660 }
9661 op => normalized.push((table_id, op)),
9662 }
9663 }
9664 staging = normalized;
9665 let has_changes = !staging.is_empty()
9666 || !external_states.is_empty()
9667 || !prepared_materialized_views.is_empty();
9668 let truncated_tables: HashSet<u64> = staging
9669 .iter()
9670 .filter_map(|(table_id, op)| matches!(op, Staged::Truncate).then_some(*table_id))
9671 .collect();
9672
9673 let write_keys = {
9674 let cat = self.catalog.read();
9675 let mut keys: Vec<WriteKey> = Vec::new();
9676 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
9677 commit_prepare_checkpoint(control, staged_index)?;
9678 match staged {
9679 Staged::Put(cells) => {
9680 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
9681 for col in &entry.schema.columns {
9682 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
9683 if let Some((_, val)) =
9684 cells.iter().find(|(id, _)| *id == col.id)
9685 {
9686 let mut h = DefaultHasher::new();
9687 val.encode_key().hash(&mut h);
9688 keys.push(WriteKey::Unique {
9689 table_id: *table_id,
9690 index_id: 0,
9691 key_hash: h.finish(),
9692 });
9693 }
9694 }
9695 }
9696 for uc in &entry.schema.constraints.uniques {
9703 if let Some(key_bytes) = crate::constraint::encode_composite_key(
9704 &uc.columns,
9705 &cells.iter().cloned().collect(),
9706 ) {
9707 let mut h = DefaultHasher::new();
9708 key_bytes.hash(&mut h);
9709 keys.push(WriteKey::Unique {
9710 table_id: *table_id,
9711 index_id: uc.id | 0x8000,
9712 key_hash: h.finish(),
9713 });
9714 }
9715 }
9716 }
9717 }
9718 Staged::Delete(rid) => keys.push(WriteKey::Row {
9719 table_id: *table_id,
9720 row_id: rid.0,
9721 }),
9722 Staged::Truncate => keys.push(WriteKey::Table {
9723 table_id: *table_id,
9724 }),
9725 Staged::Update { .. } => {
9726 return Err(MongrelError::Other(
9727 "transaction contains an unnormalized update during preparation".into(),
9728 ));
9729 }
9730 }
9731 }
9732 for (external_index, (name, _)) in external_states.iter().enumerate() {
9733 commit_prepare_checkpoint(control, external_index)?;
9734 let mut h = DefaultHasher::new();
9735 name.hash(&mut h);
9736 keys.push(WriteKey::Unique {
9737 table_id: EXTERNAL_TABLE_ID,
9738 index_id: 0,
9739 key_hash: h.finish(),
9740 });
9741 }
9742 keys
9743 };
9744
9745 let min_active = self.active_txns.min_read_epoch();
9747 if min_active < u64::MAX {
9748 self.conflicts.prune_below(Epoch(min_active));
9749 }
9750
9751 if self.conflicts.conflicts(&write_keys, read_epoch) {
9755 return Err(MongrelError::Conflict(
9756 "write-write conflict (pre-validate, first-committer-wins)".into(),
9757 ));
9758 }
9759 let pre_validate_version = self.conflicts.version();
9760
9761 let mut spilled: Vec<SpilledRun> = Vec::new();
9765 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
9766 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
9770 {
9771 let mut table_bytes: HashMap<u64, u64> = HashMap::new();
9772 let mut put_indexes: HashMap<u64, Vec<usize>> = HashMap::new();
9773 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
9774 commit_prepare_checkpoint(control, staged_index)?;
9775 if let Staged::Put(cells) = staged {
9776 let bytes = cells.iter().fold(32_u64, |bytes, (_, value)| {
9777 bytes.saturating_add(value.estimated_bytes())
9778 });
9779 let table_bytes = table_bytes.entry(*table_id).or_default();
9780 *table_bytes = table_bytes.saturating_add(bytes);
9781 put_indexes.entry(*table_id).or_default().push(staged_index);
9782 }
9783 }
9784 let tables = self.tables.read();
9785 for (table_index, (&table_id, &bytes)) in table_bytes.iter().enumerate() {
9786 commit_prepare_checkpoint(control, table_index)?;
9787 if bytes
9788 <= self
9789 .spill_threshold
9790 .load(std::sync::atomic::Ordering::Relaxed)
9791 {
9792 continue;
9793 }
9794 let Some(handle) = tables.get(&table_id) else {
9795 continue;
9796 };
9797 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
9798 let mut t = handle.lock();
9799 let tdir = t.table_dir().to_path_buf();
9800 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
9801 std::fs::create_dir_all(&txn_dir)?;
9802 let run_id = t.alloc_run_id()? as u128;
9803 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
9804 let final_path = t.run_path(run_id as u64);
9805
9806 let mut rows: Vec<Row> = Vec::new();
9807 for (put_index, staged_index) in put_indexes[&table_id].iter().enumerate() {
9808 commit_prepare_checkpoint(control, put_index)?;
9809 let Staged::Put(cells) = &mut staging[*staged_index].1 else {
9810 return Err(MongrelError::Other(
9811 "transaction put index no longer references a put".into(),
9812 ));
9813 };
9814 t.validate_cells_not_null(cells)?;
9815 let row_id = t.alloc_row_id()?;
9816 let mut row = Row::new(row_id, Epoch(0));
9817 row.columns.extend(std::mem::take(cells));
9818 rows.push(row);
9819 }
9820 let schema = t.schema_ref().clone();
9821 let kek = t.kek_ref().cloned();
9822 let specs = t.indexable_column_specs();
9823 drop(t);
9824
9825 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
9826 .uniform_epoch(true);
9827 if let Some(ref kek) = kek {
9828 writer = writer.with_encryption(kek.as_ref(), specs);
9829 }
9830 commit_prepare_checkpoint(control, 0)?;
9831 let header = writer.write(&pending_path, &rows)?;
9832 commit_prepare_checkpoint(control, 0)?;
9833 let row_count = header.row_count;
9834 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
9835 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
9836
9837 spilled.push(SpilledRun {
9838 table_id,
9839 run_id,
9840 pending_path,
9841 final_path,
9842 rows,
9843 row_count,
9844 min_rid,
9845 max_rid,
9846 content_hash: header.content_hash,
9847 });
9848 spilled_tables.insert(table_id);
9849 }
9850 }
9851
9852 if spill_guard.is_some() {
9854 if let Some(hook) = self.spill_hook.lock().as_ref() {
9855 hook();
9856 }
9857 }
9858
9859 let mut prebuilt: Vec<Option<Row>> = std::iter::repeat_with(|| None)
9870 .take(staging.len())
9871 .collect();
9872 let mut delete_images: Vec<Option<Row>> = std::iter::repeat_with(|| None)
9873 .take(staging.len())
9874 .collect();
9875 {
9876 let mut indexes_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
9877 for (index, (table_id, staged)) in staging.iter().enumerate() {
9878 commit_prepare_checkpoint(control, index)?;
9879 if matches!(staged, Staged::Delete(_))
9880 || matches!(staged, Staged::Put(_) if !spilled_tables.contains(table_id))
9881 {
9882 indexes_by_table.entry(*table_id).or_default().push(index);
9883 }
9884 }
9885 let tables = self.tables.read();
9886 for (table_index, (table_id, indexes)) in indexes_by_table.into_iter().enumerate() {
9887 commit_prepare_checkpoint(control, table_index)?;
9888 let handle = tables.get(&table_id).ok_or_else(|| {
9889 MongrelError::NotFound(format!("table {table_id} not mounted"))
9890 })?;
9891 #[cfg(test)]
9892 PREBUILD_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
9893 let mut t = handle.lock();
9894 for (prepare_index, index) in indexes.into_iter().enumerate() {
9895 commit_prepare_checkpoint(control, prepare_index)?;
9896 match &staging[index].1 {
9897 Staged::Put(cells) if !spilled_tables.contains(&table_id) => {
9898 t.validate_cells_not_null(cells)?;
9899 let mut row = Row::new(t.alloc_row_id()?, Epoch(0));
9900 for (column, value) in cells {
9901 row.columns.insert(*column, value.clone());
9902 }
9903 prebuilt[index] = Some(row);
9904 }
9905 Staged::Delete(row_id) => {
9906 delete_images[index] = t.get(*row_id, Snapshot::at(read_epoch));
9907 }
9908 Staged::Put(_) | Staged::Truncate => {}
9909 Staged::Update { .. } => {
9910 return Err(MongrelError::Other(
9911 "transaction contains an unnormalized update during row preparation"
9912 .into(),
9913 ));
9914 }
9915 }
9916 }
9917 }
9918 }
9919
9920 let prepared_table_handles = {
9924 let table_ids: HashSet<u64> = staging.iter().map(|(table_id, _)| *table_id).collect();
9925 let put_table_ids: HashSet<u64> = staging
9926 .iter()
9927 .filter_map(|(table_id, staged)| {
9928 matches!(staged, Staged::Put(_)).then_some(*table_id)
9929 })
9930 .collect();
9931 let tables = self.tables.read();
9932 let mut handles = HashMap::with_capacity(table_ids.len());
9933 for (table_index, table_id) in table_ids.into_iter().enumerate() {
9934 commit_prepare_checkpoint(control, table_index)?;
9935 let handle = tables.get(&table_id).ok_or_else(|| {
9936 MongrelError::NotFound(format!("table {table_id} not mounted"))
9937 })?;
9938 if put_table_ids.contains(&table_id) {
9939 match control {
9940 Some(control) => {
9941 handle.lock().prepare_durable_publish_controlled(control)?
9942 }
9943 None => handle.lock().prepare_durable_publish()?,
9944 }
9945 }
9946 handles.insert(table_id, handle.clone());
9947 }
9948 handles
9949 };
9950
9951 let mut prepared_run_links = PreparedRunLinks::prepare(&spilled)?;
9955
9956 let mut spilled_row_ids: HashMap<u64, VecDeque<RowId>> = spilled
9957 .iter()
9958 .map(|run| {
9959 (
9960 run.table_id,
9961 run.rows.iter().map(|row| row.row_id).collect(),
9962 )
9963 })
9964 .collect();
9965 let committed_row_ids = staging
9966 .iter()
9967 .enumerate()
9968 .filter_map(|(index, (table_id, staged))| {
9969 if !matches!(staged, Staged::Put(_)) {
9970 return None;
9971 }
9972 prebuilt[index].as_ref().map(|row| row.row_id).or_else(|| {
9973 spilled_row_ids
9974 .get_mut(table_id)
9975 .and_then(VecDeque::pop_front)
9976 })
9977 })
9978 .collect();
9979
9980 let mut prepared_external = Vec::with_capacity(external_states.len());
9981 for (external_index, (name, state)) in external_states.iter().enumerate() {
9982 commit_prepare_checkpoint(control, external_index)?;
9983 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
9984 prepared_external.push((name.clone(), state.clone(), pending));
9985 }
9986
9987 let added_runs: Vec<crate::wal::AddedRun> = spilled
9989 .iter()
9990 .map(|s| crate::wal::AddedRun {
9991 table_id: s.table_id,
9992 run_id: s.run_id,
9993 row_count: s.row_count,
9994 level: 0,
9995 min_row_id: s.min_rid,
9996 max_row_id: s.max_rid,
9997 content_hash: s.content_hash,
9998 })
9999 .collect();
10000 if let Some(hook) = self.catalog_commit_hook.lock().as_ref() {
10001 hook();
10002 }
10003 let security_guard = self.security_coordinator.gate.read();
10007 if self.security_coordinator.version.load(Ordering::Acquire) != observed_security_version {
10008 return Err(MongrelError::Conflict(
10009 "security policy changed during write".into(),
10010 ));
10011 }
10012 if spill_guard.is_some() {
10013 if let Some(hook) = self.security_commit_hook.lock().as_ref() {
10014 hook();
10015 }
10016 }
10017 let commit_guard = self.commit_lock.lock();
10018 let catalog_generation_result = (|| {
10019 {
10020 let catalog = self.catalog.read();
10021 for table_id in prepared_table_handles.keys() {
10022 let is_current = catalog.tables.iter().any(|entry| {
10023 entry.table_id == *table_id
10024 && matches!(entry.state, TableState::Live | TableState::Building { .. })
10025 });
10026 if !is_current {
10027 return Err(MongrelError::Conflict(format!(
10028 "table {table_id} changed during transaction preparation"
10029 )));
10030 }
10031 }
10032 for (name, created_epoch) in &expected_external_generations {
10033 let current = catalog
10034 .external_tables
10035 .iter()
10036 .find(|entry| entry.name == *name)
10037 .map(|entry| entry.created_epoch);
10038 if current != Some(*created_epoch) {
10039 return Err(MongrelError::Conflict(format!(
10040 "external table {name:?} changed during transaction preparation"
10041 )));
10042 }
10043 }
10044 for (table_id, definition) in &prepared_materialized_views {
10045 let current = catalog.live(&definition.name).map(|entry| entry.table_id);
10046 if current != Some(*table_id) {
10047 return Err(MongrelError::Conflict(format!(
10048 "materialized view {:?} changed during transaction preparation",
10049 definition.name
10050 )));
10051 }
10052 }
10053 if trigger_catalog_binding(&catalog) != trigger_binding {
10054 return Err(MongrelError::Conflict(
10055 "trigger or referenced table generation changed during transaction preparation"
10056 .into(),
10057 ));
10058 }
10059 }
10060 let tables = self.tables.read();
10061 for (table_id, prepared) in &prepared_table_handles {
10062 if !tables
10063 .get(table_id)
10064 .is_some_and(|current| current.ptr_eq(prepared))
10065 {
10066 return Err(MongrelError::Conflict(format!(
10067 "table {table_id} mount changed during transaction preparation"
10068 )));
10069 }
10070 }
10071 Ok(())
10072 })();
10073 if let Err(error) = catalog_generation_result {
10074 drop(commit_guard);
10075 for (_, _, pending) in &prepared_external {
10076 let _ = std::fs::remove_file(pending);
10077 }
10078 return Err(error);
10079 }
10080 let new_epoch = self.epoch.assigned().next();
10084 let mut spilled_wal_bytes = 0;
10085 let mut spilled_wal_records = Vec::<(u64, Op)>::new();
10086 let spill_prepare = (|| {
10087 for run in &mut spilled {
10088 for row in &mut run.rows {
10089 row.committed_epoch = new_epoch;
10090 }
10091 for rows in encode_spilled_row_chunks(
10092 &run.rows,
10093 &mut spilled_wal_bytes,
10094 SPILLED_WAL_TOTAL_MAX_BYTES,
10095 control,
10096 )? {
10097 spilled_wal_records.push((
10098 run.table_id,
10099 Op::SpilledRows {
10100 table_id: run.table_id,
10101 rows,
10102 },
10103 ));
10104 }
10105 }
10106 Result::<()>::Ok(())
10107 })();
10108 if let Err(error) = spill_prepare {
10109 for (_, _, pending) in &prepared_external {
10110 let _ = std::fs::remove_file(pending);
10111 }
10112 return Err(error);
10113 }
10114 let (new_epoch, mut _epoch_guard, applies, committed_materialized_views, commit_seq) = {
10115 let mut wal = self.shared_wal.lock();
10116
10117 if self.conflicts.version() != pre_validate_version
10122 && self.conflicts.conflicts(&write_keys, read_epoch)
10123 {
10124 drop(wal);
10127 for (_, _, pending) in &prepared_external {
10128 let _ = std::fs::remove_file(pending);
10129 }
10130 return Err(MongrelError::Conflict(
10131 "write-write conflict (sequencer delta re-check)".into(),
10132 ));
10133 }
10134
10135 if let Some(control) = control {
10136 if let Err(error) = control.checkpoint() {
10137 drop(wal);
10138 for (_, _, pending) in &prepared_external {
10139 let _ = std::fs::remove_file(pending);
10140 }
10141 return Err(error);
10142 }
10143 }
10144 let mut applies = Vec::<TableApplyBatch>::new();
10145 let mut apply_indexes = HashMap::<u64, usize>::new();
10146 let mut committed_materialized_views = Vec::new();
10147 let mut wal_records = spilled_wal_records;
10148
10149 let mut index = 0;
10150 while index < staging.len() {
10151 let table_id = staging[index].0;
10152 let handle = prepared_table_handles
10153 .get(&table_id)
10154 .cloned()
10155 .ok_or_else(|| {
10156 MongrelError::NotFound(format!("table {table_id} not prepared"))
10157 })?;
10158 let batch_index = *apply_indexes.entry(table_id).or_insert_with(|| {
10159 let index = applies.len();
10160 applies.push(TableApplyBatch {
10161 table_id,
10162 handle,
10163 ops: Vec::new(),
10164 });
10165 index
10166 });
10167
10168 if spilled_tables.contains(&table_id) && matches!(&staging[index].1, Staged::Put(_))
10171 {
10172 index += 1;
10173 continue;
10174 }
10175
10176 match &staging[index].1 {
10177 Staged::Put(_) => {
10178 let mut rows = Vec::new();
10179 while index < staging.len()
10180 && staging[index].0 == table_id
10181 && matches!(&staging[index].1, Staged::Put(_))
10182 {
10183 let mut row = prebuilt[index].take().ok_or_else(|| {
10184 MongrelError::Other(
10185 "transaction prepare lost a prebuilt put row".into(),
10186 )
10187 })?;
10188 row.committed_epoch = new_epoch;
10189 rows.push(row);
10190 index += 1;
10191 }
10192 let payload = bincode::serialize(&rows)
10193 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
10194 wal_records.push((
10195 table_id,
10196 Op::Put {
10197 table_id,
10198 rows: payload,
10199 },
10200 ));
10201 applies[batch_index].ops.push(StagedOp::Put(rows));
10202 }
10203 Staged::Delete(_) => {
10204 let mut row_ids = Vec::new();
10205 while index < staging.len()
10206 && staging[index].0 == table_id
10207 && matches!(&staging[index].1, Staged::Delete(_))
10208 {
10209 let Staged::Delete(row_id) = &staging[index].1 else {
10210 return Err(MongrelError::Other(
10211 "transaction delete batch changed during WAL preparation"
10212 .into(),
10213 ));
10214 };
10215 if let Some(before) = &delete_images[index] {
10216 wal_records.push((
10217 table_id,
10218 Op::BeforeImage {
10219 table_id,
10220 row_id: *row_id,
10221 row: bincode::serialize(before).map_err(|error| {
10222 MongrelError::Other(format!(
10223 "before-image serialize: {error}"
10224 ))
10225 })?,
10226 },
10227 ));
10228 }
10229 row_ids.push(*row_id);
10230 index += 1;
10231 }
10232 wal_records.push((
10233 table_id,
10234 Op::Delete {
10235 table_id,
10236 row_ids: row_ids.clone(),
10237 },
10238 ));
10239 applies[batch_index].ops.push(StagedOp::Delete(row_ids));
10240 }
10241 Staged::Truncate => {
10242 wal_records.push((table_id, Op::TruncateTable { table_id }));
10243 applies[batch_index].ops.push(StagedOp::Truncate);
10244 index += 1;
10245 }
10246 Staged::Update { .. } => {
10247 return Err(MongrelError::Other(
10248 "transaction contains an unnormalized update at the sequencer".into(),
10249 ));
10250 }
10251 }
10252 }
10253
10254 for (name, state, _) in &prepared_external {
10255 wal_records.push((
10256 EXTERNAL_TABLE_ID,
10257 Op::ExternalTableState {
10258 name: name.clone(),
10259 state: state.clone(),
10260 },
10261 ));
10262 }
10263
10264 for (table_id, definition) in &prepared_materialized_views {
10265 let mut definition = definition.clone();
10266 definition.last_refresh_epoch = new_epoch.0;
10267 wal_records.push((
10268 *table_id,
10269 Op::Ddl(crate::wal::DdlOp::SetMaterializedView {
10270 name: definition.name.clone(),
10271 definition_json: crate::wal::DdlOp::encode_materialized_view(&definition)?,
10272 }),
10273 ));
10274 committed_materialized_views.push(definition);
10275 }
10276 if !committed_materialized_views.is_empty() {
10277 let mut next_catalog = self.catalog.read().clone();
10278 for definition in &committed_materialized_views {
10279 if let Some(existing) = next_catalog
10280 .materialized_views
10281 .iter_mut()
10282 .find(|existing| existing.name == definition.name)
10283 {
10284 *existing = definition.clone();
10285 } else {
10286 next_catalog.materialized_views.push(definition.clone());
10287 }
10288 }
10289 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
10290 wal_records.push((
10291 WAL_TABLE_ID,
10292 Op::Ddl(crate::wal::DdlOp::CatalogSnapshot {
10293 catalog_json: crate::wal::DdlOp::encode_catalog(&next_catalog)?,
10294 }),
10295 ));
10296 }
10297
10298 if let Some(control) = control {
10299 if let Err(error) = control.checkpoint() {
10300 drop(wal);
10301 for (_, _, pending) in &prepared_external {
10302 let _ = std::fs::remove_file(pending);
10303 }
10304 return Err(error);
10305 }
10306 }
10307 if let Some(before_commit) = before_commit.as_mut() {
10308 if let Err(error) = before_commit() {
10309 drop(wal);
10310 for (_, _, pending) in &prepared_external {
10311 let _ = std::fs::remove_file(pending);
10312 }
10313 return Err(error);
10314 }
10315 }
10316
10317 let assigned_epoch = self.epoch.bump_assigned();
10318 let _epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
10319 if assigned_epoch != new_epoch {
10320 for (_, _, pending) in &prepared_external {
10321 let _ = std::fs::remove_file(pending);
10322 }
10323 return Err(MongrelError::Conflict(
10324 "commit epoch changed while sequencer lock was held".into(),
10325 ));
10326 }
10327
10328 prepared_run_links.disarm();
10332
10333 let append: Result<u64> = (|| {
10334 for (table_id, op) in wal_records {
10335 wal.append(txn_id, table_id, op)?;
10336 }
10337 wal.append_commit(txn_id, new_epoch, &added_runs)
10338 })();
10339 let commit_seq =
10340 append.map_err(|error| self.commit_outcome_unknown(new_epoch, error))?;
10341
10342 self.conflicts.record(&write_keys, new_epoch);
10347 (
10348 new_epoch,
10349 _epoch_guard,
10350 applies,
10351 committed_materialized_views,
10352 commit_seq,
10353 )
10354 };
10355 drop(commit_guard);
10356
10357 self.await_durable_commit(commit_seq, new_epoch)?;
10359 drop(security_guard);
10360
10361 let publish_result: Result<()> = {
10363 let mut first_error = None;
10364 let mut spilled_by_table: HashMap<u64, Vec<&SpilledRun>> = HashMap::new();
10365 for run in &spilled {
10366 spilled_by_table.entry(run.table_id).or_default().push(run);
10367 }
10368 let mut modified_tables = Vec::with_capacity(applies.len());
10369 for batch in applies {
10372 #[cfg(test)]
10373 PUBLISH_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
10374 let mut t = batch.handle.lock();
10375 for op in batch.ops {
10376 match op {
10377 StagedOp::Put(rows) => t.apply_put_rows_prepared(rows),
10378 StagedOp::Delete(row_ids) => {
10379 for row_id in row_ids {
10380 t.apply_delete(row_id, new_epoch);
10381 }
10382 }
10383 StagedOp::Truncate => t.apply_truncate(new_epoch),
10384 }
10385 }
10386 if let Some(runs) = spilled_by_table.remove(&batch.table_id) {
10387 for run in runs {
10388 t.link_run(crate::manifest::RunRef {
10389 run_id: run.run_id,
10390 level: 0,
10391 epoch_created: new_epoch.0,
10392 row_count: run.row_count,
10393 });
10394 t.apply_run_metadata_prepared(&run.rows)?;
10395 if truncated_tables.contains(&batch.table_id) {
10396 t.set_flushed_epoch(new_epoch);
10401 }
10402 }
10403 }
10404 t.invalidate_pending_cache();
10405 drop(t);
10406 modified_tables.push(batch.handle);
10407 }
10408
10409 for handle in modified_tables {
10413 #[cfg(test)]
10414 COMMIT_MANIFEST_WRITES.with(|count| count.set(count.get() + 1));
10415 if let Err(error) = handle.lock().persist_manifest(new_epoch) {
10416 first_error.get_or_insert(error);
10417 }
10418 }
10419 for (name, _, pending) in &prepared_external {
10420 if let Err(error) = publish_external_state_file(&self.root, name, pending) {
10421 first_error.get_or_insert(error);
10422 }
10423 }
10424 if !committed_materialized_views.is_empty() {
10425 let mut next_catalog = self.catalog.read().clone();
10426 for definition in committed_materialized_views {
10427 if let Some(existing) = next_catalog
10428 .materialized_views
10429 .iter_mut()
10430 .find(|existing| existing.name == definition.name)
10431 {
10432 *existing = definition;
10433 } else {
10434 next_catalog.materialized_views.push(definition);
10435 }
10436 }
10437 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
10438 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
10439 first_error.get_or_insert(error);
10440 }
10441 }
10442 match first_error {
10443 Some(error) => Err(error),
10444 None => Ok(()),
10445 }
10446 };
10447
10448 if has_changes {
10449 let _ = self.change_wake.send(());
10450 }
10451 self.finish_durable_publish(new_epoch, &mut _epoch_guard, publish_result)?;
10452 Ok((new_epoch, committed_row_ids))
10453 }
10454
10455 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
10458 let e = self.epoch.visible();
10459 let g = self.snapshots.register(e);
10460 (Snapshot::at(e), g)
10461 }
10462
10463 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
10466 let e = self.epoch.visible();
10467 let g = self.snapshots.register_owned(e);
10468 (Snapshot::at(e), g)
10469 }
10470
10471 pub fn set_history_retention_epochs(&self, epochs: u64) -> Result<()> {
10476 let _guard = self.ddl_lock.lock();
10477 let current = self.epoch.visible();
10478 let (old_epochs, old_start) = self.snapshots.history_config();
10479 let earliest_already_guaranteed = if old_epochs == 0 {
10480 current
10481 } else {
10482 Epoch(old_start.0.max(current.0.saturating_sub(old_epochs)))
10483 };
10484 let start = if epochs == 0 {
10485 current
10486 } else {
10487 earliest_already_guaranteed
10488 };
10489 let published = std::cell::Cell::new(false);
10490 let result = write_history_retention(&self.root, epochs, start, || {
10491 self.snapshots.configure_history(epochs, start);
10492 published.set(true);
10493 });
10494 match result {
10495 Err(error) if published.get() => Err(MongrelError::CommitOutcomeUnknown {
10496 epoch: current.0,
10497 message: format!("history-retention publication was not durable: {error}"),
10498 }),
10499 result => result,
10500 }
10501 }
10502
10503 pub fn history_retention_epochs(&self) -> u64 {
10504 self.snapshots.history_config().0
10505 }
10506
10507 pub fn earliest_retained_epoch(&self) -> Epoch {
10508 let current = self.epoch.visible();
10509 self.snapshots.history_floor(current).unwrap_or(current)
10510 }
10511
10512 pub fn snapshot_at_owned(&self, epoch: Epoch) -> Result<(Snapshot, OwnedSnapshotGuard)> {
10515 let current = self.epoch.visible();
10516 if epoch > current {
10517 return Err(MongrelError::InvalidArgument(format!(
10518 "epoch {} is in the future; current epoch is {}",
10519 epoch.0, current.0
10520 )));
10521 }
10522 let earliest = self.earliest_retained_epoch();
10523 if epoch < earliest {
10524 return Err(MongrelError::InvalidArgument(format!(
10525 "epoch {} is no longer retained; earliest available epoch is {}",
10526 epoch.0, earliest.0
10527 )));
10528 }
10529 let guard = self.snapshots.register_owned(epoch);
10530 Ok((Snapshot::at(epoch), guard))
10531 }
10532
10533 pub fn table_names(&self) -> Vec<String> {
10535 self.catalog
10536 .read()
10537 .tables
10538 .iter()
10539 .filter(|t| matches!(t.state, TableState::Live))
10540 .map(|t| t.name.clone())
10541 .collect()
10542 }
10543
10544 pub fn close(&self) -> Result<()> {
10550 for name in self.table_names() {
10551 if let Ok(handle) = self.table(&name) {
10552 if let Err(e) = handle.lock().close() {
10553 eprintln!("[close] flush failed for {name}: {e}");
10554 }
10555 }
10556 }
10557 Ok(())
10558 }
10559
10560 pub fn compact(&self) -> Result<(usize, usize)> {
10567 self.require(&crate::auth::Permission::Ddl)?;
10568 let mut compacted = 0;
10569 let mut skipped = 0;
10570 for name in self.table_names() {
10571 let Ok(handle) = self.table(&name) else {
10572 continue;
10573 };
10574 {
10575 let mut t = handle.lock();
10576 let before = t.run_count();
10577 if before < 2 && !t.should_compact() {
10578 skipped += 1;
10579 continue;
10580 }
10581 match t.compact() {
10582 Ok(()) => {
10583 let after = t.run_count();
10584 compacted += 1;
10585 eprintln!("[compact] {name}: {before} -> {after} runs");
10586 }
10587 Err(e) => {
10588 eprintln!("[compact] {name}: compaction failed: {e}");
10589 skipped += 1;
10590 }
10591 }
10592 }
10593 }
10594 Ok((compacted, skipped))
10595 }
10596
10597 pub fn compact_table(&self, name: &str) -> Result<bool> {
10600 self.require(&crate::auth::Permission::Ddl)?;
10601 let handle = self.table(name)?;
10602 let mut t = handle.lock();
10603 let before = t.run_count();
10604 if before < 2 {
10605 return Ok(false);
10606 }
10607 t.compact()?;
10608 Ok(t.run_count() < before)
10609 }
10610
10611 pub fn table(&self, name: &str) -> Result<TableHandle> {
10613 self.ensure_owner_process()?;
10614 let cat = self.catalog.read();
10615 let entry = cat
10616 .live(name)
10617 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
10618 let id = entry.table_id;
10619 drop(cat);
10620 self.tables
10621 .read()
10622 .get(&id)
10623 .cloned()
10624 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
10625 }
10626
10627 pub fn has_ttl_tables(&self) -> bool {
10630 self.tables
10631 .read()
10632 .values()
10633 .any(|table| table.lock().ttl().is_some())
10634 }
10635
10636 pub(crate) fn table_by_id(&self, id: u64) -> Result<TableHandle> {
10639 self.tables
10640 .read()
10641 .get(&id)
10642 .cloned()
10643 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
10644 }
10645
10646 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
10652 if name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
10653 return Err(MongrelError::InvalidArgument(format!(
10654 "table names beginning with {CTAS_BUILD_TABLE_PREFIX:?} are reserved"
10655 )));
10656 }
10657 self.create_table_with_state(name, schema, TableState::Live)
10658 }
10659
10660 #[doc(hidden)]
10662 pub fn create_building_table(
10663 &self,
10664 build_name: &str,
10665 intended_name: &str,
10666 query_id: &str,
10667 schema: Schema,
10668 ) -> Result<u64> {
10669 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10670 || intended_name.is_empty()
10671 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10672 || query_id.is_empty()
10673 {
10674 return Err(MongrelError::InvalidArgument(
10675 "invalid CTAS building-table identity".into(),
10676 ));
10677 }
10678 self.create_table_with_state(
10679 build_name,
10680 schema,
10681 TableState::Building {
10682 intended_name: intended_name.to_string(),
10683 query_id: query_id.to_string(),
10684 created_at_unix_nanos: current_unix_nanos(),
10685 replaces_table_id: None,
10686 },
10687 )
10688 }
10689
10690 #[doc(hidden)]
10693 pub fn create_rebuilding_table(
10694 &self,
10695 build_name: &str,
10696 intended_name: &str,
10697 query_id: &str,
10698 schema: Schema,
10699 ) -> Result<u64> {
10700 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10701 || intended_name.is_empty()
10702 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10703 || query_id.is_empty()
10704 {
10705 return Err(MongrelError::InvalidArgument(
10706 "invalid rebuilding-table identity".into(),
10707 ));
10708 }
10709 let replaces_table_id = self
10710 .catalog
10711 .read()
10712 .live(intended_name)
10713 .ok_or_else(|| MongrelError::NotFound(format!("table {intended_name:?} not found")))?
10714 .table_id;
10715 self.create_table_with_state(
10716 build_name,
10717 schema,
10718 TableState::Building {
10719 intended_name: intended_name.to_string(),
10720 query_id: query_id.to_string(),
10721 created_at_unix_nanos: current_unix_nanos(),
10722 replaces_table_id: Some(replaces_table_id),
10723 },
10724 )
10725 }
10726
10727 fn create_table_with_state(
10728 &self,
10729 name: &str,
10730 schema: Schema,
10731 state: TableState,
10732 ) -> Result<u64> {
10733 use crate::wal::DdlOp;
10734 use std::sync::atomic::Ordering;
10735
10736 self.require(&crate::auth::Permission::Ddl)?;
10737 if self.poisoned.load(Ordering::Relaxed) {
10738 return Err(MongrelError::Other(
10739 "database poisoned by fsync error".into(),
10740 ));
10741 }
10742
10743 let _g = self.ddl_lock.lock();
10744 let _security_write = self.security_write()?;
10745 self.require(&crate::auth::Permission::Ddl)?;
10746 {
10747 let cat = self.catalog.read();
10748 match &state {
10749 TableState::Live => {
10750 if cat.live(name).is_some() || cat.building_for(name).is_some() {
10751 return Err(MongrelError::InvalidArgument(format!(
10752 "table {name:?} already exists or is being built"
10753 )));
10754 }
10755 }
10756 TableState::Building {
10757 intended_name,
10758 replaces_table_id,
10759 ..
10760 } => {
10761 let target_matches = match replaces_table_id {
10762 Some(table_id) => cat
10763 .live(intended_name)
10764 .is_some_and(|entry| entry.table_id == *table_id),
10765 None => cat.live(intended_name).is_none(),
10766 };
10767 if !target_matches || cat.building_for(intended_name).is_some() {
10768 return Err(MongrelError::InvalidArgument(format!(
10769 "table {intended_name:?} changed or is already being built"
10770 )));
10771 }
10772 if cat.building(name).is_some() {
10773 return Err(MongrelError::InvalidArgument(format!(
10774 "building table {name:?} already exists"
10775 )));
10776 }
10777 }
10778 TableState::Dropped { .. } => {
10779 return Err(MongrelError::InvalidArgument(
10780 "cannot create a dropped table".into(),
10781 ));
10782 }
10783 }
10784 }
10785
10786 let commit_lock = Arc::clone(&self.commit_lock);
10789 let _c = commit_lock.lock();
10790 let table_id = {
10791 let mut cat = self.catalog.write();
10792 let id = cat.next_table_id;
10793 cat.next_table_id = id
10794 .checked_add(1)
10795 .ok_or_else(|| MongrelError::InvalidArgument("table id space exhausted".into()))?;
10796 Result::<u64>::Ok(id)
10797 }?;
10798 let epoch = self.epoch.bump_assigned();
10799 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
10800 let txn_id = self.alloc_txn_id()?;
10801
10802 let mut schema = schema;
10806 schema.schema_id = table_id;
10807 schema.validate_auto_increment()?;
10814 schema.validate_defaults()?;
10815 schema.validate_ai()?;
10816 for index in &schema.indexes {
10817 index.validate_options()?;
10818 }
10819 for constraint in &schema.constraints.checks {
10820 constraint.expr.validate()?;
10821 }
10822
10823 let table_relative = Path::new(TABLES_DIR).join(table_id.to_string());
10826 let canonical_tdir = self.root.join(&table_relative);
10827 let table_root = Arc::new(
10828 self.durable_root
10829 .create_directory_all_pinned(&table_relative)?,
10830 );
10831 let tdir = table_root.io_path()?;
10832 let mut pending_table_dir = PendingTableDir::new(canonical_tdir);
10833 let ctx = SharedCtx {
10834 root_guard: Some(table_root),
10835 epoch: Arc::clone(&self.epoch),
10836 page_cache: Arc::clone(&self.page_cache),
10837 decoded_cache: Arc::clone(&self.decoded_cache),
10838 snapshots: Arc::clone(&self.snapshots),
10839 kek: self.kek.clone(),
10840 commit_lock: Arc::clone(&self.commit_lock),
10841 shared: Some(crate::engine::SharedWalCtx {
10842 wal: Arc::clone(&self.shared_wal),
10843 group: Arc::clone(&self.group),
10844 poisoned: Arc::clone(&self.poisoned),
10845 txn_ids: Arc::clone(&self.next_txn_id),
10846 change_wake: self.change_wake.clone(),
10847 }),
10848 table_name: Some(name.to_string()),
10849 auth: self.table_auth_checker(),
10850 read_only: self.read_only,
10851 };
10852 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
10853
10854 let schema_json = DdlOp::encode_schema(&schema)?;
10857 let ddl = match &state {
10858 TableState::Live => DdlOp::CreateTable {
10859 table_id,
10860 name: name.to_string(),
10861 schema_json,
10862 },
10863 TableState::Building {
10864 intended_name,
10865 query_id,
10866 created_at_unix_nanos,
10867 replaces_table_id,
10868 } => match replaces_table_id {
10869 Some(replaces_table_id) => DdlOp::CreateRebuildingTable {
10870 table_id,
10871 build_name: name.to_string(),
10872 intended_name: intended_name.clone(),
10873 query_id: query_id.clone(),
10874 created_at_unix_nanos: *created_at_unix_nanos,
10875 replaces_table_id: *replaces_table_id,
10876 schema_json,
10877 },
10878 None => DdlOp::CreateBuildingTable {
10879 table_id,
10880 build_name: name.to_string(),
10881 intended_name: intended_name.clone(),
10882 query_id: query_id.clone(),
10883 created_at_unix_nanos: *created_at_unix_nanos,
10884 schema_json,
10885 },
10886 },
10887 TableState::Dropped { .. } => {
10888 return Err(MongrelError::InvalidArgument(
10889 "cannot create a table in dropped state".into(),
10890 ));
10891 }
10892 };
10893 let mut next_catalog = self.catalog.read().clone();
10894 next_catalog.tables.push(CatalogEntry {
10895 table_id,
10896 name: name.to_string(),
10897 schema: schema.clone(),
10898 state: state.clone(),
10899 created_epoch: epoch.0,
10900 });
10901 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
10902 let commit_seq = {
10903 let mut wal = self.shared_wal.lock();
10904 let append: Result<u64> = (|| {
10905 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
10906 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
10907 wal.append_commit(txn_id, epoch, &[])
10908 })();
10909 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
10910 };
10911 self.await_durable_commit(commit_seq, epoch)?;
10912 pending_table_dir.disarm();
10913
10914 self.tables
10917 .write()
10918 .insert(table_id, TableHandle::new(table));
10919 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
10920 self.finish_durable_publish(epoch, &mut _epoch_guard, checkpoint)?;
10921 Ok(table_id)
10922 }
10923
10924 pub fn drop_table(&self, name: &str) -> Result<()> {
10926 self.drop_table_with_epoch(name).map(|_| ())
10927 }
10928
10929 pub fn drop_table_with_epoch(&self, name: &str) -> Result<Epoch> {
10931 self.drop_table_with_state(name, false, None)
10932 }
10933
10934 pub fn drop_table_with_epoch_controlled<F>(
10935 &self,
10936 name: &str,
10937 mut before_commit: F,
10938 ) -> Result<Epoch>
10939 where
10940 F: FnMut() -> Result<()>,
10941 {
10942 self.drop_table_with_state(name, false, Some(&mut before_commit))
10943 }
10944
10945 #[doc(hidden)]
10947 pub fn discard_building_table(&self, name: &str) -> Result<()> {
10948 if !name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
10949 return Err(MongrelError::InvalidArgument(
10950 "not a CTAS building table".into(),
10951 ));
10952 }
10953 self.drop_table_with_state(name, true, None).map(|_| ())
10954 }
10955
10956 fn drop_table_with_state(
10957 &self,
10958 name: &str,
10959 building: bool,
10960 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
10961 ) -> Result<Epoch> {
10962 use crate::wal::DdlOp;
10963 use std::sync::atomic::Ordering;
10964
10965 self.require(&crate::auth::Permission::Ddl)?;
10966 if self.poisoned.load(Ordering::Relaxed) {
10967 return Err(MongrelError::Other(
10968 "database poisoned by fsync error".into(),
10969 ));
10970 }
10971
10972 let _g = self.ddl_lock.lock();
10973 let _security_write = self.security_write()?;
10974 self.require(&crate::auth::Permission::Ddl)?;
10975 let table_id = {
10976 let cat = self.catalog.read();
10977 if building {
10978 cat.building(name)
10979 } else {
10980 cat.live(name)
10981 }
10982 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
10983 .table_id
10984 };
10985
10986 let commit_lock = Arc::clone(&self.commit_lock);
10987 let _c = commit_lock.lock();
10988 let epoch = self.epoch.bump_assigned();
10989 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
10990 let txn_id = self.alloc_txn_id()?;
10991 let mut next_catalog = self.catalog.read().clone();
10992 let entry = next_catalog
10993 .tables
10994 .iter_mut()
10995 .find(|t| t.table_id == table_id)
10996 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
10997 entry.state = TableState::Dropped { at_epoch: epoch.0 };
10998 next_catalog.triggers.retain(|trigger| {
10999 !matches!(
11000 &trigger.trigger.target,
11001 TriggerTarget::Table(target) if target == name
11002 )
11003 });
11004 next_catalog
11005 .materialized_views
11006 .retain(|definition| definition.name != name);
11007 next_catalog
11008 .security
11009 .rls_tables
11010 .retain(|table| table != name);
11011 next_catalog
11012 .security
11013 .policies
11014 .retain(|policy| policy.table != name);
11015 next_catalog
11016 .security
11017 .masks
11018 .retain(|mask| mask.table != name);
11019 for role in &mut next_catalog.roles {
11020 role.permissions
11021 .retain(|permission| permission_table(permission) != Some(name));
11022 }
11023 advance_security_version(&mut next_catalog)?;
11024 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11025 let commit_seq = {
11026 let mut wal = self.shared_wal.lock();
11027 if let Some(before_commit) = before_commit {
11028 before_commit()?;
11029 }
11030 let append: Result<u64> = (|| {
11031 wal.append(
11032 txn_id,
11033 table_id,
11034 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
11035 )?;
11036 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11037 wal.append_commit(txn_id, epoch, &[])
11038 })();
11039 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11040 };
11041 self.await_durable_commit(commit_seq, epoch)?;
11042
11043 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
11044 self.tables.write().remove(&table_id);
11045 self.finish_durable_publish(epoch, &mut _epoch_guard, checkpoint)?;
11046 Ok(epoch)
11047 }
11048
11049 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
11058 self.rename_table_with_epoch(name, new_name).map(|_| ())
11059 }
11060
11061 pub fn rename_table_with_epoch(&self, name: &str, new_name: &str) -> Result<Epoch> {
11063 self.rename_table_with_epoch_inner(name, new_name, None)
11064 }
11065
11066 pub fn rename_table_with_epoch_controlled<F>(
11067 &self,
11068 name: &str,
11069 new_name: &str,
11070 mut before_commit: F,
11071 ) -> Result<Epoch>
11072 where
11073 F: FnMut() -> Result<()>,
11074 {
11075 self.rename_table_with_epoch_inner(name, new_name, Some(&mut before_commit))
11076 }
11077
11078 fn rename_table_with_epoch_inner(
11079 &self,
11080 name: &str,
11081 new_name: &str,
11082 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11083 ) -> Result<Epoch> {
11084 if name.starts_with(CTAS_BUILD_TABLE_PREFIX)
11085 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
11086 {
11087 return Err(MongrelError::InvalidArgument(
11088 "the CTAS building-table namespace is reserved".into(),
11089 ));
11090 }
11091 self.rename_table_with_state(name, new_name, false, None, before_commit)
11092 }
11093
11094 #[doc(hidden)]
11096 pub fn publish_building_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
11097 self.publish_building_table_inner(build_name, new_name, None)
11098 }
11099
11100 #[doc(hidden)]
11101 pub fn publish_building_table_controlled<F>(
11102 &self,
11103 build_name: &str,
11104 new_name: &str,
11105 mut before_commit: F,
11106 ) -> Result<Epoch>
11107 where
11108 F: FnMut() -> Result<()>,
11109 {
11110 self.publish_building_table_inner(build_name, new_name, Some(&mut before_commit))
11111 }
11112
11113 fn publish_building_table_inner(
11114 &self,
11115 build_name: &str,
11116 new_name: &str,
11117 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11118 ) -> Result<Epoch> {
11119 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
11120 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
11121 {
11122 return Err(MongrelError::InvalidArgument(
11123 "invalid CTAS publish identity".into(),
11124 ));
11125 }
11126 self.rename_table_with_state(build_name, new_name, true, None, before_commit)
11127 }
11128
11129 #[doc(hidden)]
11131 pub fn publish_materialized_building_table(
11132 &self,
11133 build_name: &str,
11134 new_name: &str,
11135 definition: crate::catalog::MaterializedViewEntry,
11136 ) -> Result<Epoch> {
11137 self.publish_materialized_building_table_inner(build_name, new_name, definition, None)
11138 }
11139
11140 #[doc(hidden)]
11141 pub fn publish_materialized_building_table_controlled<F>(
11142 &self,
11143 build_name: &str,
11144 new_name: &str,
11145 definition: crate::catalog::MaterializedViewEntry,
11146 mut before_commit: F,
11147 ) -> Result<Epoch>
11148 where
11149 F: FnMut() -> Result<()>,
11150 {
11151 self.publish_materialized_building_table_inner(
11152 build_name,
11153 new_name,
11154 definition,
11155 Some(&mut before_commit),
11156 )
11157 }
11158
11159 fn publish_materialized_building_table_inner(
11160 &self,
11161 build_name: &str,
11162 new_name: &str,
11163 definition: crate::catalog::MaterializedViewEntry,
11164 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11165 ) -> Result<Epoch> {
11166 if definition.name != new_name || definition.query.trim().is_empty() {
11167 return Err(MongrelError::InvalidArgument(
11168 "invalid materialized-view publication".into(),
11169 ));
11170 }
11171 self.rename_table_with_state(build_name, new_name, true, Some(definition), before_commit)
11172 }
11173
11174 #[doc(hidden)]
11176 pub fn publish_rebuilding_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
11177 self.publish_rebuilding_table_inner(build_name, new_name, None, None)
11178 }
11179
11180 #[doc(hidden)]
11181 pub fn publish_rebuilding_table_controlled<F>(
11182 &self,
11183 build_name: &str,
11184 new_name: &str,
11185 mut before_commit: F,
11186 ) -> Result<Epoch>
11187 where
11188 F: FnMut() -> Result<()>,
11189 {
11190 self.publish_rebuilding_table_inner(build_name, new_name, None, Some(&mut before_commit))
11191 }
11192
11193 #[doc(hidden)]
11195 pub fn publish_materialized_rebuilding_table_controlled<F>(
11196 &self,
11197 build_name: &str,
11198 new_name: &str,
11199 definition: crate::catalog::MaterializedViewEntry,
11200 mut before_commit: F,
11201 ) -> Result<Epoch>
11202 where
11203 F: FnMut() -> Result<()>,
11204 {
11205 self.publish_rebuilding_table_inner(
11206 build_name,
11207 new_name,
11208 Some(definition),
11209 Some(&mut before_commit),
11210 )
11211 }
11212
11213 fn publish_rebuilding_table_inner(
11214 &self,
11215 build_name: &str,
11216 new_name: &str,
11217 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
11218 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11219 ) -> Result<Epoch> {
11220 use crate::wal::DdlOp;
11221
11222 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
11223 || new_name.is_empty()
11224 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
11225 {
11226 return Err(MongrelError::InvalidArgument(
11227 "invalid rebuilding-table publish identity".into(),
11228 ));
11229 }
11230 if materialized_view.as_ref().is_some_and(|definition| {
11231 definition.name != new_name || definition.query.trim().is_empty()
11232 }) {
11233 return Err(MongrelError::InvalidArgument(
11234 "invalid materialized-view replacement".into(),
11235 ));
11236 }
11237 self.require(&crate::auth::Permission::Ddl)?;
11238 if self.poisoned.load(Ordering::Relaxed) {
11239 return Err(MongrelError::Other(
11240 "database poisoned by fsync error".into(),
11241 ));
11242 }
11243
11244 let _ddl = self.ddl_lock.lock();
11245 let _security_write = self.security_write()?;
11246 let (table_id, replaced_table_id) = {
11247 let catalog = self.catalog.read();
11248 let build = catalog.building(build_name).ok_or_else(|| {
11249 MongrelError::NotFound(format!("building table {build_name:?} not found"))
11250 })?;
11251 let replaced_table_id = match &build.state {
11252 TableState::Building {
11253 intended_name,
11254 replaces_table_id: Some(replaced_table_id),
11255 ..
11256 } if intended_name == new_name => *replaced_table_id,
11257 _ => {
11258 return Err(MongrelError::InvalidArgument(format!(
11259 "building table {build_name:?} is not a replacement for {new_name:?}"
11260 )))
11261 }
11262 };
11263 if catalog
11264 .live(new_name)
11265 .is_none_or(|entry| entry.table_id != replaced_table_id)
11266 {
11267 return Err(MongrelError::Conflict(format!(
11268 "table {new_name:?} changed while its replacement was built"
11269 )));
11270 }
11271 (build.table_id, replaced_table_id)
11272 };
11273
11274 let _commit = self.commit_lock.lock();
11275 let epoch = self.epoch.assigned().next();
11276 let txn_id = self.alloc_txn_id()?;
11277 let mut next_catalog = self.catalog.read().clone();
11278 apply_rebuilding_publish(
11279 &mut next_catalog,
11280 table_id,
11281 replaced_table_id,
11282 new_name,
11283 epoch.0,
11284 )?;
11285 if let Some(definition) = materialized_view.as_mut() {
11286 definition.last_refresh_epoch = epoch.0;
11287 }
11288 let materialized_view_json = materialized_view
11289 .as_ref()
11290 .map(DdlOp::encode_materialized_view)
11291 .transpose()?;
11292 if let Some(definition) = materialized_view {
11293 if let Some(existing) = next_catalog
11294 .materialized_views
11295 .iter_mut()
11296 .find(|existing| existing.name == definition.name)
11297 {
11298 *existing = definition;
11299 } else {
11300 next_catalog.materialized_views.push(definition);
11301 }
11302 }
11303 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11304 if let Some(before_commit) = before_commit {
11305 before_commit()?;
11306 }
11307 let assigned_epoch = self.epoch.bump_assigned();
11308 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
11309 if assigned_epoch != epoch {
11310 return Err(MongrelError::Conflict(
11311 "commit epoch changed while sequencer lock was held".into(),
11312 ));
11313 }
11314 let commit_seq = {
11315 let mut wal = self.shared_wal.lock();
11316 let append: Result<u64> = (|| {
11317 wal.append(
11318 txn_id,
11319 table_id,
11320 crate::wal::Op::Ddl(DdlOp::ReplaceBuildingTable {
11321 table_id,
11322 replaced_table_id,
11323 new_name: new_name.to_string(),
11324 }),
11325 )?;
11326 if let Some(definition_json) = materialized_view_json {
11327 wal.append(
11328 txn_id,
11329 table_id,
11330 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
11331 name: new_name.to_string(),
11332 definition_json,
11333 }),
11334 )?;
11335 }
11336 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11337 wal.append_commit(txn_id, epoch, &[])
11338 })();
11339 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11340 };
11341 self.await_durable_commit(commit_seq, epoch)?;
11342
11343 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
11344 self.tables.write().remove(&replaced_table_id);
11345 if let Some(table) = self.tables.read().get(&table_id) {
11346 table.lock().set_catalog_name(new_name.to_string());
11347 }
11348 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
11349 Ok(epoch)
11350 }
11351
11352 fn rename_table_with_state(
11353 &self,
11354 name: &str,
11355 new_name: &str,
11356 building: bool,
11357 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
11358 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11359 ) -> Result<Epoch> {
11360 use crate::wal::DdlOp;
11361 use std::sync::atomic::Ordering;
11362
11363 self.require(&crate::auth::Permission::Ddl)?;
11364 if self.poisoned.load(Ordering::Relaxed) {
11365 return Err(MongrelError::Other(
11366 "database poisoned by fsync error".into(),
11367 ));
11368 }
11369
11370 if name == new_name {
11373 return Ok(self.visible_epoch());
11374 }
11375 if new_name.is_empty() {
11376 return Err(MongrelError::InvalidArgument(
11377 "rename_table: new name must not be empty".into(),
11378 ));
11379 }
11380
11381 let _g = self.ddl_lock.lock();
11382 let _security_write = self.security_write()?;
11383 self.require(&crate::auth::Permission::Ddl)?;
11384 let table_id = {
11385 let cat = self.catalog.read();
11386 let src = if building {
11387 cat.building(name)
11388 } else {
11389 cat.live(name)
11390 }
11391 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
11392 if building
11393 && !matches!(
11394 &src.state,
11395 TableState::Building { intended_name, .. } if intended_name == new_name
11396 )
11397 {
11398 return Err(MongrelError::InvalidArgument(format!(
11399 "building table {name:?} is not reserved for {new_name:?}"
11400 )));
11401 }
11402 if cat.live(new_name).is_some() || (!building && cat.building_for(new_name).is_some()) {
11406 return Err(MongrelError::InvalidArgument(format!(
11407 "rename_table: a table named {new_name:?} already exists"
11408 )));
11409 }
11410 src.table_id
11411 };
11412
11413 let commit_lock = Arc::clone(&self.commit_lock);
11414 let _c = commit_lock.lock();
11415 let epoch = self.epoch.bump_assigned();
11416 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
11417 let txn_id = self.alloc_txn_id()?;
11418 if let Some(definition) = materialized_view.as_mut() {
11419 definition.last_refresh_epoch = epoch.0;
11420 }
11421 let materialized_view_json = materialized_view
11422 .as_ref()
11423 .map(DdlOp::encode_materialized_view)
11424 .transpose()?;
11425 let mut next_catalog = self.catalog.read().clone();
11426 let entry = next_catalog
11427 .tables
11428 .iter_mut()
11429 .find(|t| t.table_id == table_id)
11430 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
11431 entry.name = new_name.to_string();
11432 if building {
11433 entry.state = TableState::Live;
11434 }
11435 for trigger in &mut next_catalog.triggers {
11436 if matches!(
11437 &trigger.trigger.target,
11438 TriggerTarget::Table(target) if target == name
11439 ) {
11440 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
11441 }
11442 }
11443 if let Some(definition) = next_catalog
11444 .materialized_views
11445 .iter_mut()
11446 .find(|definition| definition.name == name)
11447 {
11448 definition.name = new_name.to_string();
11449 }
11450 if let Some(definition) = materialized_view.take() {
11451 next_catalog.materialized_views.push(definition);
11452 }
11453 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11454 for table in &mut next_catalog.security.rls_tables {
11455 if table == name {
11456 *table = new_name.to_string();
11457 }
11458 }
11459 for policy in &mut next_catalog.security.policies {
11460 if policy.table == name {
11461 policy.table = new_name.to_string();
11462 }
11463 }
11464 for mask in &mut next_catalog.security.masks {
11465 if mask.table == name {
11466 mask.table = new_name.to_string();
11467 }
11468 }
11469 for role in &mut next_catalog.roles {
11470 for permission in &mut role.permissions {
11471 rename_permission_table(permission, name, new_name);
11472 }
11473 }
11474 advance_security_version(&mut next_catalog)?;
11475 let ddl = if building {
11476 DdlOp::PublishBuildingTable {
11477 table_id,
11478 new_name: new_name.to_string(),
11479 }
11480 } else {
11481 DdlOp::RenameTable {
11482 table_id,
11483 new_name: new_name.to_string(),
11484 }
11485 };
11486 let commit_seq = {
11487 let mut wal = self.shared_wal.lock();
11488 if let Some(before_commit) = before_commit {
11489 before_commit()?;
11490 }
11491 let append: Result<u64> = (|| {
11492 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
11493 if let Some(definition_json) = materialized_view_json {
11494 wal.append(
11495 txn_id,
11496 table_id,
11497 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
11498 name: new_name.to_string(),
11499 definition_json,
11500 }),
11501 )?;
11502 }
11503 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11504 wal.append_commit(txn_id, epoch, &[])
11505 })();
11506 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11507 };
11508 self.await_durable_commit(commit_seq, epoch)?;
11509
11510 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
11511 if let Some(table) = self.tables.read().get(&table_id) {
11514 table.lock().set_catalog_name(new_name.to_string());
11515 }
11516 self.finish_durable_publish(epoch, &mut _epoch_guard, checkpoint)?;
11517 Ok(epoch)
11518 }
11519
11520 pub fn alter_column(
11521 &self,
11522 table_name: &str,
11523 column_name: &str,
11524 change: AlterColumn,
11525 ) -> Result<ColumnDef> {
11526 self.alter_column_with_epoch(table_name, column_name, change)
11527 .map(|(column, _)| column)
11528 }
11529
11530 pub fn alter_column_with_epoch(
11531 &self,
11532 table_name: &str,
11533 column_name: &str,
11534 change: AlterColumn,
11535 ) -> Result<(ColumnDef, Option<Epoch>)> {
11536 self.alter_column_with_epoch_inner(table_name, column_name, change, None, None, None)
11537 }
11538
11539 pub fn alter_column_with_epoch_controlled<B, A>(
11544 &self,
11545 table_name: &str,
11546 column_name: &str,
11547 change: AlterColumn,
11548 control: &crate::ExecutionControl,
11549 mut before_commit: B,
11550 mut after_commit: A,
11551 ) -> Result<(ColumnDef, Option<Epoch>)>
11552 where
11553 B: FnMut() -> Result<()>,
11554 A: FnMut(Option<Epoch>) -> Result<()>,
11555 {
11556 self.alter_column_with_epoch_inner(
11557 table_name,
11558 column_name,
11559 change,
11560 Some(control),
11561 Some(&mut before_commit),
11562 Some(&mut after_commit),
11563 )
11564 }
11565
11566 #[allow(clippy::too_many_arguments)]
11567 fn alter_column_with_epoch_inner(
11568 &self,
11569 table_name: &str,
11570 column_name: &str,
11571 change: AlterColumn,
11572 control: Option<&crate::ExecutionControl>,
11573 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11574 mut after_commit: Option<&mut dyn FnMut(Option<Epoch>) -> Result<()>>,
11575 ) -> Result<(ColumnDef, Option<Epoch>)> {
11576 use crate::wal::DdlOp;
11577 use std::sync::atomic::Ordering;
11578
11579 self.require(&crate::auth::Permission::Ddl)?;
11580 commit_prepare_checkpoint(control, 0)?;
11581 if self.poisoned.load(Ordering::Relaxed) {
11582 return Err(MongrelError::Other(
11583 "database poisoned by fsync error".into(),
11584 ));
11585 }
11586
11587 let _g = self.ddl_lock.lock();
11588 let table_id = {
11589 let cat = self.catalog.read();
11590 cat.live(table_name)
11591 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
11592 .table_id
11593 };
11594 let handle =
11595 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
11596 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
11597 })?;
11598
11599 let backfill = {
11605 let table = handle.lock();
11606 let old = table
11607 .schema()
11608 .column(column_name)
11609 .cloned()
11610 .ok_or_else(|| MongrelError::Schema(format!("unknown column {column_name}")))?;
11611 let next_flags = change.flags.unwrap_or(old.flags);
11612 if old.flags.contains(crate::schema::ColumnFlags::NULLABLE)
11613 && !next_flags.contains(crate::schema::ColumnFlags::NULLABLE)
11614 && old.default_value.is_some()
11615 {
11616 let snapshot = Snapshot::at(self.epoch.visible());
11617 let mut updates = Vec::new();
11618 let rows = match control {
11619 Some(control) => table.visible_rows_controlled(snapshot, control)?,
11620 None => table.visible_rows(snapshot)?,
11621 };
11622 for (row_index, row) in rows.into_iter().enumerate() {
11623 commit_prepare_checkpoint(control, row_index)?;
11624 if row
11625 .columns
11626 .get(&old.id)
11627 .is_some_and(|value| !matches!(value, Value::Null))
11628 {
11629 continue;
11630 }
11631 let mut cells: Vec<(u16, Value)> = row.columns.into_iter().collect();
11632 table.apply_defaults(&mut cells)?;
11633 updates.push((
11634 table_id,
11635 crate::txn::Staged::Update {
11636 row_id: row.row_id,
11637 new_row: cells,
11638 changed_columns: vec![old.id],
11639 },
11640 ));
11641 }
11642 updates
11643 } else {
11644 Vec::new()
11645 }
11646 };
11647 let durable_epoch = std::cell::Cell::new(None);
11648 let backfill_epoch = if backfill.is_empty() {
11649 None
11650 } else {
11651 let (principal, catalog_bound) = self.transaction_principal_snapshot();
11652 let txn_id = self.alloc_txn_id()?;
11653 let mut entered_fence = false;
11654 let commit_result = match (control, before_commit.as_deref_mut()) {
11655 (Some(control), Some(before_commit)) => self
11656 .commit_transaction_with_external_states_controlled(
11657 txn_id,
11658 self.epoch.visible(),
11659 backfill,
11660 Vec::new(),
11661 Vec::new(),
11662 principal.clone(),
11663 catalog_bound,
11664 None,
11665 control,
11666 &mut || {
11667 before_commit()?;
11668 entered_fence = true;
11669 Ok(())
11670 },
11671 )
11672 .map(|(epoch, _)| epoch),
11673 _ => self
11674 .commit_transaction_with_external_states(
11675 txn_id,
11676 self.epoch.visible(),
11677 backfill,
11678 Vec::new(),
11679 Vec::new(),
11680 principal,
11681 catalog_bound,
11682 None,
11683 )
11684 .map(|(epoch, _)| epoch),
11685 };
11686 let commit_result = if entered_fence {
11687 finish_controlled_commit_attempt(commit_result, &mut after_commit)
11688 } else {
11689 commit_result
11690 };
11691 match &commit_result {
11692 Ok(epoch) => durable_epoch.set(Some(*epoch)),
11693 Err(MongrelError::DurableCommit { epoch, .. }) => {
11694 durable_epoch.set(Some(Epoch(*epoch)));
11695 }
11696 Err(_) => {}
11697 }
11698 Some(commit_result?)
11699 };
11700 let result: Result<(ColumnDef, Option<Epoch>)> = (|| {
11701 let _security_write = self.security_write()?;
11702 self.require(&crate::auth::Permission::Ddl)?;
11703 if self
11704 .catalog
11705 .read()
11706 .live(table_name)
11707 .is_none_or(|entry| entry.table_id != table_id)
11708 {
11709 return Err(MongrelError::Conflict(format!(
11710 "table {table_name:?} changed during ALTER"
11711 )));
11712 }
11713 let mut table = handle.lock();
11714 let (column, prepared_schema) = table.prepare_alter_column(column_name, &change)?;
11715 let renamed_column = (column.name != column_name).then(|| column.name.clone());
11716 let Some(prepared_schema) = prepared_schema else {
11717 return Ok((column, backfill_epoch));
11718 };
11719
11720 let commit_lock = Arc::clone(&self.commit_lock);
11721 let _c = commit_lock.lock();
11722 let epoch = self.epoch.bump_assigned();
11723 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
11724 let txn_id = self.alloc_txn_id()?;
11725 let column_json = DdlOp::encode_column(&column)?;
11726 let mut next_catalog = self.catalog.read().clone();
11727 let catalog_entry_index = next_catalog
11728 .tables
11729 .iter()
11730 .position(|entry| entry.table_id == table_id)
11731 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
11732 if let Some(new_column_name) = &renamed_column {
11733 for (trigger_index, trigger) in next_catalog.triggers.iter_mut().enumerate() {
11734 commit_prepare_checkpoint(control, trigger_index)?;
11735 if matches!(
11736 &trigger.trigger.target,
11737 TriggerTarget::Table(target) if target == table_name
11738 ) {
11739 trigger.trigger = trigger.trigger.renamed_update_column(
11740 column_name,
11741 new_column_name.clone(),
11742 epoch.0,
11743 )?;
11744 }
11745 }
11746 for (role_index, role) in next_catalog.roles.iter_mut().enumerate() {
11747 commit_prepare_checkpoint(control, role_index)?;
11748 for (permission_index, permission) in role.permissions.iter_mut().enumerate() {
11749 commit_prepare_checkpoint(control, permission_index)?;
11750 rename_permission_column(
11751 permission,
11752 table_name,
11753 column_name,
11754 new_column_name,
11755 );
11756 }
11757 }
11758 advance_security_version(&mut next_catalog)?;
11759 }
11760 next_catalog.tables[catalog_entry_index].schema = prepared_schema.clone();
11761 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11762 commit_prepare_checkpoint(control, 0)?;
11763 let mut entered_fence = false;
11764 if let Some(before_commit) = before_commit.as_deref_mut() {
11765 before_commit()?;
11766 entered_fence = true;
11767 }
11768 let commit_result: Result<Epoch> = (|| {
11769 let commit_seq = {
11770 let mut wal = self.shared_wal.lock();
11771 let append: Result<u64> = (|| {
11772 wal.append(
11773 txn_id,
11774 table_id,
11775 crate::wal::Op::Ddl(DdlOp::AlterTable {
11776 table_id,
11777 column_json,
11778 }),
11779 )?;
11780 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11781 wal.append_commit(txn_id, epoch, &[])
11782 })();
11783 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11784 };
11785 self.await_durable_commit(commit_seq, epoch)?;
11786 durable_epoch.set(Some(epoch));
11787
11788 table.apply_altered_schema_prepared(prepared_schema);
11789 let schema = table.schema().clone();
11790 let table_checkpoint = table.checkpoint_altered_schema();
11791 drop(table);
11792 next_catalog.tables[catalog_entry_index].schema = schema;
11793 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11794 let catalog_result =
11795 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref());
11796 let security_version = next_catalog.security_version;
11797 *self.catalog.write() = next_catalog;
11798 if renamed_column.is_some() {
11799 self.security_coordinator
11800 .version
11801 .store(security_version, Ordering::Release);
11802 }
11803 self.epoch.publish_in_order(epoch);
11804 _epoch_guard.disarm();
11805 if let Err(error) = table_checkpoint.and(catalog_result) {
11806 self.poisoned.store(true, Ordering::Relaxed);
11807 return Err(MongrelError::DurableCommit {
11808 epoch: epoch.0,
11809 message: error.to_string(),
11810 });
11811 }
11812 Ok(epoch)
11813 })();
11814 let commit_result = if entered_fence {
11815 finish_controlled_commit_attempt(commit_result, &mut after_commit)
11816 } else {
11817 commit_result
11818 };
11819 let epoch = commit_result?;
11820 Ok((column, Some(epoch)))
11821 })();
11822 result.map_err(|error| match (durable_epoch.get(), error) {
11823 (_, error @ MongrelError::DurableCommit { .. }) => error,
11824 (Some(epoch), error) => MongrelError::DurableCommit {
11825 epoch: epoch.0,
11826 message: error.to_string(),
11827 },
11828 (None, error) => error,
11829 })
11830 }
11831
11832 pub fn set_table_ttl(
11835 &self,
11836 table_name: &str,
11837 column_name: &str,
11838 duration_nanos: u64,
11839 ) -> Result<crate::manifest::TtlPolicy> {
11840 let policy = self.replace_table_ttl(table_name, Some((column_name, duration_nanos)))?;
11841 policy.ok_or_else(|| MongrelError::Other("set TTL produced no policy".into()))
11842 }
11843
11844 #[doc(hidden)]
11846 pub fn set_building_table_ttl(
11847 &self,
11848 table_name: &str,
11849 column_name: &str,
11850 duration_nanos: u64,
11851 ) -> Result<crate::manifest::TtlPolicy> {
11852 let policy = self.replace_table_ttl_with_state(
11853 table_name,
11854 Some((column_name, duration_nanos)),
11855 true,
11856 )?;
11857 policy
11858 .ok_or_else(|| MongrelError::Other("set building-table TTL produced no policy".into()))
11859 }
11860
11861 pub fn clear_table_ttl(&self, table_name: &str) -> Result<()> {
11862 self.replace_table_ttl(table_name, None)?;
11863 Ok(())
11864 }
11865
11866 fn replace_table_ttl(
11867 &self,
11868 table_name: &str,
11869 requested: Option<(&str, u64)>,
11870 ) -> Result<Option<crate::manifest::TtlPolicy>> {
11871 self.replace_table_ttl_with_state(table_name, requested, false)
11872 }
11873
11874 fn replace_table_ttl_with_state(
11875 &self,
11876 table_name: &str,
11877 requested: Option<(&str, u64)>,
11878 building: bool,
11879 ) -> Result<Option<crate::manifest::TtlPolicy>> {
11880 use crate::wal::DdlOp;
11881 use std::sync::atomic::Ordering;
11882
11883 self.require(&crate::auth::Permission::Ddl)?;
11884 if self.poisoned.load(Ordering::Relaxed) {
11885 return Err(MongrelError::Other(
11886 "database poisoned by fsync error".into(),
11887 ));
11888 }
11889
11890 let _g = self.ddl_lock.lock();
11891 let _security_write = self.security_write()?;
11892 self.require(&crate::auth::Permission::Ddl)?;
11893 let table_id = {
11894 let cat = self.catalog.read();
11895 if building {
11896 cat.building(table_name)
11897 } else {
11898 cat.live(table_name)
11899 }
11900 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
11901 .table_id
11902 };
11903 let handle =
11904 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
11905 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
11906 })?;
11907 let mut table = handle.lock();
11908 let policy = match requested {
11909 Some((column, duration)) => Some(table.prepare_ttl_policy(column, duration)?),
11910 None => None,
11911 };
11912 if table.ttl() == policy {
11913 return Ok(policy);
11914 }
11915
11916 let commit_lock = Arc::clone(&self.commit_lock);
11917 let _c = commit_lock.lock();
11918 let epoch = self.epoch.bump_assigned();
11919 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
11920 let txn_id = self.alloc_txn_id()?;
11921 let policy_json = DdlOp::encode_ttl(policy)?;
11922 let mut next_catalog = self.catalog.read().clone();
11923 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11924 let commit_seq = {
11925 let mut wal = self.shared_wal.lock();
11926 let append: Result<u64> = (|| {
11927 wal.append(
11928 txn_id,
11929 table_id,
11930 crate::wal::Op::Ddl(DdlOp::SetTtl {
11931 table_id,
11932 policy_json,
11933 }),
11934 )?;
11935 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11936 wal.append_commit(txn_id, epoch, &[])
11937 })();
11938 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11939 };
11940 self.await_durable_commit(commit_seq, epoch)?;
11941
11942 let mut publish_error = table.apply_ttl_policy_at(policy, epoch).err();
11943 drop(table);
11944 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
11945 publish_error.get_or_insert(error);
11946 }
11947 self.finish_durable_publish(epoch, &mut _epoch_guard, publish_error.map_or(Ok(()), Err))?;
11948 Ok(policy)
11949 }
11950
11951 pub fn gc(&self) -> Result<usize> {
11957 let control = crate::ExecutionControl::new(None);
11958 self.gc_controlled(&control, || true)
11959 }
11960
11961 #[doc(hidden)]
11964 pub fn gc_controlled<F>(
11965 &self,
11966 control: &crate::ExecutionControl,
11967 before_publish: F,
11968 ) -> Result<usize>
11969 where
11970 F: FnOnce() -> bool,
11971 {
11972 self.gc_controlled_with_receipt(control, before_publish)
11973 .map(|(reclaimed, _)| reclaimed)
11974 }
11975
11976 #[doc(hidden)]
11979 pub fn gc_controlled_with_receipt<F>(
11980 &self,
11981 control: &crate::ExecutionControl,
11982 before_publish: F,
11983 ) -> Result<(usize, Option<MaintenanceReceipt>)>
11984 where
11985 F: FnOnce() -> bool,
11986 {
11987 enum Candidate {
11988 Directory(PathBuf),
11989 File(PathBuf),
11990 }
11991
11992 self.require(&crate::auth::Permission::Ddl)?;
11993 let _ddl = self.ddl_lock.lock();
11994 self.require(&crate::auth::Permission::Ddl)?;
11995 control.checkpoint()?;
11996 let maintenance_epoch = self.epoch.visible();
11997 let min_active = self.snapshots.min_active(maintenance_epoch).0;
11998 let mut candidates = Vec::new();
11999
12000 let cat = self.catalog.read();
12002 for (entry_index, entry) in cat.tables.iter().enumerate() {
12003 if entry_index % 256 == 0 {
12004 control.checkpoint()?;
12005 }
12006 if let TableState::Dropped { at_epoch } = entry.state {
12007 if at_epoch <= min_active {
12008 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
12009 if tdir.exists() {
12010 candidates.push(Candidate::Directory(tdir));
12011 }
12012 }
12013 }
12014 }
12015 drop(cat);
12016
12017 let cat = self.catalog.read();
12022 for (entry_index, entry) in cat.tables.iter().enumerate() {
12023 if entry_index % 256 == 0 {
12024 control.checkpoint()?;
12025 }
12026 if !matches!(entry.state, TableState::Live) {
12027 continue;
12028 }
12029 let txn_dir = self
12030 .root
12031 .join(TABLES_DIR)
12032 .join(entry.table_id.to_string())
12033 .join("_txn");
12034 if !txn_dir.exists() {
12035 continue;
12036 }
12037 for (sub_index, sub) in std::fs::read_dir(&txn_dir)?.enumerate() {
12038 if sub_index % 256 == 0 {
12039 control.checkpoint()?;
12040 }
12041 let sub = sub?;
12042 let name = sub.file_name();
12043 let Some(name) = name.to_str() else { continue };
12044 let is_active = name
12046 .parse::<u64>()
12047 .map(|id| self.active_spills.is_active(id))
12048 .unwrap_or(false);
12049 if is_active {
12050 continue;
12051 }
12052 candidates.push(Candidate::Directory(sub.path()));
12053 }
12054 }
12055 drop(cat);
12056
12057 let external_names = {
12058 let cat = self.catalog.read();
12059 cat.external_tables
12060 .iter()
12061 .map(|entry| entry.name.clone())
12062 .collect::<std::collections::HashSet<_>>()
12063 };
12064 let vtab_dir = self.root.join(VTAB_DIR);
12065 if vtab_dir.exists() {
12066 for (entry_index, entry) in std::fs::read_dir(&vtab_dir)?.enumerate() {
12067 if entry_index % 256 == 0 {
12068 control.checkpoint()?;
12069 }
12070 let entry = entry?;
12071 let name = entry.file_name();
12072 let Some(name) = name.to_str() else { continue };
12073 if external_names.contains(name) {
12074 continue;
12075 }
12076 let path = entry.path();
12077 if path.is_dir() {
12078 candidates.push(Candidate::Directory(path));
12079 } else {
12080 candidates.push(Candidate::File(path));
12081 }
12082 }
12083 }
12084
12085 let tables = self
12089 .tables
12090 .read()
12091 .iter()
12092 .map(|(table_id, handle)| (*table_id, handle.clone()))
12093 .collect::<Vec<_>>();
12094 let mut retiring = Vec::new();
12095 for (table_index, (table_id, handle)) in tables.iter().enumerate() {
12096 if table_index % 256 == 0 {
12097 control.checkpoint()?;
12098 }
12099 let backup_pinned: HashSet<u128> = self
12100 .backup_pins
12101 .lock()
12102 .keys()
12103 .filter_map(|(pinned_table, run_id)| {
12104 (*pinned_table == *table_id).then_some(*run_id)
12105 })
12106 .collect();
12107 if handle
12108 .lock()
12109 .has_reapable_retiring(Epoch(min_active), &backup_pinned)
12110 {
12111 retiring.push((handle.clone(), backup_pinned));
12112 }
12113 }
12114
12115 let all_durable = self.active_spills.is_idle()
12123 && tables.iter().all(|(_, handle)| {
12124 let g = handle.lock();
12125 g.memtable_len() == 0 && g.mutable_run_len() == 0
12126 });
12127 let retain = self
12128 .replication_wal_retention_segments
12129 .load(std::sync::atomic::Ordering::Relaxed);
12130 let reap_wal = all_durable
12131 && self
12132 .shared_wal
12133 .lock()
12134 .has_gc_segments_retain_recent(retain)?;
12135
12136 if candidates.is_empty() && retiring.is_empty() && !reap_wal {
12137 return Ok((0, None));
12138 }
12139 control.checkpoint()?;
12140 if !before_publish() {
12141 return Err(MongrelError::Cancelled);
12142 }
12143
12144 let mut reclaimed = 0;
12145 for candidate in candidates {
12146 match candidate {
12147 Candidate::Directory(path) => std::fs::remove_dir_all(path)?,
12148 Candidate::File(path) => std::fs::remove_file(path)?,
12149 }
12150 reclaimed += 1;
12151 }
12152 for (handle, backup_pinned) in retiring {
12153 reclaimed += handle
12154 .lock()
12155 .reap_retiring(Epoch(min_active), &backup_pinned)?;
12156 }
12157 if reap_wal {
12158 reclaimed += self
12159 .shared_wal
12160 .lock()
12161 .gc_segments_retain_recent(u64::MAX, retain)?;
12162 }
12163
12164 Ok((
12165 reclaimed,
12166 Some(MaintenanceReceipt {
12167 epoch: maintenance_epoch,
12168 }),
12169 ))
12170 }
12171
12172 pub fn checkpoint(&self) -> Result<()> {
12190 self.checkpoint_controlled(|| Ok(()))
12191 }
12192
12193 #[doc(hidden)]
12196 pub fn checkpoint_controlled<F>(&self, before_wal_reset: F) -> Result<()>
12197 where
12198 F: FnOnce() -> Result<()>,
12199 {
12200 self.require(&crate::auth::Permission::Ddl)?;
12201 let _replication = self.replication_barrier.write();
12205 let _ddl = self.ddl_lock.lock();
12206 let _security = self.security_coordinator.gate.read();
12207 self.require(&crate::auth::Permission::Ddl)?;
12208
12209 let mut handles = self
12210 .tables
12211 .read()
12212 .iter()
12213 .map(|(table_id, handle)| (*table_id, handle.clone()))
12214 .collect::<Vec<_>>();
12215 handles.sort_by_key(|(table_id, _)| *table_id);
12216 let mut tables = handles
12217 .iter()
12218 .map(|(table_id, handle)| (*table_id, handle.lock()))
12219 .collect::<Vec<_>>();
12220
12221 for (_, table) in &mut tables {
12223 if table.has_pending_writes() || table.memtable_len() > 0 || table.mutable_run_len() > 0
12224 {
12225 table.force_flush()?;
12226 }
12227 }
12228
12229 for (_, table) in &mut tables {
12232 if table.run_count() >= 2 || table.should_compact() {
12233 table.compact()?;
12234 }
12235 }
12236
12237 before_wal_reset()?;
12238
12239 let maintenance_epoch = self.epoch.visible();
12241 let min_active = self.snapshots.min_active(maintenance_epoch);
12242 for (table_id, table) in &mut tables {
12243 let backup_pinned: HashSet<u128> = self
12244 .backup_pins
12245 .lock()
12246 .keys()
12247 .filter_map(|(pinned_table, run_id)| {
12248 (*pinned_table == *table_id).then_some(*run_id)
12249 })
12250 .collect();
12251 table.reap_retiring(min_active, &backup_pinned)?;
12252 }
12253
12254 self.shared_wal.lock().reset_after_checkpoint()?;
12257
12258 let catalog_snapshot = self.catalog.read().clone();
12260 for entry in &catalog_snapshot.tables {
12261 if matches!(entry.state, TableState::Dropped { at_epoch } if at_epoch <= min_active.0) {
12262 crate::durable_file::remove_directory_all(
12263 &self.root.join(TABLES_DIR).join(entry.table_id.to_string()),
12264 )?;
12265 }
12266 if !matches!(entry.state, TableState::Live) {
12267 continue;
12268 }
12269 let transaction_dir = self
12270 .root
12271 .join(TABLES_DIR)
12272 .join(entry.table_id.to_string())
12273 .join("_txn");
12274 if transaction_dir.is_dir() {
12275 for child in std::fs::read_dir(&transaction_dir)? {
12276 let child = child?;
12277 let active = child
12278 .file_name()
12279 .to_str()
12280 .and_then(|name| name.parse::<u64>().ok())
12281 .is_some_and(|txn_id| self.active_spills.is_active(txn_id));
12282 if !active {
12283 crate::durable_file::remove_directory_all(&child.path())?;
12284 }
12285 }
12286 }
12287 }
12288 let external_names = catalog_snapshot
12289 .external_tables
12290 .iter()
12291 .map(|entry| entry.name.as_str())
12292 .collect::<HashSet<_>>();
12293 let external_root = self.root.join(VTAB_DIR);
12294 if external_root.is_dir() {
12295 for entry in std::fs::read_dir(&external_root)? {
12296 let entry = entry?;
12297 let name = entry.file_name();
12298 if name
12299 .to_str()
12300 .is_some_and(|name| external_names.contains(name))
12301 {
12302 continue;
12303 }
12304 if entry.file_type()?.is_dir() {
12305 crate::durable_file::remove_directory_all(&entry.path())?;
12306 } else {
12307 std::fs::remove_file(entry.path())?;
12308 crate::durable_file::sync_directory(&external_root)?;
12309 }
12310 }
12311 }
12312
12313 catalog::write_atomic(&self.root, &catalog_snapshot, self.meta_dek.as_ref())?;
12316 let visible = self.epoch.visible();
12317 for (_, table) in &tables {
12318 table.persist_manifest(visible)?;
12319 }
12320
12321 Ok(())
12322 }
12323 fn alloc_txn_id(&self) -> Result<u64> {
12324 self.ensure_owner_process()?;
12325 crate::txn::allocate_txn_id(&self.next_txn_id)
12326 }
12327
12328 pub fn set_spill_threshold(&self, bytes: u64) {
12332 self.spill_threshold
12333 .store(bytes, std::sync::atomic::Ordering::Relaxed);
12334 }
12335
12336 #[doc(hidden)]
12340 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12341 *self.spill_hook.lock() = Some(Box::new(f));
12342 }
12343
12344 #[doc(hidden)]
12347 pub fn __set_security_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12348 *self.security_commit_hook.lock() = Some(Box::new(f));
12349 }
12350
12351 #[doc(hidden)]
12354 pub fn __set_catalog_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12355 *self.catalog_commit_hook.lock() = Some(Box::new(f));
12356 }
12357
12358 #[doc(hidden)]
12361 pub fn __set_backup_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12362 *self.backup_hook.lock() = Some(Box::new(f));
12363 }
12364
12365 #[doc(hidden)]
12367 pub fn __set_replication_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12368 *self.replication_hook.lock() = Some(Box::new(f));
12369 }
12370
12371 #[doc(hidden)]
12375 pub fn __wal_group_sync_count(&self) -> u64 {
12376 self.shared_wal.lock().group_sync_count()
12377 }
12378
12379 #[doc(hidden)]
12382 pub fn __poison(&self) {
12383 self.poisoned
12384 .store(true, std::sync::atomic::Ordering::Relaxed);
12385 }
12386
12387 pub fn check(&self) -> Vec<CheckIssue> {
12400 match self.check_inner(None) {
12401 Ok(issues) => issues,
12402 Err(error) => vec![CheckIssue {
12403 table_id: WAL_TABLE_ID,
12404 table_name: "shared WAL".into(),
12405 severity: "error".into(),
12406 description: error.to_string(),
12407 }],
12408 }
12409 }
12410
12411 #[doc(hidden)]
12413 pub fn check_controlled(&self, control: &crate::ExecutionControl) -> Result<Vec<CheckIssue>> {
12414 self.check_inner(Some(control))
12415 }
12416
12417 fn check_inner(&self, control: Option<&crate::ExecutionControl>) -> Result<Vec<CheckIssue>> {
12418 let mut issues = Vec::new();
12419 let cat = self.catalog.read();
12420 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
12421 for (table_index, entry) in cat.tables.iter().enumerate() {
12422 if table_index % 256 == 0 {
12423 if let Some(control) = control {
12424 control.checkpoint()?;
12425 }
12426 }
12427 if !matches!(entry.state, TableState::Live) {
12428 continue;
12429 }
12430 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
12431 let mut err = |sev: &str, desc: String| {
12432 issues.push(CheckIssue {
12433 table_id: entry.table_id,
12434 table_name: entry.name.clone(),
12435 severity: sev.into(),
12436 description: desc,
12437 });
12438 };
12439 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
12440 Ok(m) => m,
12441 Err(e) => {
12442 err("error", format!("manifest read failed: {e}"));
12443 continue;
12444 }
12445 };
12446 if m.flushed_epoch > m.current_epoch {
12447 err(
12448 "error",
12449 format!(
12450 "flushed_epoch {} exceeds current_epoch {} (impossible)",
12451 m.flushed_epoch, m.current_epoch
12452 ),
12453 );
12454 }
12455
12456 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
12457 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
12458 for (run_index, rr) in m.runs.iter().enumerate() {
12459 if run_index % 256 == 0 {
12460 if let Some(control) = control {
12461 control.checkpoint()?;
12462 }
12463 }
12464 referenced.insert(rr.run_id);
12465 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
12466 if !run_path.exists() {
12467 err("error", format!("missing run file: r-{}.sr", rr.run_id));
12468 continue;
12469 }
12470 match crate::sorted_run::RunReader::open(
12471 &run_path,
12472 entry.schema.clone(),
12473 self.kek.clone(),
12474 ) {
12475 Ok(reader) => {
12476 if reader.row_count() as u64 != rr.row_count {
12477 err(
12478 "error",
12479 format!(
12480 "run r-{} row count mismatch: manifest {} vs run {}",
12481 rr.run_id,
12482 rr.row_count,
12483 reader.row_count()
12484 ),
12485 );
12486 }
12487 }
12488 Err(e) => {
12489 err(
12490 "error",
12491 format!("run r-{} integrity check failed: {e}", rr.run_id),
12492 );
12493 }
12494 }
12495 }
12496
12497 for r in &m.retiring {
12501 referenced.insert(r.run_id);
12502 }
12503
12504 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
12506 for (entry_index, ent) in rd.flatten().enumerate() {
12507 if entry_index % 256 == 0 {
12508 if let Some(control) = control {
12509 control.checkpoint()?;
12510 }
12511 }
12512 let p = ent.path();
12513 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
12514 continue;
12515 }
12516 let run_id = p
12517 .file_stem()
12518 .and_then(|s| s.to_str())
12519 .and_then(|s| s.strip_prefix("r-"))
12520 .and_then(|s| s.parse::<u128>().ok());
12521 if let Some(id) = run_id {
12522 if !referenced.contains(&id) {
12523 err(
12524 "warning",
12525 format!("orphan run file r-{id}.sr not referenced by the manifest"),
12526 );
12527 }
12528 }
12529 }
12530 }
12531 }
12532
12533 let external_names = cat
12534 .external_tables
12535 .iter()
12536 .map(|entry| entry.name.clone())
12537 .collect::<std::collections::HashSet<_>>();
12538 let vtab_dir = self.root.join(VTAB_DIR);
12539 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
12540 for (entry_index, entry) in entries.flatten().enumerate() {
12541 if entry_index % 256 == 0 {
12542 if let Some(control) = control {
12543 control.checkpoint()?;
12544 }
12545 }
12546 let name = entry.file_name();
12547 let Some(name) = name.to_str() else { continue };
12548 if !external_names.contains(name) {
12549 issues.push(CheckIssue {
12550 table_id: EXTERNAL_TABLE_ID,
12551 table_name: name.to_string(),
12552 severity: "warning".into(),
12553 description: format!(
12554 "orphan external table state entry {:?} not referenced by the catalog",
12555 entry.path()
12556 ),
12557 });
12558 }
12559 }
12560 }
12561
12562 if let Some(control) = control {
12569 control.checkpoint()?;
12570 }
12571 for (seg, msg) in self.shared_wal.lock().verify_segments() {
12572 issues.push(CheckIssue {
12573 table_id: WAL_TABLE_ID,
12574 table_name: "<wal>".into(),
12575 severity: "error".into(),
12576 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
12577 });
12578 }
12579 Ok(issues)
12580 }
12581
12582 pub fn doctor(&self) -> Result<Vec<u64>> {
12586 let control = crate::ExecutionControl::new(None);
12587 self.doctor_controlled(&control, || true)
12588 }
12589
12590 #[doc(hidden)]
12594 pub fn doctor_controlled<F>(
12595 &self,
12596 control: &crate::ExecutionControl,
12597 before_publish: F,
12598 ) -> Result<Vec<u64>>
12599 where
12600 F: FnOnce() -> bool,
12601 {
12602 self.doctor_controlled_with_receipt(control, before_publish)
12603 .map(|(quarantined, _)| quarantined)
12604 }
12605
12606 #[doc(hidden)]
12609 pub fn doctor_controlled_with_receipt<F>(
12610 &self,
12611 control: &crate::ExecutionControl,
12612 before_publish: F,
12613 ) -> Result<(Vec<u64>, Option<MaintenanceReceipt>)>
12614 where
12615 F: FnOnce() -> bool,
12616 {
12617 let _ddl = self.ddl_lock.lock();
12620 let _security_write = self.security_write()?;
12621 let issues = self.check_inner(Some(control))?;
12622 let bad_tables: std::collections::HashSet<u64> = issues
12627 .iter()
12628 .filter(|i| {
12629 i.severity == "error"
12630 && i.table_id != WAL_TABLE_ID
12631 && i.table_id != EXTERNAL_TABLE_ID
12632 })
12633 .map(|i| i.table_id)
12634 .collect();
12635 if bad_tables.is_empty() {
12636 return Ok((Vec::new(), None));
12637 }
12638 let _commit = self.commit_lock.lock();
12639 control.checkpoint()?;
12640 if !before_publish() {
12641 return Err(MongrelError::Cancelled);
12642 }
12643 let maintenance_epoch = self.epoch.bump_assigned();
12644 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), maintenance_epoch);
12645
12646 let qdir = self.root.join("_quarantine");
12647 crate::durable_file::create_directory(&qdir)?;
12648 let mut bad_tables = bad_tables.into_iter().collect::<Vec<_>>();
12649 bad_tables.sort_unstable();
12650
12651 let mut handles = self
12655 .tables
12656 .read()
12657 .iter()
12658 .filter(|(table_id, _)| bad_tables.binary_search(table_id).is_ok())
12659 .map(|(table_id, handle)| (*table_id, handle.clone()))
12660 .collect::<Vec<_>>();
12661 handles.sort_by_key(|(table_id, _)| *table_id);
12662 let mut table_guards = handles
12663 .iter()
12664 .map(|(table_id, handle)| (*table_id, handle.lock()))
12665 .collect::<Vec<_>>();
12666
12667 let mut next_catalog = self.catalog.read().clone();
12668 for table_id in &bad_tables {
12669 if let Some(entry) = next_catalog
12670 .tables
12671 .iter_mut()
12672 .find(|entry| entry.table_id == *table_id)
12673 {
12674 entry.state = TableState::Dropped {
12675 at_epoch: maintenance_epoch.0,
12676 };
12677 }
12678 }
12679 next_catalog.db_epoch = next_catalog.db_epoch.max(maintenance_epoch.0);
12680
12681 let txn_id = self.alloc_txn_id()?;
12682 let commit_seq = {
12683 let mut wal = self.shared_wal.lock();
12684 let append: Result<u64> = (|| {
12685 for table_id in &bad_tables {
12686 wal.append(
12687 txn_id,
12688 *table_id,
12689 crate::wal::Op::Ddl(crate::wal::DdlOp::DropTable {
12690 table_id: *table_id,
12691 }),
12692 )?;
12693 }
12694 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
12695 wal.append_commit(txn_id, maintenance_epoch, &[])
12696 })();
12697 append.map_err(|error| self.commit_outcome_unknown(maintenance_epoch, error))?
12698 };
12699 self.await_durable_commit(commit_seq, maintenance_epoch)?;
12700 for (_, table) in &mut table_guards {
12701 table.mark_unavailable_after_quarantine();
12702 }
12703 {
12704 let mut live_tables = self.tables.write();
12705 for table_id in &bad_tables {
12706 live_tables.remove(table_id);
12707 }
12708 }
12709 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
12710 self.finish_durable_publish(maintenance_epoch, &mut epoch_guard, checkpoint)?;
12711
12712 for table_id in &bad_tables {
12716 let source = self.root.join(TABLES_DIR).join(table_id.to_string());
12717 if source.exists() {
12718 let destination = qdir.join(table_id.to_string());
12719 if let Err(error) = crate::durable_file::rename(&source, &destination) {
12720 return Err(MongrelError::DurableCommit {
12721 epoch: maintenance_epoch.0,
12722 message: format!(
12723 "DOCTOR dropped table {table_id} but quarantine move failed: {error}"
12724 ),
12725 });
12726 }
12727 }
12728 }
12729 Ok((
12730 bad_tables,
12731 Some(MaintenanceReceipt {
12732 epoch: maintenance_epoch,
12733 }),
12734 ))
12735 }
12736
12737 #[allow(dead_code)]
12739 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
12740 self.kek.as_ref()
12741 }
12742
12743 #[allow(dead_code)]
12745 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
12746 &self.epoch
12747 }
12748
12749 #[allow(dead_code)]
12751 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
12752 &self.snapshots
12753 }
12754}
12755
12756fn external_state_dir(root: &Path, name: &str) -> PathBuf {
12757 root.join(VTAB_DIR).join(name)
12758}
12759
12760fn append_catalog_snapshot(
12761 wal: &mut crate::wal::SharedWal,
12762 txn_id: u64,
12763 catalog: &Catalog,
12764) -> Result<()> {
12765 let catalog_json = crate::wal::DdlOp::encode_catalog(catalog)?;
12766 wal.append(
12767 txn_id,
12768 WAL_TABLE_ID,
12769 crate::wal::Op::Ddl(crate::wal::DdlOp::CatalogSnapshot { catalog_json }),
12770 )?;
12771 Ok(())
12772}
12773
12774fn filter_ignored_staging(
12775 staging: Vec<(u64, crate::txn::Staged)>,
12776 ignored_indices: &std::collections::BTreeSet<usize>,
12777) -> Vec<(u64, crate::txn::Staged)> {
12778 if ignored_indices.is_empty() {
12779 return staging;
12780 }
12781 staging
12782 .into_iter()
12783 .enumerate()
12784 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
12785 .collect()
12786}
12787
12788fn external_state_file(root: &Path, name: &str) -> PathBuf {
12789 external_state_dir(root, name).join("state.json")
12790}
12791
12792fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
12793 let path = external_state_file(root, name);
12794 match std::fs::read(path) {
12795 Ok(bytes) => Ok(bytes),
12796 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
12797 Err(e) => Err(e.into()),
12798 }
12799}
12800
12801fn current_external_state_bytes(
12802 root: &Path,
12803 external_states: &[(String, Vec<u8>)],
12804 name: &str,
12805) -> Result<Vec<u8>> {
12806 for (table, state) in external_states.iter().rev() {
12807 if table == name {
12808 return Ok(state.clone());
12809 }
12810 }
12811 read_external_state_file(root, name)
12812}
12813
12814fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
12815 let mut out = external_states;
12816 dedup_external_states_in_place(&mut out);
12817 out
12818}
12819
12820fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
12821 let mut seen = std::collections::HashSet::new();
12822 let mut out = Vec::with_capacity(external_states.len());
12823 for (name, state) in std::mem::take(external_states).into_iter().rev() {
12824 if seen.insert(name.clone()) {
12825 out.push((name, state));
12826 }
12827 }
12828 out.reverse();
12829 *external_states = out;
12830}
12831
12832fn prepare_external_state_file(
12833 root: &Path,
12834 name: &str,
12835 state: &[u8],
12836 txn_id: u64,
12837) -> Result<PathBuf> {
12838 crate::durable_file::create_directory(&root.join(VTAB_DIR))?;
12839 let dir = external_state_dir(root, name);
12840 crate::durable_file::create_directory(&dir)?;
12841 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
12842 {
12843 let mut file = std::fs::OpenOptions::new()
12844 .create_new(true)
12845 .write(true)
12846 .open(&pending)?;
12847 file.write_all(state)?;
12848 file.sync_all()?;
12849 }
12850 Ok(pending)
12851}
12852
12853fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
12854 let path = external_state_file(root, name);
12855 crate::durable_file::replace(pending, &path)?;
12856 Ok(())
12857}
12858
12859fn write_external_state_file(
12860 durable: &crate::durable_file::DurableRoot,
12861 name: &str,
12862 state: &[u8],
12863) -> Result<()> {
12864 let directory = Path::new(VTAB_DIR).join(name);
12865 durable.create_directory_all(&directory)?;
12866 durable.write_atomic(directory.join("state.json"), state)?;
12867 Ok(())
12868}
12869
12870fn validate_recovered_data_table(
12871 catalog: &Catalog,
12872 tables: &HashMap<u64, TableHandle>,
12873 table_id: u64,
12874 commit_epoch: u64,
12875 offset: u64,
12876) -> Result<bool> {
12877 let entry = catalog
12878 .tables
12879 .iter()
12880 .find(|entry| entry.table_id == table_id)
12881 .ok_or_else(|| MongrelError::CorruptWal {
12882 offset,
12883 reason: format!("committed record references unknown table {table_id}"),
12884 })?;
12885 if commit_epoch < entry.created_epoch {
12886 return Err(MongrelError::CorruptWal {
12887 offset,
12888 reason: format!(
12889 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
12890 entry.created_epoch
12891 ),
12892 });
12893 }
12894 match entry.state {
12895 TableState::Dropped { at_epoch } => {
12896 let abandoned_build_boundary =
12901 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
12902 if commit_epoch >= at_epoch && !abandoned_build_boundary {
12903 Err(MongrelError::CorruptWal {
12904 offset,
12905 reason: format!(
12906 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
12907 ),
12908 })
12909 } else {
12910 Ok(false)
12911 }
12912 }
12913 TableState::Live | TableState::Building { .. } => {
12914 if tables.contains_key(&table_id) {
12915 Ok(true)
12916 } else {
12917 Err(MongrelError::CorruptWal {
12918 offset,
12919 reason: format!("live table {table_id} has no mounted recovery handle"),
12920 })
12921 }
12922 }
12923 }
12924}
12925
12926type RecoveryTableStage = (
12927 Vec<crate::memtable::Row>,
12928 Vec<(crate::rowid::RowId, Epoch)>,
12929 Option<Epoch>,
12930 Epoch,
12931);
12932
12933#[derive(Clone)]
12934struct RecoveryValidationTable {
12935 schema: Schema,
12936 flushed_epoch: u64,
12937}
12938
12939fn validate_shared_wal_recovery_plan(
12940 durable_root: &crate::durable_file::DurableRoot,
12941 catalog: &Catalog,
12942 recovered_table_ids: &HashSet<u64>,
12943 reconciled_table_ids: &HashSet<u64>,
12944 meta_dek: Option<&[u8; META_DEK_LEN]>,
12945 kek: Option<Arc<crate::encryption::Kek>>,
12946 records: &[crate::wal::Record],
12947) -> Result<()> {
12948 use crate::wal::{DdlOp, Op};
12949
12950 let mut tables = HashMap::<u64, RecoveryValidationTable>::new();
12951 for entry in &catalog.tables {
12952 if !matches!(entry.state, TableState::Live) {
12953 continue;
12954 }
12955 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
12956 let manifest = match crate::manifest::read_durable(durable_root, &relative_dir, meta_dek) {
12957 Ok(manifest) => Some(manifest),
12958 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
12959 Err(error) => return Err(error),
12960 };
12961 let flushed_epoch = if let Some(manifest) = manifest {
12962 if manifest.table_id != entry.table_id {
12963 return Err(MongrelError::Conflict(format!(
12964 "catalog table {} storage identity mismatch",
12965 entry.table_id
12966 )));
12967 }
12968 if (manifest.schema_id != entry.schema.schema_id
12969 && !reconciled_table_ids.contains(&entry.table_id))
12970 || manifest.flushed_epoch > manifest.current_epoch
12971 || manifest.global_idx_epoch > manifest.current_epoch
12972 || manifest.next_row_id == u64::MAX
12973 || manifest.auto_inc_next < 0
12974 || manifest.auto_inc_next == i64::MAX
12975 || (entry.schema.auto_increment_column().is_none() && manifest.auto_inc_next != 0)
12976 {
12977 return Err(MongrelError::InvalidArgument(format!(
12978 "table {} manifest counters or schema identity are invalid",
12979 entry.table_id
12980 )));
12981 }
12982 #[cfg(feature = "encryption")]
12983 let idx_dek = kek.as_ref().map(|key| key.derive_idx_key());
12984 #[cfg(not(feature = "encryption"))]
12985 let idx_dek: Option<zeroize::Zeroizing<[u8; 32]>> = None;
12986 crate::global_idx::read_durable_for(
12987 durable_root,
12988 &relative_dir,
12989 entry.table_id,
12990 &entry.schema,
12991 idx_dek.as_deref(),
12992 )?;
12993 let mut run_ids = HashSet::new();
12994 let mut maximum_row_id = None::<u64>;
12995 for run in &manifest.runs {
12996 if run.run_id >= u64::MAX as u128
12997 || run.epoch_created > manifest.current_epoch
12998 || !run_ids.insert(run.run_id)
12999 {
13000 return Err(MongrelError::InvalidArgument(format!(
13001 "table {} manifest contains an invalid or duplicate run id",
13002 entry.table_id
13003 )));
13004 }
13005 let relative = relative_dir
13006 .join(crate::engine::RUNS_DIR)
13007 .join(format!("r-{}.sr", run.run_id as u64));
13008 let file = durable_root.open_regular(&relative)?;
13009 let mut reader = crate::sorted_run::RunReader::open_file(
13010 file,
13011 entry.schema.clone(),
13012 kek.clone(),
13013 )?;
13014 let header = reader.header();
13015 if header.run_id != run.run_id
13016 || header.level != run.level
13017 || header.row_count != run.row_count
13018 || !header.is_uniform_epoch() && header.epoch_created != run.epoch_created
13019 || header.is_uniform_epoch() && header.epoch_created != 0
13020 || header.schema_id > entry.schema.schema_id
13021 {
13022 return Err(MongrelError::InvalidArgument(format!(
13023 "table {} run {} differs from its manifest: header=(id {}, level {}, rows {}, epoch {}, schema {}), manifest=(id {}, level {}, rows {}, epoch {}, schema <= {})",
13024 entry.table_id,
13025 run.run_id,
13026 header.run_id,
13027 header.level,
13028 header.row_count,
13029 header.epoch_created,
13030 header.schema_id,
13031 run.run_id,
13032 run.level,
13033 run.row_count,
13034 run.epoch_created,
13035 entry.schema.schema_id,
13036 )));
13037 }
13038 if header.row_count != 0 {
13039 maximum_row_id = Some(
13040 maximum_row_id
13041 .map_or(header.max_row_id, |value| value.max(header.max_row_id)),
13042 );
13043 }
13044 reader.validate_all_pages()?;
13045 }
13046 if maximum_row_id.is_some_and(|maximum| manifest.next_row_id <= maximum) {
13047 return Err(MongrelError::InvalidArgument(format!(
13048 "table {} next_row_id does not advance beyond persisted rows",
13049 entry.table_id
13050 )));
13051 }
13052 for run in &manifest.retiring {
13053 if run.run_id >= u64::MAX as u128
13054 || run.retire_epoch > manifest.current_epoch
13055 || !run_ids.insert(run.run_id)
13056 {
13057 return Err(MongrelError::InvalidArgument(format!(
13058 "table {} manifest contains an invalid or aliased retired run",
13059 entry.table_id
13060 )));
13061 }
13062 }
13063 manifest.flushed_epoch
13064 } else {
13065 if !recovered_table_ids.contains(&entry.table_id) {
13066 return Err(MongrelError::NotFound(format!(
13067 "live table {} manifest is missing",
13068 entry.table_id
13069 )));
13070 }
13071 0
13072 };
13073 tables.insert(
13074 entry.table_id,
13075 RecoveryValidationTable {
13076 schema: entry.schema.clone(),
13077 flushed_epoch,
13078 },
13079 );
13080 }
13081
13082 let committed = records
13083 .iter()
13084 .filter_map(|record| match record.op {
13085 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
13086 _ => None,
13087 })
13088 .collect::<HashMap<_, _>>();
13089 let mut run_ids = HashSet::new();
13090 let mut recovered_row_ids = HashMap::<u64, HashSet<u64>>::new();
13091 for record in records {
13092 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
13093 continue;
13094 };
13095 match &record.op {
13096 Op::Put { table_id, rows } => {
13097 let table = validate_recovery_data_table_plan(
13098 catalog,
13099 &tables,
13100 *table_id,
13101 commit_epoch,
13102 record.seq.0,
13103 )?;
13104 let decoded: Vec<crate::memtable::Row> =
13105 bincode::deserialize(rows).map_err(|error| MongrelError::CorruptWal {
13106 offset: record.seq.0,
13107 reason: format!(
13108 "committed Put payload for transaction {} could not be decoded: {error}",
13109 record.txn_id
13110 ),
13111 })?;
13112 if let Some(table) = table {
13113 for row in &decoded {
13114 if !recovered_row_ids
13115 .entry(*table_id)
13116 .or_default()
13117 .insert(row.row_id.0)
13118 {
13119 return Err(MongrelError::CorruptWal {
13120 offset: record.seq.0,
13121 reason: format!(
13122 "committed WAL repeats recovered row id {} for table {table_id}",
13123 row.row_id.0
13124 ),
13125 });
13126 }
13127 validate_recovered_row(&table.schema, row)?;
13128 }
13129 }
13130 }
13131 Op::Delete { table_id, .. } | Op::TruncateTable { table_id } => {
13132 validate_recovery_data_table_plan(
13133 catalog,
13134 &tables,
13135 *table_id,
13136 commit_epoch,
13137 record.seq.0,
13138 )?;
13139 }
13140 Op::ExternalTableState { name, .. } => validate_recovered_external_name(name)?,
13141 Op::Ddl(DdlOp::ResetExternalTableState {
13142 name,
13143 generation_epoch,
13144 }) => {
13145 if *generation_epoch != commit_epoch {
13146 return Err(MongrelError::CorruptWal {
13147 offset: record.seq.0,
13148 reason: format!(
13149 "external state reset epoch {generation_epoch} does not match WAL commit epoch {commit_epoch}"
13150 ),
13151 });
13152 }
13153 validate_recovered_external_name(name)?;
13154 }
13155 Op::TxnCommit { added_runs, .. } => {
13156 for added in added_runs {
13157 let Some(table) = validate_recovery_data_table_plan(
13158 catalog,
13159 &tables,
13160 added.table_id,
13161 commit_epoch,
13162 record.seq.0,
13163 )?
13164 else {
13165 continue;
13166 };
13167 if added.run_id >= u64::MAX as u128
13168 || !run_ids.insert((added.table_id, added.run_id))
13169 {
13170 return Err(MongrelError::CorruptWal {
13171 offset: record.seq.0,
13172 reason: format!(
13173 "duplicate or invalid recovered run {} for table {}",
13174 added.run_id, added.table_id
13175 ),
13176 });
13177 }
13178 if commit_epoch <= table.flushed_epoch {
13179 continue;
13180 }
13181 validate_planned_spilled_run(
13182 durable_root,
13183 record.txn_id,
13184 commit_epoch,
13185 added,
13186 &table.schema,
13187 kek.clone(),
13188 )?;
13189 }
13190 }
13191 _ => {}
13192 }
13193 }
13194 Ok(())
13195}
13196
13197fn validate_recovery_data_table_plan<'a>(
13198 catalog: &Catalog,
13199 tables: &'a HashMap<u64, RecoveryValidationTable>,
13200 table_id: u64,
13201 commit_epoch: u64,
13202 offset: u64,
13203) -> Result<Option<&'a RecoveryValidationTable>> {
13204 let entry = catalog
13205 .tables
13206 .iter()
13207 .find(|entry| entry.table_id == table_id)
13208 .ok_or_else(|| MongrelError::CorruptWal {
13209 offset,
13210 reason: format!("committed record references unknown table {table_id}"),
13211 })?;
13212 if commit_epoch < entry.created_epoch {
13213 return Err(MongrelError::CorruptWal {
13214 offset,
13215 reason: format!(
13216 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
13217 entry.created_epoch
13218 ),
13219 });
13220 }
13221 match entry.state {
13222 TableState::Dropped { at_epoch } => {
13223 let abandoned =
13224 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
13225 if commit_epoch >= at_epoch && !abandoned {
13226 return Err(MongrelError::CorruptWal {
13227 offset,
13228 reason: format!(
13229 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
13230 ),
13231 });
13232 }
13233 Ok(None)
13234 }
13235 TableState::Live => {
13236 tables
13237 .get(&table_id)
13238 .map(Some)
13239 .ok_or_else(|| MongrelError::CorruptWal {
13240 offset,
13241 reason: format!("live table {table_id} has no recovery plan"),
13242 })
13243 }
13244 TableState::Building { .. } => Err(MongrelError::CorruptWal {
13245 offset,
13246 reason: format!("building table {table_id} was not normalized before recovery"),
13247 }),
13248 }
13249}
13250
13251fn validate_planned_spilled_run(
13252 root: &crate::durable_file::DurableRoot,
13253 txn_id: u64,
13254 commit_epoch: u64,
13255 added: &crate::wal::AddedRun,
13256 schema: &Schema,
13257 kek: Option<Arc<crate::encryption::Kek>>,
13258) -> Result<()> {
13259 let table = Path::new(TABLES_DIR).join(added.table_id.to_string());
13260 let destination = table
13261 .join(crate::engine::RUNS_DIR)
13262 .join(format!("r-{}.sr", added.run_id as u64));
13263 let pending = table
13264 .join("_txn")
13265 .join(txn_id.to_string())
13266 .join(format!("r-{}.sr", added.run_id as u64));
13267 let file = match root.open_regular(&destination) {
13268 Ok(file) => file,
13269 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
13270 root.open_regular(&pending).map_err(|pending_error| {
13271 if pending_error.kind() == std::io::ErrorKind::NotFound {
13272 MongrelError::CorruptWal {
13273 offset: commit_epoch,
13274 reason: format!(
13275 "committed spilled run {} for transaction {txn_id} is missing",
13276 added.run_id
13277 ),
13278 }
13279 } else {
13280 pending_error.into()
13281 }
13282 })?
13283 }
13284 Err(error) => return Err(error.into()),
13285 };
13286 let mut reader = crate::sorted_run::RunReader::open_file(file, schema.clone(), kek)?;
13287 let header = reader.header();
13288 if header.run_id != added.run_id
13289 || header.content_hash != added.content_hash
13290 || header.row_count != added.row_count
13291 || header.level != added.level
13292 || header.min_row_id != added.min_row_id
13293 || header.max_row_id != added.max_row_id
13294 || header.schema_id != schema.schema_id
13295 || !header.is_uniform_epoch()
13296 || header.epoch_created != 0
13297 {
13298 return Err(MongrelError::CorruptWal {
13299 offset: commit_epoch,
13300 reason: format!(
13301 "committed spilled run {} metadata differs from WAL",
13302 added.run_id
13303 ),
13304 });
13305 }
13306 reader.validate_all_pages()?;
13307 Ok(())
13308}
13309
13310fn recover_shared_wal(
13319 durable_root: &crate::durable_file::DurableRoot,
13320 tables: &HashMap<u64, TableHandle>,
13321 catalog: &Catalog,
13322 epoch: &EpochAuthority,
13323 records: &[crate::wal::Record],
13324) -> Result<()> {
13325 use crate::memtable::Row;
13326 use crate::wal::{DdlOp, Op};
13327
13328 let mut committed: HashMap<u64, u64> = HashMap::new();
13330 let mut spilled_to_link: Vec<(
13331 u64, u64, Vec<crate::wal::AddedRun>,
13334 )> = Vec::new();
13335 for r in records {
13336 if let Op::TxnCommit {
13337 epoch: ce,
13338 ref added_runs,
13339 } = r.op
13340 {
13341 committed.insert(r.txn_id, ce);
13342 if !added_runs.is_empty() {
13343 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
13344 }
13345 }
13346 }
13347 for record in records {
13348 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
13349 continue;
13350 };
13351 match &record.op {
13352 Op::Put { table_id, .. }
13353 | Op::Delete { table_id, .. }
13354 | Op::TruncateTable { table_id } => {
13355 validate_recovered_data_table(
13356 catalog,
13357 tables,
13358 *table_id,
13359 commit_epoch,
13360 record.seq.0,
13361 )?;
13362 }
13363 Op::TxnCommit { added_runs, .. } => {
13364 for run in added_runs {
13365 validate_recovered_data_table(
13366 catalog,
13367 tables,
13368 run.table_id,
13369 commit_epoch,
13370 record.seq.0,
13371 )?;
13372 }
13373 }
13374 _ => {}
13375 }
13376 }
13377 let truncated_transactions: HashSet<(u64, u64)> = records
13378 .iter()
13379 .filter_map(|record| {
13380 committed.get(&record.txn_id)?;
13381 match record.op {
13382 Op::TruncateTable { table_id } => Some((record.txn_id, table_id)),
13383 _ => None,
13384 }
13385 })
13386 .collect();
13387
13388 enum ExternalRecoveryAction {
13390 Write { name: String, state: Vec<u8> },
13391 Reset { name: String },
13392 }
13393 let mut stage: HashMap<u64, RecoveryTableStage> = HashMap::new();
13394 let mut external_actions = Vec::new();
13395 let mut max_epoch = epoch.visible().0;
13396 for r in records.iter().cloned() {
13397 let Some(&ce) = committed.get(&r.txn_id) else {
13398 continue; };
13400 let commit_epoch = Epoch(ce);
13401 max_epoch = max_epoch.max(ce);
13402 match r.op {
13403 Op::Put { table_id, rows } => {
13404 let skip = tables
13406 .get(&table_id)
13407 .map(|h| h.lock().flushed_epoch() >= ce)
13408 .unwrap_or(true);
13409 if skip {
13410 continue;
13411 }
13412 let rows: Vec<Row> = bincode::deserialize(&rows).map_err(|error| {
13413 MongrelError::CorruptWal {
13414 offset: r.seq.0,
13415 reason: format!(
13416 "committed Put payload for transaction {} could not be decoded: {error}",
13417 r.txn_id
13418 ),
13419 }
13420 })?;
13421 let rows: Vec<Row> = rows
13424 .into_iter()
13425 .map(|mut row| {
13426 row.committed_epoch = commit_epoch;
13427 row
13428 })
13429 .collect();
13430 let entry = stage
13431 .entry(table_id)
13432 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
13433 entry.0.extend(rows);
13434 entry.3 = commit_epoch;
13435 }
13436 Op::Delete { table_id, row_ids } => {
13437 let skip = tables
13438 .get(&table_id)
13439 .map(|h| h.lock().flushed_epoch() >= ce)
13440 .unwrap_or(true);
13441 if skip {
13442 continue;
13443 }
13444 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
13445 let entry = stage
13446 .entry(table_id)
13447 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
13448 entry.1.extend(dels);
13449 entry.3 = commit_epoch;
13450 }
13451 Op::TruncateTable { table_id } => {
13452 let skip = tables
13453 .get(&table_id)
13454 .map(|h| h.lock().flushed_epoch() >= ce)
13455 .unwrap_or(true);
13456 if skip {
13457 continue;
13458 }
13459 stage.insert(
13460 table_id,
13461 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
13462 );
13463 }
13464 Op::ExternalTableState { name, state } => {
13465 let current_generation = catalog
13466 .external_tables
13467 .iter()
13468 .find(|entry| entry.name == name)
13469 .map(|entry| entry.created_epoch);
13470 if current_generation.is_some_and(|created_epoch| ce >= created_epoch) {
13471 validate_recovered_external_name(&name)?;
13472 external_actions.push(ExternalRecoveryAction::Write { name, state });
13473 }
13474 }
13475 Op::Ddl(DdlOp::ResetExternalTableState {
13476 name,
13477 generation_epoch,
13478 }) => {
13479 if generation_epoch != ce {
13480 return Err(MongrelError::CorruptWal {
13481 offset: r.seq.0,
13482 reason: format!(
13483 "external state reset epoch {generation_epoch} does not match WAL commit epoch {ce}"
13484 ),
13485 });
13486 }
13487 validate_recovered_external_name(&name)?;
13488 external_actions.push(ExternalRecoveryAction::Reset { name });
13489 }
13490 Op::Flush { .. }
13491 | Op::TxnCommit { .. }
13492 | Op::TxnAbort
13493 | Op::Ddl(_)
13494 | Op::BeforeImage { .. }
13495 | Op::CommitTimestamp { .. }
13496 | Op::SpilledRows { .. } => {}
13497 }
13498 }
13499 for (_, commit_epoch, added_runs) in &mut spilled_to_link {
13500 added_runs.retain(|added| {
13501 tables
13502 .get(&added.table_id)
13503 .is_some_and(|table| table.lock().flushed_epoch() < *commit_epoch)
13504 });
13505 }
13506 spilled_to_link.retain(|(_, _, added_runs)| !added_runs.is_empty());
13507 validate_recovery_table_stages(tables, &stage)?;
13508 validate_recovery_spilled_runs(durable_root, tables, &spilled_to_link)?;
13509
13510 for action in external_actions {
13514 match action {
13515 ExternalRecoveryAction::Write { name, state } => {
13516 write_external_state_file(durable_root, &name, &state)?;
13517 }
13518 ExternalRecoveryAction::Reset { name } => {
13519 durable_root.create_directory_all(VTAB_DIR)?;
13520 durable_root.remove_directory_all(Path::new(VTAB_DIR).join(name))?;
13521 }
13522 }
13523 }
13524 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
13525 let Some(handle) = tables.get(&table_id) else {
13526 continue;
13527 };
13528 let mut t = handle.lock();
13529 if let Some(epoch) = truncate_epoch {
13530 t.apply_truncate(epoch);
13531 }
13532 t.recover_apply(rows, deletes)?;
13533 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
13537 t.live_count = rows.len() as u64;
13538 t.persist_manifest(table_epoch.max(epoch.visible()))?;
13542 }
13543
13544 for (txn_id, ce, added_runs) in &spilled_to_link {
13548 for ar in added_runs {
13549 let Some(handle) = tables.get(&ar.table_id) else {
13550 continue;
13551 };
13552 let mut t = handle.lock();
13553 let table_dir = Path::new(TABLES_DIR).join(ar.table_id.to_string());
13554 let destination = table_dir
13555 .join(crate::engine::RUNS_DIR)
13556 .join(format!("r-{}.sr", ar.run_id));
13557 match durable_root.open_regular(&destination) {
13558 Ok(_) => {}
13559 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
13560 let pending = table_dir
13561 .join("_txn")
13562 .join(txn_id.to_string())
13563 .join(format!("r-{}.sr", ar.run_id));
13564 durable_root.rename_file_new(&pending, &destination)?;
13565 }
13566 Err(error) => return Err(error.into()),
13567 }
13568 let linked = t.recover_spilled_run(crate::manifest::RunRef {
13574 run_id: ar.run_id,
13575 level: ar.level,
13576 epoch_created: *ce,
13577 row_count: ar.row_count,
13578 });
13579 let replaced = truncated_transactions.contains(&(*txn_id, ar.table_id));
13580 if replaced {
13581 t.set_flushed_epoch(Epoch(*ce));
13582 }
13583 if linked || replaced {
13584 t.persist_manifest(Epoch(*ce).max(epoch.visible()))?;
13585 }
13586 }
13587 }
13588
13589 epoch.advance_recovered(Epoch(max_epoch));
13590 Ok(())
13591}
13592
13593fn reconcile_recovered_table_metadata(
13594 tables: &HashMap<u64, TableHandle>,
13595 epoch: Epoch,
13596) -> Result<()> {
13597 let mut table_ids = tables.keys().copied().collect::<Vec<_>>();
13598 table_ids.sort_unstable();
13599 let mut plans = Vec::with_capacity(table_ids.len());
13600 for table_id in &table_ids {
13601 let handle = tables.get(table_id).ok_or_else(|| {
13602 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
13603 })?;
13604 plans.push((*table_id, handle.lock().plan_recovered_metadata()?));
13605 }
13606 for (table_id, plan) in plans {
13609 let handle = tables.get(&table_id).ok_or_else(|| {
13610 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
13611 })?;
13612 handle.lock().apply_recovered_metadata(plan, epoch)?;
13613 }
13614 Ok(())
13615}
13616
13617fn validate_recovered_external_name(name: &str) -> Result<()> {
13618 if name.is_empty()
13619 || !name.chars().all(|character| {
13620 character.is_ascii_alphanumeric() || character == '_' || character == '-'
13621 })
13622 {
13623 return Err(MongrelError::CorruptWal {
13624 offset: 0,
13625 reason: format!("unsafe recovered external-table name {name:?}"),
13626 });
13627 }
13628 Ok(())
13629}
13630
13631fn validate_recovery_table_stages(
13632 tables: &HashMap<u64, TableHandle>,
13633 stages: &HashMap<u64, RecoveryTableStage>,
13634) -> Result<()> {
13635 for (table_id, (rows, _, _, _)) in stages {
13636 let handle = tables
13637 .get(table_id)
13638 .ok_or_else(|| MongrelError::CorruptWal {
13639 offset: *table_id,
13640 reason: format!("recovery stage references unmounted table {table_id}"),
13641 })?;
13642 let table = handle.lock();
13643 table.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
13646 for row in rows {
13647 validate_recovered_row(table.schema(), row)?;
13648 }
13649 }
13650 Ok(())
13651}
13652
13653fn validate_recovered_row(schema: &Schema, row: &crate::memtable::Row) -> Result<()> {
13654 if row.deleted || row.row_id.0 == u64::MAX {
13655 return Err(MongrelError::CorruptWal {
13656 offset: row.row_id.0,
13657 reason: "committed Put payload contains a tombstone or exhausted row id".into(),
13658 });
13659 }
13660 let cells = row
13661 .columns
13662 .iter()
13663 .map(|(column, value)| (*column, value.clone()))
13664 .collect::<Vec<_>>();
13665 schema
13666 .validate_persisted_values(&cells)
13667 .map_err(|error| MongrelError::CorruptWal {
13668 offset: row.row_id.0,
13669 reason: format!("recovered row violates table schema: {error}"),
13670 })?;
13671 if schema.auto_increment_column().is_some_and(|column| {
13672 matches!(row.columns.get(&column.id), Some(Value::Int64(value)) if *value == i64::MAX)
13673 }) {
13674 return Err(MongrelError::CorruptWal {
13675 offset: row.row_id.0,
13676 reason: "recovered AUTO_INCREMENT value exhausts i64".into(),
13677 });
13678 }
13679 Ok(())
13680}
13681
13682fn validate_recovery_spilled_runs(
13683 root: &crate::durable_file::DurableRoot,
13684 tables: &HashMap<u64, TableHandle>,
13685 spilled: &[(u64, u64, Vec<crate::wal::AddedRun>)],
13686) -> Result<()> {
13687 let mut identities = HashSet::new();
13688 for (txn_id, commit_epoch, added_runs) in spilled {
13689 for added in added_runs {
13690 if added.run_id >= u64::MAX as u128 {
13691 return Err(MongrelError::CorruptWal {
13692 offset: *commit_epoch,
13693 reason: format!(
13694 "recovered run id {} exceeds the on-disk namespace",
13695 added.run_id
13696 ),
13697 });
13698 }
13699 let Some(handle) = tables.get(&added.table_id) else {
13700 continue;
13701 };
13702 if !identities.insert((added.table_id, added.run_id)) {
13703 return Err(MongrelError::CorruptWal {
13704 offset: *commit_epoch,
13705 reason: format!(
13706 "duplicate recovered run {} for table {}",
13707 added.run_id, added.table_id
13708 ),
13709 });
13710 }
13711 let table = handle.lock();
13712 validate_planned_spilled_run(
13713 root,
13714 *txn_id,
13715 *commit_epoch,
13716 added,
13717 table.schema(),
13718 table.kek(),
13719 )?;
13720 }
13721 }
13722 Ok(())
13723}
13724
13725fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
13726 match condition {
13727 ProcedureCondition::Pk { .. } => {
13728 if schema.primary_key().is_none() {
13729 return Err(MongrelError::InvalidArgument(
13730 "procedure condition Pk references a table without a primary key".into(),
13731 ));
13732 }
13733 }
13734 ProcedureCondition::BitmapEq { column_id, .. }
13735 | ProcedureCondition::BitmapIn { column_id, .. }
13736 | ProcedureCondition::Range { column_id, .. }
13737 | ProcedureCondition::RangeF64 { column_id, .. }
13738 | ProcedureCondition::IsNull { column_id }
13739 | ProcedureCondition::IsNotNull { column_id }
13740 | ProcedureCondition::FmContains { column_id, .. } => {
13741 validate_column_id(*column_id, schema)?;
13742 }
13743 }
13744 Ok(())
13745}
13746
13747fn bind_procedure_args(
13748 procedure: &StoredProcedure,
13749 mut args: HashMap<String, crate::Value>,
13750) -> Result<HashMap<String, crate::Value>> {
13751 let mut out = HashMap::new();
13752 for param in &procedure.params {
13753 let value = match args.remove(¶m.name) {
13754 Some(value) => value,
13755 None => param.default.clone().ok_or_else(|| {
13756 MongrelError::InvalidArgument(format!(
13757 "missing required procedure parameter {:?}",
13758 param.name
13759 ))
13760 })?,
13761 };
13762 if !param.nullable && matches!(value, crate::Value::Null) {
13763 return Err(MongrelError::InvalidArgument(format!(
13764 "procedure parameter {:?} must not be NULL",
13765 param.name
13766 )));
13767 }
13768 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty.clone()) {
13769 return Err(MongrelError::InvalidArgument(format!(
13770 "procedure parameter {:?} has wrong type",
13771 param.name
13772 )));
13773 }
13774 out.insert(param.name.clone(), value);
13775 }
13776 if let Some(extra) = args.keys().next() {
13777 return Err(MongrelError::InvalidArgument(format!(
13778 "unknown procedure parameter {extra:?}"
13779 )));
13780 }
13781 Ok(out)
13782}
13783
13784fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
13785 matches!(
13786 (value, ty),
13787 (crate::Value::Bool(_), crate::TypeId::Bool)
13788 | (crate::Value::Int64(_), crate::TypeId::Int8)
13789 | (crate::Value::Int64(_), crate::TypeId::Int16)
13790 | (crate::Value::Int64(_), crate::TypeId::Int32)
13791 | (crate::Value::Int64(_), crate::TypeId::Int64)
13792 | (crate::Value::Int64(_), crate::TypeId::UInt8)
13793 | (crate::Value::Int64(_), crate::TypeId::UInt16)
13794 | (crate::Value::Int64(_), crate::TypeId::UInt32)
13795 | (crate::Value::Int64(_), crate::TypeId::UInt64)
13796 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
13797 | (crate::Value::Int64(_), crate::TypeId::Date32)
13798 | (crate::Value::Float64(_), crate::TypeId::Float32)
13799 | (crate::Value::Float64(_), crate::TypeId::Float64)
13800 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
13801 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
13802 )
13803}
13804
13805fn eval_cells(
13806 cells: &[crate::procedure::ProcedureCell],
13807 args: &HashMap<String, crate::Value>,
13808 outputs: &HashMap<String, ProcedureCallOutput>,
13809) -> Result<Vec<(u16, crate::Value)>> {
13810 cells
13811 .iter()
13812 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
13813 .collect()
13814}
13815
13816fn eval_condition(
13817 condition: &ProcedureCondition,
13818 args: &HashMap<String, crate::Value>,
13819 outputs: &HashMap<String, ProcedureCallOutput>,
13820) -> Result<crate::Condition> {
13821 Ok(match condition {
13822 ProcedureCondition::Pk { value } => {
13823 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
13824 }
13825 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
13826 column_id: *column_id,
13827 value: eval_value(value, args, outputs)?.encode_key(),
13828 },
13829 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
13830 column_id: *column_id,
13831 values: values
13832 .iter()
13833 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
13834 .collect::<Result<Vec<_>>>()?,
13835 },
13836 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
13837 column_id: *column_id,
13838 lo: expect_i64(eval_value(lo, args, outputs)?)?,
13839 hi: expect_i64(eval_value(hi, args, outputs)?)?,
13840 },
13841 ProcedureCondition::RangeF64 {
13842 column_id,
13843 lo,
13844 lo_inclusive,
13845 hi,
13846 hi_inclusive,
13847 } => crate::Condition::RangeF64 {
13848 column_id: *column_id,
13849 lo: expect_f64(eval_value(lo, args, outputs)?)?,
13850 lo_inclusive: *lo_inclusive,
13851 hi: expect_f64(eval_value(hi, args, outputs)?)?,
13852 hi_inclusive: *hi_inclusive,
13853 },
13854 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
13855 column_id: *column_id,
13856 },
13857 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
13858 column_id: *column_id,
13859 },
13860 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
13861 column_id: *column_id,
13862 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
13863 },
13864 })
13865}
13866
13867fn eval_value(
13868 value: &ProcedureValue,
13869 args: &HashMap<String, crate::Value>,
13870 outputs: &HashMap<String, ProcedureCallOutput>,
13871) -> Result<crate::Value> {
13872 match value {
13873 ProcedureValue::Literal(value) => Ok(value.clone()),
13874 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
13875 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
13876 }),
13877 ProcedureValue::StepScalar(id) => match outputs.get(id) {
13878 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
13879 _ => Err(MongrelError::InvalidArgument(format!(
13880 "procedure step {id:?} did not return a scalar"
13881 ))),
13882 },
13883 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
13884 Err(MongrelError::InvalidArgument(
13885 "row-valued procedure reference cannot be used as a scalar".into(),
13886 ))
13887 }
13888 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
13889 "structured procedure value cannot be used as a scalar cell".into(),
13890 )),
13891 }
13892}
13893
13894fn eval_return_output(
13895 value: &ProcedureValue,
13896 args: &HashMap<String, crate::Value>,
13897 outputs: &HashMap<String, ProcedureCallOutput>,
13898) -> Result<ProcedureCallOutput> {
13899 match value {
13900 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
13901 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
13902 args.get(name).cloned().ok_or_else(|| {
13903 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
13904 })?,
13905 )),
13906 ProcedureValue::StepRows(id)
13907 | ProcedureValue::StepRow(id)
13908 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
13909 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
13910 }),
13911 ProcedureValue::Object(fields) => {
13912 let mut out = Vec::with_capacity(fields.len());
13913 for (name, value) in fields {
13914 out.push((name.clone(), eval_return_output(value, args, outputs)?));
13915 }
13916 Ok(ProcedureCallOutput::Object(out))
13917 }
13918 ProcedureValue::Array(values) => {
13919 let mut out = Vec::with_capacity(values.len());
13920 for value in values {
13921 out.push(eval_return_output(value, args, outputs)?);
13922 }
13923 Ok(ProcedureCallOutput::Array(out))
13924 }
13925 }
13926}
13927
13928fn expect_i64(value: crate::Value) -> Result<i64> {
13929 match value {
13930 crate::Value::Int64(value) => Ok(value),
13931 _ => Err(MongrelError::InvalidArgument(
13932 "procedure value must be Int64".into(),
13933 )),
13934 }
13935}
13936
13937fn expect_f64(value: crate::Value) -> Result<f64> {
13938 match value {
13939 crate::Value::Float64(value) => Ok(value),
13940 _ => Err(MongrelError::InvalidArgument(
13941 "procedure value must be Float64".into(),
13942 )),
13943 }
13944}
13945
13946fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
13947 match value {
13948 crate::Value::Bytes(value) => Ok(value),
13949 _ => Err(MongrelError::InvalidArgument(
13950 "procedure value must be Bytes".into(),
13951 )),
13952 }
13953}
13954
13955fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
13956 if schema.columns.iter().any(|c| c.id == column_id) {
13957 Ok(())
13958 } else {
13959 Err(MongrelError::InvalidArgument(format!(
13960 "unknown column id {column_id}"
13961 )))
13962 }
13963}
13964
13965fn trigger_matches_event(
13966 trigger: &StoredTrigger,
13967 event: &WriteEvent,
13968 cat: &Catalog,
13969) -> Result<bool> {
13970 if trigger.event != event.kind {
13971 return Ok(false);
13972 }
13973 let TriggerTarget::Table(target) = &trigger.target else {
13974 return Ok(false);
13975 };
13976 if target != &event.table {
13977 return Ok(false);
13978 }
13979 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
13980 let schema = &cat
13981 .live(target)
13982 .ok_or_else(|| {
13983 MongrelError::InvalidArgument(format!(
13984 "trigger {:?} references unknown table {target:?}",
13985 trigger.name
13986 ))
13987 })?
13988 .schema;
13989 let mut watched = Vec::with_capacity(trigger.update_of.len());
13990 for name in &trigger.update_of {
13991 let col = schema.column(name).ok_or_else(|| {
13992 MongrelError::InvalidArgument(format!(
13993 "trigger {:?} references unknown UPDATE OF column {name:?}",
13994 trigger.name
13995 ))
13996 })?;
13997 watched.push(col.id);
13998 }
13999 if !event
14000 .changed_columns
14001 .iter()
14002 .any(|column_id| watched.contains(column_id))
14003 {
14004 return Ok(false);
14005 }
14006 }
14007 Ok(true)
14008}
14009
14010fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
14011 let mut ids = std::collections::BTreeSet::new();
14012 if let Some(old) = old {
14013 ids.extend(old.columns.keys().copied());
14014 }
14015 if let Some(new) = new {
14016 ids.extend(new.columns.keys().copied());
14017 }
14018 ids.into_iter()
14019 .filter(|id| {
14020 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
14021 })
14022 .collect()
14023}
14024
14025fn eval_trigger_cells(
14026 cells: &[crate::trigger::TriggerCell],
14027 event: &WriteEvent,
14028 selected: Option<&TriggerRowImage>,
14029) -> Result<Vec<(u16, Value)>> {
14030 cells
14031 .iter()
14032 .map(|cell| {
14033 Ok((
14034 cell.column_id,
14035 eval_trigger_value(&cell.value, event, selected)?,
14036 ))
14037 })
14038 .collect()
14039}
14040
14041fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
14042 match expr {
14043 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
14044 Value::Bool(value) => Ok(value),
14045 Value::Null => Ok(false),
14046 other => Err(MongrelError::InvalidArgument(format!(
14047 "trigger WHEN value must be boolean, got {other:?}"
14048 ))),
14049 },
14050 TriggerExpr::Eq { left, right } => Ok(values_equal(
14051 &eval_trigger_value(left, event, None)?,
14052 &eval_trigger_value(right, event, None)?,
14053 )),
14054 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
14055 &eval_trigger_value(left, event, None)?,
14056 &eval_trigger_value(right, event, None)?,
14057 )),
14058 TriggerExpr::Lt { left, right } => match value_order(
14059 &eval_trigger_value(left, event, None)?,
14060 &eval_trigger_value(right, event, None)?,
14061 ) {
14062 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
14063 None => Ok(false),
14064 },
14065 TriggerExpr::Lte { left, right } => match value_order(
14066 &eval_trigger_value(left, event, None)?,
14067 &eval_trigger_value(right, event, None)?,
14068 ) {
14069 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
14070 None => Ok(false),
14071 },
14072 TriggerExpr::Gt { left, right } => match value_order(
14073 &eval_trigger_value(left, event, None)?,
14074 &eval_trigger_value(right, event, None)?,
14075 ) {
14076 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
14077 None => Ok(false),
14078 },
14079 TriggerExpr::Gte { left, right } => match value_order(
14080 &eval_trigger_value(left, event, None)?,
14081 &eval_trigger_value(right, event, None)?,
14082 ) {
14083 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
14084 None => Ok(false),
14085 },
14086 TriggerExpr::IsNull(value) => Ok(matches!(
14087 eval_trigger_value(value, event, None)?,
14088 Value::Null
14089 )),
14090 TriggerExpr::IsNotNull(value) => Ok(!matches!(
14091 eval_trigger_value(value, event, None)?,
14092 Value::Null
14093 )),
14094 TriggerExpr::And { left, right } => {
14095 if !eval_trigger_expr(left, event)? {
14096 Ok(false)
14097 } else {
14098 Ok(eval_trigger_expr(right, event)?)
14099 }
14100 }
14101 TriggerExpr::Or { left, right } => {
14102 if eval_trigger_expr(left, event)? {
14103 Ok(true)
14104 } else {
14105 Ok(eval_trigger_expr(right, event)?)
14106 }
14107 }
14108 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
14109 }
14110}
14111
14112fn eval_trigger_condition(
14113 condition: &TriggerCondition,
14114 event: &WriteEvent,
14115 selected: &TriggerRowImage,
14116 schema: &Schema,
14117) -> Result<bool> {
14118 match condition {
14119 TriggerCondition::Pk { value } => {
14120 let pk = schema.primary_key().ok_or_else(|| {
14121 MongrelError::InvalidArgument(
14122 "trigger condition Pk references a table without a primary key".into(),
14123 )
14124 })?;
14125 let lhs = eval_trigger_value(value, event, Some(selected))?;
14126 Ok(values_equal(
14127 &lhs,
14128 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
14129 ))
14130 }
14131 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
14132 selected.columns.get(column_id).unwrap_or(&Value::Null),
14133 &eval_trigger_value(value, event, Some(selected))?,
14134 )),
14135 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
14136 selected.columns.get(column_id).unwrap_or(&Value::Null),
14137 &eval_trigger_value(value, event, Some(selected))?,
14138 )),
14139 TriggerCondition::Lt { column_id, value } => match value_order(
14140 selected.columns.get(column_id).unwrap_or(&Value::Null),
14141 &eval_trigger_value(value, event, Some(selected))?,
14142 ) {
14143 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
14144 None => Ok(false),
14145 },
14146 TriggerCondition::Lte { column_id, value } => match value_order(
14147 selected.columns.get(column_id).unwrap_or(&Value::Null),
14148 &eval_trigger_value(value, event, Some(selected))?,
14149 ) {
14150 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
14151 None => Ok(false),
14152 },
14153 TriggerCondition::Gt { column_id, value } => match value_order(
14154 selected.columns.get(column_id).unwrap_or(&Value::Null),
14155 &eval_trigger_value(value, event, Some(selected))?,
14156 ) {
14157 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
14158 None => Ok(false),
14159 },
14160 TriggerCondition::Gte { column_id, value } => match value_order(
14161 selected.columns.get(column_id).unwrap_or(&Value::Null),
14162 &eval_trigger_value(value, event, Some(selected))?,
14163 ) {
14164 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
14165 None => Ok(false),
14166 },
14167 TriggerCondition::IsNull { column_id } => Ok(matches!(
14168 selected.columns.get(column_id),
14169 None | Some(Value::Null)
14170 )),
14171 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
14172 selected.columns.get(column_id),
14173 None | Some(Value::Null)
14174 )),
14175 TriggerCondition::And { left, right } => {
14176 if !eval_trigger_condition(left, event, selected, schema)? {
14177 Ok(false)
14178 } else {
14179 Ok(eval_trigger_condition(right, event, selected, schema)?)
14180 }
14181 }
14182 TriggerCondition::Or { left, right } => {
14183 if eval_trigger_condition(left, event, selected, schema)? {
14184 Ok(true)
14185 } else {
14186 Ok(eval_trigger_condition(right, event, selected, schema)?)
14187 }
14188 }
14189 TriggerCondition::Not(condition) => {
14190 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
14191 }
14192 }
14193}
14194
14195fn eval_trigger_value(
14196 value: &TriggerValue,
14197 event: &WriteEvent,
14198 selected: Option<&TriggerRowImage>,
14199) -> Result<Value> {
14200 match value {
14201 TriggerValue::Literal(value) => Ok(value.clone()),
14202 TriggerValue::NewColumn(column_id) => event
14203 .new
14204 .as_ref()
14205 .and_then(|row| row.columns.get(column_id))
14206 .cloned()
14207 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
14208 TriggerValue::OldColumn(column_id) => event
14209 .old
14210 .as_ref()
14211 .and_then(|row| row.columns.get(column_id))
14212 .cloned()
14213 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
14214 TriggerValue::SelectedColumn(column_id) => selected
14215 .and_then(|row| row.columns.get(column_id))
14216 .cloned()
14217 .ok_or_else(|| {
14218 MongrelError::InvalidArgument("SELECTED column is not available".into())
14219 }),
14220 }
14221}
14222
14223fn values_equal(left: &Value, right: &Value) -> bool {
14224 match (left, right) {
14225 (Value::Null, Value::Null) => true,
14226 (Value::Bool(a), Value::Bool(b)) => a == b,
14227 (Value::Int64(a), Value::Int64(b)) => a == b,
14228 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
14229 (Value::Bytes(a), Value::Bytes(b)) => a == b,
14230 (Value::Embedding(a), Value::Embedding(b)) => {
14231 a.len() == b.len()
14232 && a.iter()
14233 .zip(b.iter())
14234 .all(|(a, b)| a.to_bits() == b.to_bits())
14235 }
14236 _ => false,
14237 }
14238}
14239
14240fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
14241 match (left, right) {
14242 (Value::Null, _) | (_, Value::Null) => None,
14243 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
14244 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
14245 (Value::Int64(a), Value::Float64(b)) => {
14248 let af = *a as f64;
14249 Some(af.total_cmp(b))
14250 }
14251 (Value::Float64(a), Value::Int64(b)) => {
14254 let bf = *b as f64;
14255 Some(a.total_cmp(&bf))
14256 }
14257 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
14258 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
14259 (Value::Embedding(_), Value::Embedding(_)) => None,
14260 _ => None,
14261 }
14262}
14263
14264fn trigger_message(value: Value) -> String {
14265 match value {
14266 Value::Null => "NULL".into(),
14267 Value::Bool(value) => value.to_string(),
14268 Value::Int64(value) => value.to_string(),
14269 Value::Float64(value) => value.to_string(),
14270 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
14271 Value::Embedding(value) => format!("{value:?}"),
14272 Value::Decimal(value) => value.to_string(),
14273 Value::Interval {
14274 months,
14275 days,
14276 nanos,
14277 } => format!("{months}m {days}d {nanos}ns"),
14278 Value::Uuid(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
14279 Value::Json(b) => String::from_utf8_lossy(&b).into_owned(),
14280 }
14281}
14282
14283fn validate_trigger_step<'a>(
14284 step: &TriggerStep,
14285 cat: &'a Catalog,
14286 target_schema: &Schema,
14287 event: TriggerEvent,
14288 select_schemas: &mut HashMap<String, &'a Schema>,
14289) -> Result<()> {
14290 match step {
14291 TriggerStep::SetNew { cells } => {
14292 if event == TriggerEvent::Delete {
14293 return Err(MongrelError::InvalidArgument(
14294 "SetNew trigger step is not valid for DELETE triggers".into(),
14295 ));
14296 }
14297 for cell in cells {
14298 validate_column_id(cell.column_id, target_schema)?;
14299 validate_trigger_value(&cell.value, target_schema, event)?;
14300 }
14301 }
14302 TriggerStep::Insert { table, cells } => {
14303 let schema = trigger_write_schema(cat, table, "insert")?;
14304 for cell in cells {
14305 validate_column_id(cell.column_id, schema)?;
14306 validate_trigger_value(&cell.value, target_schema, event)?;
14307 }
14308 }
14309 TriggerStep::UpdateByPk { table, pk, cells } => {
14310 let schema = trigger_write_schema(cat, table, "update")?;
14311 if schema.primary_key().is_none() {
14312 return Err(MongrelError::InvalidArgument(format!(
14313 "trigger update_by_pk references table {table:?} without a primary key"
14314 )));
14315 }
14316 validate_trigger_value(pk, target_schema, event)?;
14317 for cell in cells {
14318 validate_column_id(cell.column_id, schema)?;
14319 validate_trigger_value(&cell.value, target_schema, event)?;
14320 }
14321 }
14322 TriggerStep::DeleteByPk { table, pk } => {
14323 let schema = trigger_write_schema(cat, table, "delete")?;
14324 if schema.primary_key().is_none() {
14325 return Err(MongrelError::InvalidArgument(format!(
14326 "trigger delete_by_pk references table {table:?} without a primary key"
14327 )));
14328 }
14329 validate_trigger_value(pk, target_schema, event)?;
14330 }
14331 TriggerStep::Select {
14332 id,
14333 table,
14334 conditions,
14335 } => {
14336 let schema = trigger_read_schema(cat, table)?;
14337 for condition in conditions {
14338 validate_trigger_condition(condition, schema, target_schema, event)?;
14339 }
14340 if select_schemas.contains_key(id) {
14341 return Err(MongrelError::InvalidArgument(format!(
14342 "duplicate select id {id:?} in trigger program"
14343 )));
14344 }
14345 select_schemas.insert(id.clone(), schema);
14346 }
14347 TriggerStep::Foreach { id, steps } => {
14348 if !select_schemas.contains_key(id) {
14349 return Err(MongrelError::InvalidArgument(format!(
14350 "foreach references unknown select id {id:?}"
14351 )));
14352 }
14353 let mut inner_select_schemas = select_schemas.clone();
14354 for step in steps {
14355 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
14356 }
14357 }
14358 TriggerStep::DeleteWhere { table, conditions } => {
14359 let schema = trigger_write_schema(cat, table, "delete")?;
14360 for condition in conditions {
14361 validate_trigger_condition(condition, schema, target_schema, event)?;
14362 }
14363 }
14364 TriggerStep::UpdateWhere {
14365 table,
14366 conditions,
14367 cells,
14368 } => {
14369 let schema = trigger_write_schema(cat, table, "update")?;
14370 for condition in conditions {
14371 validate_trigger_condition(condition, schema, target_schema, event)?;
14372 }
14373 for cell in cells {
14374 validate_column_id(cell.column_id, schema)?;
14375 validate_trigger_value(&cell.value, target_schema, event)?;
14376 }
14377 }
14378 TriggerStep::Raise { message, .. } => {
14379 validate_trigger_value(message, target_schema, event)?
14380 }
14381 }
14382 Ok(())
14383}
14384
14385fn trigger_validation_error(error: MongrelError) -> MongrelError {
14386 match error {
14387 MongrelError::TriggerValidation(_) => error,
14388 MongrelError::InvalidArgument(message)
14389 | MongrelError::Conflict(message)
14390 | MongrelError::NotFound(message) => MongrelError::TriggerValidation(message),
14391 error => error,
14392 }
14393}
14394
14395fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
14396 if let Some(entry) = cat.live(table) {
14397 return Ok(&entry.schema);
14398 }
14399 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
14400 let allowed = match op {
14401 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
14402 "update" | "delete" => entry.capabilities.writable,
14403 _ => false,
14404 };
14405 if !allowed {
14406 return Err(MongrelError::InvalidArgument(format!(
14407 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
14408 entry.module
14409 )));
14410 }
14411 if !entry.capabilities.transaction_safe {
14412 return Err(MongrelError::InvalidArgument(format!(
14413 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
14414 entry.module
14415 )));
14416 }
14417 return Ok(&entry.declared_schema);
14418 }
14419 Err(MongrelError::InvalidArgument(format!(
14420 "trigger references unknown table {table:?}"
14421 )))
14422}
14423
14424fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
14425 if let Some(entry) = cat.live(table) {
14426 return Ok(&entry.schema);
14427 }
14428 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
14429 if entry.capabilities.trigger_safe {
14430 return Ok(&entry.declared_schema);
14431 }
14432 return Err(MongrelError::InvalidArgument(format!(
14433 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
14434 entry.module
14435 )));
14436 }
14437 Err(MongrelError::InvalidArgument(format!(
14438 "trigger references unknown table {table:?}"
14439 )))
14440}
14441
14442fn validate_trigger_condition(
14443 condition: &TriggerCondition,
14444 schema: &Schema,
14445 target_schema: &Schema,
14446 event: TriggerEvent,
14447) -> Result<()> {
14448 match condition {
14449 TriggerCondition::Pk { value } => {
14450 if schema.primary_key().is_none() {
14451 return Err(MongrelError::InvalidArgument(
14452 "trigger condition Pk references a table without a primary key".into(),
14453 ));
14454 }
14455 validate_trigger_value(value, target_schema, event)
14456 }
14457 TriggerCondition::Eq { column_id, value }
14458 | TriggerCondition::NotEq { column_id, value }
14459 | TriggerCondition::Lt { column_id, value }
14460 | TriggerCondition::Lte { column_id, value }
14461 | TriggerCondition::Gt { column_id, value }
14462 | TriggerCondition::Gte { column_id, value } => {
14463 validate_column_id(*column_id, schema)?;
14464 validate_trigger_value(value, target_schema, event)
14465 }
14466 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
14467 validate_column_id(*column_id, schema)
14468 }
14469 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
14470 validate_trigger_condition(left, schema, target_schema, event)?;
14471 validate_trigger_condition(right, schema, target_schema, event)
14472 }
14473 TriggerCondition::Not(condition) => {
14474 validate_trigger_condition(condition, schema, target_schema, event)
14475 }
14476 }
14477}
14478
14479fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
14480 match expr {
14481 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
14482 validate_trigger_value(value, schema, event)
14483 }
14484 TriggerExpr::Eq { left, right }
14485 | TriggerExpr::NotEq { left, right }
14486 | TriggerExpr::Lt { left, right }
14487 | TriggerExpr::Lte { left, right }
14488 | TriggerExpr::Gt { left, right }
14489 | TriggerExpr::Gte { left, right } => {
14490 validate_trigger_value(left, schema, event)?;
14491 validate_trigger_value(right, schema, event)
14492 }
14493 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
14494 validate_trigger_expr(left, schema, event)?;
14495 validate_trigger_expr(right, schema, event)
14496 }
14497 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
14498 }
14499}
14500
14501fn validate_trigger_value(
14502 value: &TriggerValue,
14503 schema: &Schema,
14504 event: TriggerEvent,
14505) -> Result<()> {
14506 match value {
14507 TriggerValue::Literal(_) => Ok(()),
14508 TriggerValue::NewColumn(id) => {
14509 if event == TriggerEvent::Delete {
14510 return Err(MongrelError::InvalidArgument(
14511 "DELETE triggers cannot reference NEW".into(),
14512 ));
14513 }
14514 validate_column_id(*id, schema)
14515 }
14516 TriggerValue::OldColumn(id) => {
14517 if event == TriggerEvent::Insert {
14518 return Err(MongrelError::InvalidArgument(
14519 "INSERT triggers cannot reference OLD".into(),
14520 ));
14521 }
14522 validate_column_id(*id, schema)
14523 }
14524 TriggerValue::SelectedColumn(_) => Ok(()),
14528 }
14529}
14530
14531fn recover_ddl_from_wal(
14537 root: &Path,
14538 durable_root: Option<&crate::durable_file::DurableRoot>,
14539 target_catalog: &mut Catalog,
14540 meta_dek: Option<&[u8; META_DEK_LEN]>,
14541 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
14542 apply: bool,
14543 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
14544) -> Result<()> {
14545 use crate::wal::SharedWal;
14546 let records = match durable_root {
14547 Some(root) => SharedWal::replay_durable_with_dek(root, wal_dek)?,
14548 None => SharedWal::replay_with_dek(root, wal_dek)?,
14549 };
14550 recover_ddl_from_records(
14551 root,
14552 durable_root,
14553 target_catalog,
14554 meta_dek,
14555 apply,
14556 table_roots,
14557 &records,
14558 )
14559}
14560
14561fn recover_ddl_from_records(
14562 root: &Path,
14563 durable_root: Option<&crate::durable_file::DurableRoot>,
14564 target_catalog: &mut Catalog,
14565 meta_dek: Option<&[u8; META_DEK_LEN]>,
14566 apply: bool,
14567 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
14568 records: &[crate::wal::Record],
14569) -> Result<()> {
14570 use crate::wal::{DdlOp, Op};
14571
14572 let original_catalog = target_catalog.clone();
14573 let mut recovered_catalog = original_catalog.clone();
14574 let cat = &mut recovered_catalog;
14575 let mut created_table_ids = HashSet::<u64>::new();
14576 let mut ttl_updates = HashMap::<u64, (Option<crate::manifest::TtlPolicy>, u64)>::new();
14577
14578 let mut committed: HashMap<u64, u64> = HashMap::new();
14579 for r in records {
14580 if let Op::TxnCommit { epoch: ce, .. } = r.op {
14581 committed.insert(r.txn_id, ce);
14582 }
14583 }
14584 let catalog_snapshot_txns = records
14585 .iter()
14586 .filter_map(|record| {
14587 (committed.contains_key(&record.txn_id)
14588 && matches!(&record.op, Op::Ddl(DdlOp::CatalogSnapshot { .. })))
14589 .then_some(record.txn_id)
14590 })
14591 .collect::<HashSet<_>>();
14592
14593 let mut changed = false;
14594 let mut applied_catalog_epoch = cat.db_epoch;
14595 let max_committed_epoch = committed.values().copied().max().unwrap_or(cat.db_epoch);
14596 for r in records.iter().cloned() {
14597 let Some(&ce) = committed.get(&r.txn_id) else {
14598 continue;
14599 };
14600 let txn_id = r.txn_id;
14601 match r.op {
14602 Op::Ddl(DdlOp::CreateTable {
14603 table_id,
14604 ref name,
14605 ref schema_json,
14606 }) => {
14607 if cat.tables.iter().any(|t| t.table_id == table_id) {
14608 continue;
14609 }
14610 let schema = DdlOp::decode_schema(schema_json)?;
14611 validate_recovered_schema(&schema)?;
14612 created_table_ids.insert(table_id);
14613 cat.tables.push(CatalogEntry {
14614 table_id,
14615 name: name.clone(),
14616 schema,
14617 state: TableState::Live,
14618 created_epoch: ce,
14619 });
14620 cat.next_table_id =
14621 cat.next_table_id
14622 .max(table_id.checked_add(1).ok_or_else(|| {
14623 MongrelError::Full("table id namespace exhausted".into())
14624 })?);
14625 changed = true;
14626 }
14627 Op::Ddl(DdlOp::CreateBuildingTable {
14628 table_id,
14629 ref build_name,
14630 ref intended_name,
14631 ref query_id,
14632 created_at_unix_nanos,
14633 ref schema_json,
14634 }) => {
14635 if cat.tables.iter().any(|table| table.table_id == table_id) {
14636 continue;
14637 }
14638 let schema = DdlOp::decode_schema(schema_json)?;
14639 validate_recovered_schema(&schema)?;
14640 created_table_ids.insert(table_id);
14641 cat.tables.push(CatalogEntry {
14642 table_id,
14643 name: build_name.clone(),
14644 schema,
14645 state: TableState::Building {
14646 intended_name: intended_name.clone(),
14647 query_id: query_id.clone(),
14648 created_at_unix_nanos,
14649 replaces_table_id: None,
14650 },
14651 created_epoch: ce,
14652 });
14653 cat.next_table_id =
14654 cat.next_table_id
14655 .max(table_id.checked_add(1).ok_or_else(|| {
14656 MongrelError::Full("table id namespace exhausted".into())
14657 })?);
14658 changed = true;
14659 }
14660 Op::Ddl(DdlOp::CreateRebuildingTable {
14661 table_id,
14662 ref build_name,
14663 ref intended_name,
14664 ref query_id,
14665 created_at_unix_nanos,
14666 replaces_table_id,
14667 ref schema_json,
14668 }) => {
14669 if cat.tables.iter().any(|table| table.table_id == table_id) {
14670 continue;
14671 }
14672 let schema = DdlOp::decode_schema(schema_json)?;
14673 validate_recovered_schema(&schema)?;
14674 created_table_ids.insert(table_id);
14675 cat.tables.push(CatalogEntry {
14676 table_id,
14677 name: build_name.clone(),
14678 schema,
14679 state: TableState::Building {
14680 intended_name: intended_name.clone(),
14681 query_id: query_id.clone(),
14682 created_at_unix_nanos,
14683 replaces_table_id: Some(replaces_table_id),
14684 },
14685 created_epoch: ce,
14686 });
14687 cat.next_table_id =
14688 cat.next_table_id
14689 .max(table_id.checked_add(1).ok_or_else(|| {
14690 MongrelError::Full("table id namespace exhausted".into())
14691 })?);
14692 changed = true;
14693 }
14694 Op::Ddl(DdlOp::DropTable { table_id }) => {
14695 let mut dropped_name = None;
14696 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
14697 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
14698 dropped_name = Some(entry.name.clone());
14699 entry.state = TableState::Dropped { at_epoch: ce };
14700 changed = true;
14701 }
14702 }
14703 if let Some(name) = dropped_name {
14704 let before = cat.materialized_views.len();
14705 cat.materialized_views
14706 .retain(|definition| definition.name != name);
14707 changed |= before != cat.materialized_views.len();
14708 cat.security.rls_tables.retain(|table| table != &name);
14709 cat.security.policies.retain(|policy| policy.table != name);
14710 cat.security.masks.retain(|mask| mask.table != name);
14711 for role in &mut cat.roles {
14712 role.permissions
14713 .retain(|permission| permission_table(permission) != Some(&name));
14714 }
14715 if !catalog_snapshot_txns.contains(&txn_id) {
14716 advance_security_version(cat)?;
14717 }
14718 }
14719 }
14720 Op::Ddl(DdlOp::PublishBuildingTable {
14721 table_id,
14722 ref new_name,
14723 }) => {
14724 if let Some(entry) = cat
14725 .tables
14726 .iter_mut()
14727 .find(|table| table.table_id == table_id)
14728 {
14729 if entry.name != *new_name || !matches!(entry.state, TableState::Live) {
14730 entry.name = new_name.clone();
14731 entry.state = TableState::Live;
14732 changed = true;
14733 }
14734 }
14735 }
14736 Op::Ddl(DdlOp::ReplaceBuildingTable {
14737 table_id,
14738 replaced_table_id,
14739 ref new_name,
14740 }) => {
14741 changed |=
14742 apply_rebuilding_publish(cat, table_id, replaced_table_id, new_name, ce)?;
14743 }
14744 Op::Ddl(DdlOp::RenameTable {
14745 table_id,
14746 ref new_name,
14747 }) => {
14748 let mut old_name = None;
14749 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
14750 if entry.name != *new_name {
14751 old_name = Some(entry.name.clone());
14752 entry.name = new_name.clone();
14753 changed = true;
14754 }
14755 }
14756 if let Some(old_name) = old_name {
14757 if let Some(definition) = cat
14758 .materialized_views
14759 .iter_mut()
14760 .find(|definition| definition.name == old_name)
14761 {
14762 definition.name = new_name.clone();
14763 }
14764 for table in &mut cat.security.rls_tables {
14765 if *table == old_name {
14766 *table = new_name.clone();
14767 }
14768 }
14769 for policy in &mut cat.security.policies {
14770 if policy.table == old_name {
14771 policy.table = new_name.clone();
14772 }
14773 }
14774 for mask in &mut cat.security.masks {
14775 if mask.table == old_name {
14776 mask.table = new_name.clone();
14777 }
14778 }
14779 for role in &mut cat.roles {
14780 for permission in &mut role.permissions {
14781 rename_permission_table(permission, &old_name, new_name);
14782 }
14783 }
14784 if !catalog_snapshot_txns.contains(&txn_id) {
14785 advance_security_version(cat)?;
14786 }
14787 }
14788 }
14792 Op::Ddl(DdlOp::AlterTable {
14793 table_id,
14794 ref column_json,
14795 }) => {
14796 let column = DdlOp::decode_column(column_json)?;
14797 let mut renamed = None;
14798 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
14799 renamed = entry
14800 .schema
14801 .columns
14802 .iter()
14803 .find(|existing| existing.id == column.id && existing.name != column.name)
14804 .map(|existing| {
14805 (
14806 entry.name.clone(),
14807 existing.name.clone(),
14808 column.name.clone(),
14809 )
14810 });
14811 if apply_recovered_column_def(&mut entry.schema, column)? {
14812 validate_recovered_schema(&entry.schema)?;
14813 changed = true;
14814 }
14815 }
14816 if let Some((table, old_name, new_name)) = renamed {
14817 for role in &mut cat.roles {
14818 for permission in &mut role.permissions {
14819 rename_permission_column(permission, &table, &old_name, &new_name);
14820 }
14821 }
14822 if !catalog_snapshot_txns.contains(&txn_id) {
14823 advance_security_version(cat)?;
14824 }
14825 }
14826 }
14827 Op::Ddl(DdlOp::SetTtl {
14828 table_id,
14829 ref policy_json,
14830 }) => {
14831 let policy = DdlOp::decode_ttl(policy_json)?;
14832 let entry = cat
14833 .tables
14834 .iter()
14835 .find(|entry| entry.table_id == table_id)
14836 .ok_or_else(|| {
14837 MongrelError::Schema(format!(
14838 "recovered TTL references unknown table id {table_id}"
14839 ))
14840 })?;
14841 if let Some(policy) = policy {
14842 let valid = entry
14843 .schema
14844 .columns
14845 .iter()
14846 .find(|column| column.id == policy.column_id)
14847 .is_some_and(|column| {
14848 column.ty == TypeId::TimestampNanos
14849 && policy.duration_nanos > 0
14850 && policy.duration_nanos <= i64::MAX as u64
14851 });
14852 if !valid {
14853 return Err(MongrelError::Schema(format!(
14854 "invalid recovered TTL policy for table id {table_id}"
14855 )));
14856 }
14857 }
14858 ttl_updates.insert(table_id, (policy, ce));
14859 }
14860 Op::Ddl(DdlOp::SetMaterializedView {
14861 ref name,
14862 ref definition_json,
14863 }) => {
14864 let definition = DdlOp::decode_materialized_view(definition_json)?;
14865 if definition.name != *name {
14866 return Err(MongrelError::Schema(format!(
14867 "materialized view WAL name mismatch: {name:?}"
14868 )));
14869 }
14870 if cat.live(name).is_some() {
14871 if let Some(existing) = cat
14872 .materialized_views
14873 .iter_mut()
14874 .find(|existing| existing.name == *name)
14875 {
14876 if *existing != definition {
14877 *existing = definition;
14878 changed = true;
14879 }
14880 } else {
14881 cat.materialized_views.push(definition);
14882 changed = true;
14883 }
14884 }
14885 }
14886 Op::Ddl(DdlOp::SetSecurityCatalog { ref security_json }) => {
14887 let security = DdlOp::decode_security(security_json)?;
14888 validate_security_catalog(cat, &security)?;
14889 if cat.security != security {
14890 cat.security = security;
14891 if !catalog_snapshot_txns.contains(&txn_id) {
14892 advance_security_version(cat)?;
14893 }
14894 changed = true;
14895 }
14896 }
14897 Op::Ddl(DdlOp::SetSqlPragma { ref key, value }) => {
14898 let target = match key.as_str() {
14899 "user_version" => &mut cat.user_version,
14900 "application_id" => &mut cat.application_id,
14901 _ => {
14902 return Err(MongrelError::InvalidArgument(format!(
14903 "unsupported recovered SQL pragma {key:?}"
14904 )))
14905 }
14906 };
14907 if *target != Some(value) {
14908 *target = Some(value);
14909 cat.db_epoch = cat.db_epoch.max(ce);
14910 changed = true;
14911 }
14912 }
14913 Op::Ddl(DdlOp::CatalogSnapshot { ref catalog_json }) => {
14914 if ce <= applied_catalog_epoch {
14915 continue;
14916 }
14917 let snapshot = DdlOp::decode_catalog(catalog_json)?;
14918 if snapshot.db_epoch != ce {
14919 return Err(MongrelError::Schema(format!(
14920 "catalog snapshot epoch {} does not match WAL commit epoch {ce}",
14921 snapshot.db_epoch
14922 )));
14923 }
14924 validate_recovered_catalog(&snapshot)?;
14925 validate_catalog_transition(cat, &snapshot)?;
14926 *cat = snapshot;
14927 applied_catalog_epoch = ce;
14928 changed = true;
14929 }
14930 _ => {}
14931 }
14932 }
14933
14934 if cat.db_epoch < max_committed_epoch {
14935 cat.db_epoch = max_committed_epoch;
14936 changed = true;
14937 }
14938 changed |= repair_catalog_allocator_counters(cat)?;
14939
14940 validate_recovered_catalog(cat)?;
14941 let storage_reconciliation = validate_recovered_storage_plan(
14942 root,
14943 durable_root,
14944 cat,
14945 &created_table_ids,
14946 &ttl_updates,
14947 meta_dek,
14948 )?;
14949
14950 let needs_storage_apply = !storage_reconciliation.is_empty() || !ttl_updates.is_empty();
14951 if apply && (changed || needs_storage_apply) {
14952 for table_id in storage_reconciliation {
14953 let entry = cat
14954 .tables
14955 .iter()
14956 .find(|entry| entry.table_id == table_id)
14957 .ok_or_else(|| MongrelError::CorruptWal {
14958 offset: table_id,
14959 reason: "recovery storage plan lost its catalog table".into(),
14960 })?;
14961 ensure_recovered_table_storage(
14962 table_roots
14963 .and_then(|roots| roots.get(&table_id))
14964 .map(Arc::as_ref),
14965 durable_root,
14966 &root.join(TABLES_DIR).join(table_id.to_string()),
14967 table_id,
14968 &entry.schema,
14969 meta_dek,
14970 )?;
14971 }
14972 for (table_id, (policy, ttl_epoch)) in ttl_updates {
14973 let Some(entry) = cat.tables.iter().find(|entry| {
14974 entry.table_id == table_id
14975 && matches!(entry.state, TableState::Live | TableState::Building { .. })
14976 }) else {
14977 continue;
14978 };
14979 let table_root = if let Some(root) = table_roots.and_then(|roots| roots.get(&table_id))
14980 {
14981 root.try_clone()?
14982 } else if let Some(root) = durable_root {
14983 root.open_directory(Path::new(TABLES_DIR).join(table_id.to_string()))?
14984 } else {
14985 crate::durable_file::DurableRoot::open(
14986 root.join(TABLES_DIR).join(table_id.to_string()),
14987 )?
14988 };
14989 let table_dir = table_root.io_path()?;
14990 let mut manifest = crate::manifest::read_durable(&table_root, "", meta_dek)?;
14991 if manifest.ttl != policy || manifest.current_epoch < ttl_epoch {
14992 manifest.ttl = policy;
14993 manifest.current_epoch = manifest.current_epoch.max(ttl_epoch);
14994 manifest.schema_id = entry.schema.schema_id;
14995 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
14996 }
14997 }
14998 if changed {
14999 match durable_root {
15000 Some(root) => catalog::write_durable(root, cat, meta_dek)?,
15001 None => catalog::write_atomic(root, cat, meta_dek)?,
15002 }
15003 }
15004 }
15005 *target_catalog = recovered_catalog;
15006 Ok(())
15007}
15008
15009fn ensure_recovered_table_storage(
15010 pinned_table: Option<&crate::durable_file::DurableRoot>,
15011 durable_root: Option<&crate::durable_file::DurableRoot>,
15012 fallback_table_dir: &Path,
15013 table_id: u64,
15014 schema: &Schema,
15015 meta_dek: Option<&[u8; META_DEK_LEN]>,
15016) -> Result<()> {
15017 let table_root = if let Some(root) = pinned_table {
15018 root.try_clone()?
15019 } else if let Some(root) = durable_root {
15020 let relative = Path::new(TABLES_DIR).join(table_id.to_string());
15021 match root.open_directory(&relative) {
15022 Ok(table) => table,
15023 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
15024 root.create_directory_all_pinned(relative)?
15025 }
15026 Err(error) => return Err(error.into()),
15027 }
15028 } else {
15029 crate::durable_file::create_directory_all(fallback_table_dir)?;
15030 crate::durable_file::DurableRoot::open(fallback_table_dir)?
15031 };
15032 let table_dir = table_root.io_path()?;
15033 let mut existing_manifest = match crate::manifest::read_durable(&table_root, "", meta_dek) {
15034 Ok(manifest) => {
15035 if manifest.table_id != table_id {
15036 return Err(MongrelError::Conflict(format!(
15037 "recovered table directory id mismatch: expected {table_id}, found {}",
15038 manifest.table_id
15039 )));
15040 }
15041 Some(manifest)
15042 }
15043 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
15044 Err(error) => return Err(error),
15045 };
15046
15047 table_root.create_directory_all(crate::engine::WAL_DIR)?;
15048 table_root.create_directory_all(crate::engine::RUNS_DIR)?;
15049 crate::engine::write_schema(&table_dir, schema)?;
15050
15051 if let Some(mut manifest) = existing_manifest.take() {
15052 if manifest.schema_id != schema.schema_id {
15053 manifest.schema_id = schema.schema_id;
15054 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
15055 }
15056 } else {
15057 let mut manifest = crate::manifest::Manifest::new(table_id, schema.schema_id);
15059 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
15060 }
15061 Ok(())
15062}
15063
15064fn validate_recovered_schema(schema: &Schema) -> Result<()> {
15065 schema.validate_auto_increment()?;
15066 schema.validate_defaults()?;
15067 schema.validate_ai()?;
15068 let mut column_ids = HashSet::new();
15069 let mut column_names = HashSet::new();
15070 for column in &schema.columns {
15071 if !column_ids.insert(column.id) || !column_names.insert(column.name.as_str()) {
15072 return Err(MongrelError::Schema(
15073 "recovered schema contains duplicate columns".into(),
15074 ));
15075 }
15076 match &column.ty {
15077 TypeId::Decimal128 { precision, scale }
15078 if *precision == 0 || *precision > 38 || scale.unsigned_abs() > *precision =>
15079 {
15080 return Err(MongrelError::Schema(format!(
15081 "column {:?} has invalid decimal precision or scale",
15082 column.name
15083 )));
15084 }
15085 TypeId::Enum { variants }
15086 if variants.is_empty()
15087 || variants.iter().any(String::is_empty)
15088 || variants.iter().collect::<HashSet<_>>().len() != variants.len() =>
15089 {
15090 return Err(MongrelError::Schema(format!(
15091 "column {:?} has invalid enum variants",
15092 column.name
15093 )));
15094 }
15095 _ => {}
15096 }
15097 }
15098 let mut index_names = HashSet::new();
15099 for index in &schema.indexes {
15100 index.validate_options()?;
15101 if index.name.is_empty()
15102 || !index_names.insert(index.name.as_str())
15103 || schema
15104 .columns
15105 .iter()
15106 .all(|column| column.id != index.column_id)
15107 {
15108 return Err(MongrelError::Schema(format!(
15109 "recovered index {:?} references missing column {}",
15110 index.name, index.column_id
15111 )));
15112 }
15113 }
15114 let mut colocated = HashSet::new();
15115 for group in &schema.colocation {
15116 if group.is_empty()
15117 || group.iter().any(|id| !column_ids.contains(id))
15118 || group.iter().any(|id| !colocated.insert(*id))
15119 {
15120 return Err(MongrelError::Schema(
15121 "recovered schema contains invalid column co-location groups".into(),
15122 ));
15123 }
15124 }
15125
15126 let mut constraint_ids = HashSet::new();
15127 let mut constraint_names = HashSet::<String>::new();
15128 let mut validate_constraint_identity = |id: u16, name: &str| -> Result<()> {
15129 if name.is_empty()
15130 || !constraint_ids.insert(id)
15131 || !constraint_names.insert(name.to_owned())
15132 {
15133 return Err(MongrelError::Schema(
15134 "recovered schema contains duplicate or empty constraint identities".into(),
15135 ));
15136 }
15137 Ok(())
15138 };
15139 for unique in &schema.constraints.uniques {
15140 validate_constraint_identity(unique.id, &unique.name)?;
15141 if unique.columns.is_empty()
15142 || unique.columns.iter().any(|id| !column_ids.contains(id))
15143 || unique.columns.iter().collect::<HashSet<_>>().len() != unique.columns.len()
15144 {
15145 return Err(MongrelError::Schema(format!(
15146 "unique constraint {:?} has invalid columns",
15147 unique.name
15148 )));
15149 }
15150 }
15151 for foreign_key in &schema.constraints.foreign_keys {
15152 validate_constraint_identity(foreign_key.id, &foreign_key.name)?;
15153 if foreign_key.ref_table.is_empty()
15154 || foreign_key.columns.is_empty()
15155 || foreign_key.columns.len() != foreign_key.ref_columns.len()
15156 || foreign_key
15157 .columns
15158 .iter()
15159 .any(|id| !column_ids.contains(id))
15160 || foreign_key.columns.iter().collect::<HashSet<_>>().len() != foreign_key.columns.len()
15161 || foreign_key.ref_columns.iter().collect::<HashSet<_>>().len()
15162 != foreign_key.ref_columns.len()
15163 {
15164 return Err(MongrelError::Schema(format!(
15165 "foreign key {:?} has invalid columns",
15166 foreign_key.name
15167 )));
15168 }
15169 if (matches!(foreign_key.on_delete, crate::constraint::FkAction::SetNull)
15170 || matches!(foreign_key.on_update, crate::constraint::FkAction::SetNull))
15171 && foreign_key.columns.iter().any(|id| {
15172 schema
15173 .columns
15174 .iter()
15175 .find(|column| column.id == *id)
15176 .is_none_or(|column| {
15177 !column.flags.contains(crate::schema::ColumnFlags::NULLABLE)
15178 })
15179 })
15180 {
15181 return Err(MongrelError::Schema(format!(
15182 "foreign key {:?} uses SET NULL on a non-nullable column",
15183 foreign_key.name
15184 )));
15185 }
15186 }
15187 for check in &schema.constraints.checks {
15188 validate_constraint_identity(check.id, &check.name)?;
15189 check.expr.validate()?;
15190 validate_check_columns(&check.expr, &column_ids)?;
15191 }
15192 Ok(())
15193}
15194
15195fn validate_check_columns(
15196 expression: &crate::constraint::CheckExpr,
15197 column_ids: &HashSet<u16>,
15198) -> Result<()> {
15199 use crate::constraint::CheckExpr;
15200 match expression {
15201 CheckExpr::Col(id) | CheckExpr::IsNull(id) | CheckExpr::IsNotNull(id) => {
15202 if column_ids.contains(id) {
15203 Ok(())
15204 } else {
15205 Err(MongrelError::Schema(format!(
15206 "check constraint references unknown column {id}"
15207 )))
15208 }
15209 }
15210 CheckExpr::Regex { col, .. } => {
15211 if column_ids.contains(col) {
15212 Ok(())
15213 } else {
15214 Err(MongrelError::Schema(format!(
15215 "check constraint references unknown column {col}"
15216 )))
15217 }
15218 }
15219 CheckExpr::Add(left, right)
15220 | CheckExpr::Sub(left, right)
15221 | CheckExpr::Mul(left, right)
15222 | CheckExpr::Div(left, right)
15223 | CheckExpr::Mod(left, right)
15224 | CheckExpr::Eq(left, right)
15225 | CheckExpr::Ne(left, right)
15226 | CheckExpr::Lt(left, right)
15227 | CheckExpr::Le(left, right)
15228 | CheckExpr::Gt(left, right)
15229 | CheckExpr::Ge(left, right)
15230 | CheckExpr::And(left, right)
15231 | CheckExpr::Or(left, right) => {
15232 validate_check_columns(left, column_ids)?;
15233 validate_check_columns(right, column_ids)
15234 }
15235 CheckExpr::Not(inner) => validate_check_columns(inner, column_ids),
15236 CheckExpr::True | CheckExpr::Lit(_) => Ok(()),
15237 }
15238}
15239
15240fn validate_catalog_transition(current: &Catalog, next: &Catalog) -> Result<()> {
15241 for (name, prior, candidate) in [
15242 ("db_epoch", current.db_epoch, next.db_epoch),
15243 ("next_table_id", current.next_table_id, next.next_table_id),
15244 (
15245 "next_segment_no",
15246 current.next_segment_no,
15247 next.next_segment_no,
15248 ),
15249 ("next_user_id", current.next_user_id, next.next_user_id),
15250 (
15251 "security_version",
15252 current.security_version,
15253 next.security_version,
15254 ),
15255 ] {
15256 if candidate < prior {
15257 return Err(MongrelError::Schema(format!(
15258 "catalog snapshot rolls back {name} from {prior} to {candidate}"
15259 )));
15260 }
15261 }
15262 for prior in ¤t.tables {
15263 let Some(candidate) = next
15264 .tables
15265 .iter()
15266 .find(|entry| entry.table_id == prior.table_id)
15267 else {
15268 return Err(MongrelError::Schema(format!(
15269 "catalog snapshot removes table identity {}",
15270 prior.table_id
15271 )));
15272 };
15273 if candidate.created_epoch != prior.created_epoch
15274 || candidate.schema.schema_id < prior.schema.schema_id
15275 || matches!(prior.state, TableState::Dropped { .. })
15276 && !matches!(candidate.state, TableState::Dropped { .. })
15277 {
15278 return Err(MongrelError::Schema(format!(
15279 "catalog snapshot rolls back table identity {}",
15280 prior.table_id
15281 )));
15282 }
15283 }
15284 for prior in ¤t.users {
15285 if let Some(candidate) = next.users.iter().find(|user| user.id == prior.id) {
15286 if candidate.username != prior.username
15287 || candidate.created_epoch != prior.created_epoch
15288 {
15289 return Err(MongrelError::Schema(format!(
15290 "catalog snapshot reuses user identity {}",
15291 prior.id
15292 )));
15293 }
15294 }
15295 }
15296 Ok(())
15297}
15298
15299fn validate_recovered_catalog(catalog: &Catalog) -> Result<()> {
15300 let mut table_ids = HashSet::new();
15301 let mut active_names = HashSet::new();
15302 let mut max_table_id = None::<u64>;
15303 for entry in &catalog.tables {
15304 if !table_ids.insert(entry.table_id) {
15305 return Err(MongrelError::Schema(format!(
15306 "catalog contains duplicate table id {}",
15307 entry.table_id
15308 )));
15309 }
15310 max_table_id = Some(max_table_id.map_or(entry.table_id, |value| value.max(entry.table_id)));
15311 if entry.name.is_empty() || entry.created_epoch > catalog.db_epoch {
15312 return Err(MongrelError::Schema(format!(
15313 "catalog table {} has invalid name or creation epoch",
15314 entry.table_id
15315 )));
15316 }
15317 validate_recovered_schema(&entry.schema)?;
15318 match &entry.state {
15319 TableState::Live => {
15320 if !active_names.insert(entry.name.as_str()) {
15321 return Err(MongrelError::Schema(format!(
15322 "catalog contains duplicate active table name {:?}",
15323 entry.name
15324 )));
15325 }
15326 }
15327 TableState::Dropped { at_epoch } => {
15328 if *at_epoch < entry.created_epoch || *at_epoch > catalog.db_epoch {
15329 return Err(MongrelError::Schema(format!(
15330 "catalog table {} has invalid drop epoch {at_epoch}",
15331 entry.table_id
15332 )));
15333 }
15334 }
15335 TableState::Building {
15336 intended_name,
15337 query_id,
15338 replaces_table_id,
15339 ..
15340 } => {
15341 if intended_name.is_empty() || query_id.is_empty() {
15342 return Err(MongrelError::Schema(format!(
15343 "building table {} has empty identity fields",
15344 entry.table_id
15345 )));
15346 }
15347 if !active_names.insert(entry.name.as_str()) {
15348 return Err(MongrelError::Schema(format!(
15349 "catalog contains duplicate active/building table name {:?}",
15350 entry.name
15351 )));
15352 }
15353 if replaces_table_id.is_some_and(|id| id == entry.table_id) {
15354 return Err(MongrelError::Schema(
15355 "building table cannot replace itself".into(),
15356 ));
15357 }
15358 }
15359 }
15360 }
15361 if let Some(maximum) = max_table_id {
15362 let required = maximum
15363 .checked_add(1)
15364 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
15365 if catalog.next_table_id < required {
15366 return Err(MongrelError::Schema(format!(
15367 "catalog next_table_id {} precedes required {required}",
15368 catalog.next_table_id
15369 )));
15370 }
15371 }
15372 for entry in &catalog.tables {
15373 if let TableState::Building {
15374 replaces_table_id: Some(replaced),
15375 ..
15376 } = entry.state
15377 {
15378 if !table_ids.contains(&replaced) {
15379 return Err(MongrelError::Schema(format!(
15380 "building table {} replaces unknown table {replaced}",
15381 entry.table_id
15382 )));
15383 }
15384 }
15385 }
15386 for entry in &catalog.tables {
15387 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
15388 validate_foreign_key_targets(catalog, &entry.schema)?;
15389 }
15390 }
15391
15392 let mut external_names = HashSet::new();
15393 for entry in &catalog.external_tables {
15394 entry.validate()?;
15395 validate_recovered_schema(&entry.declared_schema)?;
15396 if !entry.declared_schema.constraints.is_empty() {
15397 return Err(MongrelError::Schema(format!(
15398 "external table {:?} cannot carry engine-enforced constraints",
15399 entry.name
15400 )));
15401 }
15402 if entry.created_epoch > catalog.db_epoch
15403 || !external_names.insert(entry.name.as_str())
15404 || active_names.contains(entry.name.as_str())
15405 {
15406 return Err(MongrelError::Schema(format!(
15407 "invalid or duplicate external table {:?}",
15408 entry.name
15409 )));
15410 }
15411 }
15412
15413 let mut procedure_names = HashSet::new();
15414 for entry in &catalog.procedures {
15415 entry.procedure.validate()?;
15416 if entry.procedure.created_epoch > entry.procedure.updated_epoch
15417 || entry.procedure.updated_epoch > catalog.db_epoch
15418 || !procedure_names.insert(entry.procedure.name.as_str())
15419 {
15420 return Err(MongrelError::Schema(format!(
15421 "invalid or duplicate procedure {:?}",
15422 entry.procedure.name
15423 )));
15424 }
15425 validate_recovered_procedure_references(catalog, &entry.procedure)?;
15426 }
15427
15428 let mut trigger_names = HashSet::new();
15429 for entry in &catalog.triggers {
15430 entry.trigger.validate()?;
15431 if entry.trigger.created_epoch > entry.trigger.updated_epoch
15432 || entry.trigger.updated_epoch > catalog.db_epoch
15433 || !trigger_names.insert(entry.trigger.name.as_str())
15434 {
15435 return Err(MongrelError::Schema(format!(
15436 "invalid or duplicate trigger {:?}",
15437 entry.trigger.name
15438 )));
15439 }
15440 validate_recovered_trigger_references(catalog, &entry.trigger)?;
15441 }
15442
15443 let mut views = HashSet::new();
15444 for view in &catalog.materialized_views {
15445 let target = catalog.live(&view.name).ok_or_else(|| {
15446 MongrelError::Schema(format!(
15447 "materialized view {:?} has no live table",
15448 view.name
15449 ))
15450 })?;
15451 if view.name.is_empty()
15452 || view.query.trim().is_empty()
15453 || view.last_refresh_epoch > catalog.db_epoch
15454 || !views.insert(view.name.as_str())
15455 {
15456 return Err(MongrelError::Schema(format!(
15457 "materialized view {:?} has no unique live table",
15458 view.name
15459 )));
15460 }
15461 if let Some(incremental) = &view.incremental {
15462 let source = catalog.live(&incremental.source_table).ok_or_else(|| {
15463 MongrelError::Schema(format!(
15464 "materialized view {:?} references missing source {:?}",
15465 view.name, incremental.source_table
15466 ))
15467 })?;
15468 if source.table_id != incremental.source_table_id
15469 || source
15470 .schema
15471 .columns
15472 .iter()
15473 .all(|column| column.id != incremental.group_column)
15474 {
15475 return Err(MongrelError::Schema(format!(
15476 "materialized view {:?} has invalid incremental source",
15477 view.name
15478 )));
15479 }
15480 let target_ids = target
15481 .schema
15482 .columns
15483 .iter()
15484 .map(|column| column.id)
15485 .collect::<HashSet<_>>();
15486 let mut output_ids = HashSet::new();
15487 let count_outputs = incremental
15488 .outputs
15489 .iter()
15490 .filter(|output| {
15491 matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
15492 })
15493 .count();
15494 if incremental.checkpoint_event_id.is_empty()
15495 || !target_ids.contains(&incremental.group_output_column)
15496 || !target_ids.contains(&incremental.count_output_column)
15497 || incremental.outputs.is_empty()
15498 || count_outputs != 1
15499 || incremental.outputs.iter().any(|output| {
15500 !target_ids.contains(&output.output_column)
15501 || output.output_column == incremental.group_output_column
15502 || !output_ids.insert(output.output_column)
15503 || matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
15504 && output.output_column != incremental.count_output_column
15505 || match output.kind {
15506 crate::catalog::IncrementalAggregateKind::Sum { source_column } => {
15507 source
15508 .schema
15509 .columns
15510 .iter()
15511 .all(|column| column.id != source_column)
15512 }
15513 crate::catalog::IncrementalAggregateKind::Count => false,
15514 }
15515 })
15516 {
15517 return Err(MongrelError::Schema(format!(
15518 "materialized view {:?} has invalid incremental outputs",
15519 view.name
15520 )));
15521 }
15522 }
15523 }
15524
15525 validate_security_catalog(catalog, &catalog.security)?;
15526 validate_recovered_auth_catalog(catalog)?;
15527 Ok(())
15528}
15529
15530fn repair_catalog_allocator_counters(catalog: &mut Catalog) -> Result<bool> {
15531 let mut changed = false;
15532 if let Some(maximum) = catalog.tables.iter().map(|entry| entry.table_id).max() {
15533 let required = maximum
15534 .checked_add(1)
15535 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
15536 if catalog.next_table_id < required {
15537 catalog.next_table_id = required;
15538 changed = true;
15539 }
15540 }
15541 if let Some(maximum) = catalog.users.iter().map(|user| user.id).max() {
15542 let required = maximum
15543 .checked_add(1)
15544 .ok_or_else(|| MongrelError::Full("user id namespace exhausted".into()))?;
15545 if catalog.next_user_id < required {
15546 catalog.next_user_id = required;
15547 changed = true;
15548 }
15549 }
15550 Ok(changed)
15551}
15552
15553fn validate_foreign_key_targets(catalog: &Catalog, schema: &Schema) -> Result<()> {
15554 for foreign_key in &schema.constraints.foreign_keys {
15555 let parent = catalog.live(&foreign_key.ref_table).ok_or_else(|| {
15556 MongrelError::Schema(format!(
15557 "foreign key {:?} references unknown live table {:?}",
15558 foreign_key.name, foreign_key.ref_table
15559 ))
15560 })?;
15561 let referenced_unique = parent
15562 .schema
15563 .constraints
15564 .uniques
15565 .iter()
15566 .any(|unique| unique.columns == foreign_key.ref_columns)
15567 || foreign_key.ref_columns.len() == 1
15568 && parent
15569 .schema
15570 .primary_key()
15571 .is_some_and(|column| column.id == foreign_key.ref_columns[0]);
15572 if !referenced_unique {
15573 return Err(MongrelError::Schema(format!(
15574 "foreign key {:?} does not reference a unique key",
15575 foreign_key.name
15576 )));
15577 }
15578 for (local_id, parent_id) in foreign_key.columns.iter().zip(&foreign_key.ref_columns) {
15579 let local = schema.columns.iter().find(|column| column.id == *local_id);
15580 let referenced = parent
15581 .schema
15582 .columns
15583 .iter()
15584 .find(|column| column.id == *parent_id);
15585 if local
15586 .zip(referenced)
15587 .is_none_or(|(local, referenced)| local.ty != referenced.ty)
15588 {
15589 return Err(MongrelError::Schema(format!(
15590 "foreign key {:?} has missing or incompatible columns",
15591 foreign_key.name
15592 )));
15593 }
15594 }
15595 }
15596 Ok(())
15597}
15598
15599fn validate_recovered_procedure_references(
15600 catalog: &Catalog,
15601 procedure: &StoredProcedure,
15602) -> Result<()> {
15603 for step in &procedure.body.steps {
15604 let Some(table_name) = step.table() else {
15605 continue;
15606 };
15607 let schema = &catalog
15608 .live(table_name)
15609 .ok_or_else(|| {
15610 MongrelError::Schema(format!(
15611 "procedure {:?} references unknown table {table_name:?}",
15612 procedure.name
15613 ))
15614 })?
15615 .schema;
15616 match step {
15617 ProcedureStep::NativeQuery {
15618 conditions,
15619 projection,
15620 ..
15621 } => {
15622 for condition in conditions {
15623 validate_condition_columns(condition, schema)?;
15624 }
15625 for id in projection.iter().flatten() {
15626 validate_column_id(*id, schema)?;
15627 }
15628 }
15629 ProcedureStep::Put { cells, .. } => {
15630 for cell in cells {
15631 validate_column_id(cell.column_id, schema)?;
15632 }
15633 }
15634 ProcedureStep::Upsert {
15635 cells,
15636 update_cells,
15637 ..
15638 } => {
15639 for cell in cells.iter().chain(update_cells.iter().flatten()) {
15640 validate_column_id(cell.column_id, schema)?;
15641 }
15642 }
15643 ProcedureStep::DeleteByPk { .. } if schema.primary_key().is_none() => {
15644 return Err(MongrelError::Schema(format!(
15645 "procedure {:?} deletes by primary key on table without one",
15646 procedure.name
15647 )));
15648 }
15649 ProcedureStep::DeleteByPk { .. }
15650 | ProcedureStep::DeleteRows { .. }
15651 | ProcedureStep::SqlQuery { .. } => {}
15652 }
15653 }
15654 Ok(())
15655}
15656
15657fn validate_recovered_trigger_references(catalog: &Catalog, trigger: &StoredTrigger) -> Result<()> {
15658 let target_schema = match &trigger.target {
15659 TriggerTarget::Table(name) => catalog
15660 .live(name)
15661 .ok_or_else(|| {
15662 MongrelError::Schema(format!(
15663 "trigger {:?} references unknown table {name:?}",
15664 trigger.name
15665 ))
15666 })?
15667 .schema
15668 .clone(),
15669 TriggerTarget::View(_) => Schema {
15670 columns: trigger.target_columns.clone(),
15671 ..Schema::default()
15672 },
15673 };
15674 for column in &trigger.update_of {
15675 if target_schema.column(column).is_none() {
15676 return Err(MongrelError::Schema(format!(
15677 "trigger {:?} references unknown UPDATE OF column {column:?}",
15678 trigger.name
15679 )));
15680 }
15681 }
15682 if let Some(expr) = &trigger.when {
15683 validate_trigger_expr(expr, &target_schema, trigger.event)?;
15684 }
15685 let mut selects = HashMap::new();
15686 for step in &trigger.program.steps {
15687 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before {
15688 return Err(MongrelError::Schema(
15689 "SetNew is only valid in BEFORE triggers".into(),
15690 ));
15691 }
15692 validate_trigger_step(step, catalog, &target_schema, trigger.event, &mut selects)?;
15693 }
15694 Ok(())
15695}
15696
15697fn validate_recovered_auth_catalog(catalog: &Catalog) -> Result<()> {
15698 let mut role_names = HashSet::new();
15699 for role in &catalog.roles {
15700 if role.name.is_empty()
15701 || role.created_epoch > catalog.db_epoch
15702 || !role_names.insert(role.name.as_str())
15703 {
15704 return Err(MongrelError::Schema(format!(
15705 "invalid or duplicate role {:?}",
15706 role.name
15707 )));
15708 }
15709 for permission in &role.permissions {
15710 if let Some(table) = permission_table(permission) {
15711 let schema = catalog
15712 .live(table)
15713 .map(|entry| &entry.schema)
15714 .or_else(|| {
15715 catalog
15716 .external_tables
15717 .iter()
15718 .find(|entry| entry.name == table)
15719 .map(|entry| &entry.declared_schema)
15720 })
15721 .ok_or_else(|| {
15722 MongrelError::Schema(format!(
15723 "role {:?} references unknown table {table:?}",
15724 role.name
15725 ))
15726 })?;
15727 let columns = match permission {
15728 crate::auth::Permission::SelectColumns { columns, .. }
15729 | crate::auth::Permission::InsertColumns { columns, .. }
15730 | crate::auth::Permission::UpdateColumns { columns, .. } => Some(columns),
15731 _ => None,
15732 };
15733 if columns.is_some_and(|columns| {
15734 columns.is_empty()
15735 || columns.iter().any(|column| schema.column(column).is_none())
15736 }) {
15737 return Err(MongrelError::Schema(format!(
15738 "role {:?} contains invalid column permissions",
15739 role.name
15740 )));
15741 }
15742 }
15743 }
15744 }
15745 let mut user_ids = HashSet::new();
15746 let mut usernames = HashSet::new();
15747 let mut maximum_user_id = 0;
15748 for user in &catalog.users {
15749 maximum_user_id = maximum_user_id.max(user.id);
15750 if user.id == 0
15751 || user.username.is_empty()
15752 || user.password_hash.is_empty()
15753 || user.created_epoch > catalog.db_epoch
15754 || !user_ids.insert(user.id)
15755 || !usernames.insert(user.username.as_str())
15756 || user
15757 .roles
15758 .iter()
15759 .any(|role| !role_names.contains(role.as_str()))
15760 {
15761 return Err(MongrelError::Schema(format!(
15762 "invalid or duplicate user {:?}",
15763 user.username
15764 )));
15765 }
15766 }
15767 if !catalog.users.is_empty() && catalog.next_user_id <= maximum_user_id {
15768 return Err(MongrelError::Schema(
15769 "catalog next_user_id does not advance beyond existing user ids".into(),
15770 ));
15771 }
15772 if catalog.require_auth && !catalog.users.iter().any(|user| user.is_admin) {
15773 return Err(MongrelError::Schema(
15774 "authenticated catalog has no administrator".into(),
15775 ));
15776 }
15777 Ok(())
15778}
15779
15780fn validate_recovered_storage_plan(
15781 root: &Path,
15782 durable_root: Option<&crate::durable_file::DurableRoot>,
15783 catalog: &Catalog,
15784 created_table_ids: &HashSet<u64>,
15785 ttl_updates: &HashMap<u64, (Option<crate::manifest::TtlPolicy>, u64)>,
15786 meta_dek: Option<&[u8; META_DEK_LEN]>,
15787) -> Result<Vec<u64>> {
15788 const MAX_SCHEMA_BYTES: u64 = 16 * 1024 * 1024;
15789 let mut reconcile = Vec::new();
15790 for entry in &catalog.tables {
15791 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
15792 continue;
15793 }
15794 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
15795 let table_dir = root.join(TABLES_DIR).join(entry.table_id.to_string());
15796 let table_exists = match durable_root {
15797 Some(root) => match root.open_directory(&relative_dir) {
15798 Ok(_) => true,
15799 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
15800 Err(error) => return Err(error.into()),
15801 },
15802 None => table_dir.is_dir(),
15803 };
15804 if !table_exists {
15805 if created_table_ids.contains(&entry.table_id) {
15806 reconcile.push(entry.table_id);
15807 continue;
15808 }
15809 return Err(MongrelError::NotFound(format!(
15810 "catalog table {} storage is missing",
15811 entry.table_id
15812 )));
15813 }
15814 let manifest_result = match durable_root {
15815 Some(root) => crate::manifest::read_durable(root, &relative_dir, meta_dek),
15816 None => crate::manifest::read(&table_dir, meta_dek),
15817 };
15818 let manifest = match manifest_result {
15819 Ok(manifest) => manifest,
15820 Err(MongrelError::Io(error))
15821 if created_table_ids.contains(&entry.table_id)
15822 && error.kind() == std::io::ErrorKind::NotFound =>
15823 {
15824 reconcile.push(entry.table_id);
15825 continue;
15826 }
15827 Err(error) => return Err(error),
15828 };
15829 if manifest.table_id != entry.table_id {
15830 return Err(MongrelError::Conflict(format!(
15831 "catalog table {} storage identity mismatch",
15832 entry.table_id
15833 )));
15834 }
15835 let schema_result = match durable_root {
15836 Some(root) => root
15837 .open_regular(relative_dir.join(crate::engine::SCHEMA_FILENAME))
15838 .map_err(MongrelError::from),
15839 None => crate::durable_file::open_regular_nofollow(
15840 &table_dir.join(crate::engine::SCHEMA_FILENAME),
15841 ),
15842 };
15843 let file = match schema_result {
15844 Ok(file) => file,
15845 Err(MongrelError::Io(error))
15846 if created_table_ids.contains(&entry.table_id)
15847 && error.kind() == std::io::ErrorKind::NotFound =>
15848 {
15849 reconcile.push(entry.table_id);
15850 continue;
15851 }
15852 Err(error) => return Err(error),
15853 };
15854 let length = file.metadata()?.len();
15855 if length > MAX_SCHEMA_BYTES {
15856 return Err(MongrelError::ResourceLimitExceeded {
15857 resource: "recovered schema bytes",
15858 requested: usize::try_from(length).unwrap_or(usize::MAX),
15859 limit: MAX_SCHEMA_BYTES as usize,
15860 });
15861 }
15862 let disk_schema: Schema = serde_json::from_reader(file.take(MAX_SCHEMA_BYTES + 1))
15863 .map_err(|error| MongrelError::Schema(format!("decode recovered schema: {error}")))?;
15864 if manifest.schema_id != entry.schema.schema_id
15865 || crate::wal::DdlOp::encode_schema(&disk_schema)?
15866 != crate::wal::DdlOp::encode_schema(&entry.schema)?
15867 {
15868 reconcile.push(entry.table_id);
15869 }
15870 }
15871 for table_id in ttl_updates.keys() {
15872 if !catalog.tables.iter().any(|entry| {
15873 entry.table_id == *table_id
15874 && matches!(entry.state, TableState::Live | TableState::Building { .. })
15875 }) {
15876 continue;
15877 }
15878 let relative_dir = Path::new(TABLES_DIR).join(table_id.to_string());
15879 let table_exists = match durable_root {
15880 Some(root) => match root.open_directory(&relative_dir) {
15881 Ok(_) => true,
15882 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
15883 Err(error) => return Err(error.into()),
15884 },
15885 None => root.join(&relative_dir).is_dir(),
15886 };
15887 if !table_exists && !created_table_ids.contains(table_id) {
15888 return Err(MongrelError::NotFound(format!(
15889 "TTL recovery table {table_id} storage is missing"
15890 )));
15891 }
15892 }
15893 reconcile.sort_unstable();
15894 reconcile.dedup();
15895 Ok(reconcile)
15896}
15897
15898fn validate_catalog_table_storage(
15899 root: &crate::durable_file::DurableRoot,
15900 catalog: &Catalog,
15901 meta_dek: Option<&[u8; META_DEK_LEN]>,
15902) -> Result<()> {
15903 for entry in &catalog.tables {
15904 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
15905 continue;
15906 }
15907 let table_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
15908 let manifest = crate::manifest::read_durable(root, &table_dir, meta_dek)?;
15909 if manifest.table_id != entry.table_id || manifest.schema_id != entry.schema.schema_id {
15910 return Err(MongrelError::Conflict(format!(
15911 "catalog table {} storage identity mismatch",
15912 entry.table_id
15913 )));
15914 }
15915 root.open_regular(table_dir.join(crate::engine::SCHEMA_FILENAME))?;
15916 }
15917 Ok(())
15918}
15919
15920fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> Result<bool> {
15921 match schema.columns.iter_mut().find(|c| c.id == column.id) {
15922 Some(existing) if *existing == column => Ok(false),
15923 Some(existing) => {
15924 *existing = column;
15925 schema.schema_id = schema
15926 .schema_id
15927 .checked_add(1)
15928 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
15929 Ok(true)
15930 }
15931 None => {
15932 schema.columns.push(column);
15933 schema.schema_id = schema
15934 .schema_id
15935 .checked_add(1)
15936 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
15937 Ok(true)
15938 }
15939 }
15940}
15941
15942fn permission_table(permission: &crate::auth::Permission) -> Option<&str> {
15943 use crate::auth::Permission;
15944 match permission {
15945 Permission::Select { table }
15946 | Permission::Insert { table }
15947 | Permission::Update { table }
15948 | Permission::Delete { table }
15949 | Permission::SelectColumns { table, .. }
15950 | Permission::InsertColumns { table, .. }
15951 | Permission::UpdateColumns { table, .. } => Some(table),
15952 Permission::All | Permission::Ddl | Permission::Admin => None,
15953 }
15954}
15955
15956fn apply_rebuilding_publish(
15957 catalog: &mut Catalog,
15958 table_id: u64,
15959 replaced_table_id: u64,
15960 new_name: &str,
15961 epoch: u64,
15962) -> Result<bool> {
15963 let already_published = catalog.tables.iter().any(|entry| {
15964 entry.table_id == table_id
15965 && entry.name == new_name
15966 && matches!(entry.state, TableState::Live)
15967 }) && catalog.tables.iter().any(|entry| {
15968 entry.table_id == replaced_table_id && matches!(entry.state, TableState::Dropped { .. })
15969 });
15970 if already_published {
15971 return Ok(false);
15972 }
15973 let schema = catalog
15974 .tables
15975 .iter()
15976 .find(|entry| entry.table_id == table_id)
15977 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?
15978 .schema
15979 .clone();
15980 let replaced = catalog
15981 .tables
15982 .iter_mut()
15983 .find(|entry| entry.table_id == replaced_table_id)
15984 .ok_or_else(|| MongrelError::NotFound(format!("table id {replaced_table_id} not found")))?;
15985 replaced.state = TableState::Dropped { at_epoch: epoch };
15986 let replacement = catalog
15987 .tables
15988 .iter_mut()
15989 .find(|entry| entry.table_id == table_id)
15990 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?;
15991 replacement.name = new_name.to_string();
15992 replacement.state = TableState::Live;
15993
15994 for role in &mut catalog.roles {
15995 role.permissions.retain_mut(|permission| {
15996 retain_rebuilt_permission_columns(permission, new_name, &schema)
15997 });
15998 }
15999 for definition in &mut catalog.materialized_views {
16000 if let Some(incremental) = definition.incremental.as_mut() {
16001 if incremental.source_table == new_name
16002 && incremental.source_table_id == replaced_table_id
16003 {
16004 incremental.source_table_id = table_id;
16005 }
16006 }
16007 }
16008 advance_security_version(catalog)?;
16009 Ok(true)
16010}
16011
16012fn retain_rebuilt_permission_columns(
16013 permission: &mut crate::auth::Permission,
16014 target_table: &str,
16015 schema: &Schema,
16016) -> bool {
16017 use crate::auth::Permission;
16018 let columns = match permission {
16019 Permission::SelectColumns { table, columns }
16020 | Permission::InsertColumns { table, columns }
16021 | Permission::UpdateColumns { table, columns }
16022 if table == target_table =>
16023 {
16024 Some(columns)
16025 }
16026 _ => None,
16027 };
16028 if let Some(columns) = columns {
16029 columns.retain(|column| schema.column(column).is_some());
16030 !columns.is_empty()
16031 } else {
16032 true
16033 }
16034}
16035
16036fn rename_permission_table(permission: &mut crate::auth::Permission, old: &str, new: &str) {
16037 use crate::auth::Permission;
16038 let table = match permission {
16039 Permission::Select { table }
16040 | Permission::Insert { table }
16041 | Permission::Update { table }
16042 | Permission::Delete { table }
16043 | Permission::SelectColumns { table, .. }
16044 | Permission::InsertColumns { table, .. }
16045 | Permission::UpdateColumns { table, .. } => Some(table),
16046 Permission::All | Permission::Ddl | Permission::Admin => None,
16047 };
16048 if let Some(table) = table.filter(|table| table.as_str() == old) {
16049 *table = new.to_string();
16050 }
16051}
16052
16053fn rename_permission_column(
16054 permission: &mut crate::auth::Permission,
16055 target_table: &str,
16056 old: &str,
16057 new: &str,
16058) {
16059 use crate::auth::Permission;
16060 let columns = match permission {
16061 Permission::SelectColumns { table, columns }
16062 | Permission::InsertColumns { table, columns }
16063 | Permission::UpdateColumns { table, columns }
16064 if table == target_table =>
16065 {
16066 Some(columns)
16067 }
16068 _ => None,
16069 };
16070 if let Some(column) = columns
16071 .into_iter()
16072 .flatten()
16073 .find(|column| column.as_str() == old)
16074 {
16075 *column = new.to_string();
16076 }
16077}
16078
16079fn merge_permission(
16080 permissions: &mut Vec<crate::auth::Permission>,
16081 permission: crate::auth::Permission,
16082) {
16083 use crate::auth::Permission;
16084 let (kind, table, mut columns) = match permission {
16085 Permission::SelectColumns { table, columns } => (0, table, columns),
16086 Permission::InsertColumns { table, columns } => (1, table, columns),
16087 Permission::UpdateColumns { table, columns } => (2, table, columns),
16088 permission if !permissions.contains(&permission) => {
16089 permissions.push(permission);
16090 return;
16091 }
16092 _ => return,
16093 };
16094 for permission in permissions.iter_mut() {
16095 let existing = match permission {
16096 Permission::SelectColumns {
16097 table: existing_table,
16098 columns,
16099 } if kind == 0 && existing_table == &table => Some(columns),
16100 Permission::InsertColumns {
16101 table: existing_table,
16102 columns,
16103 } if kind == 1 && existing_table == &table => Some(columns),
16104 Permission::UpdateColumns {
16105 table: existing_table,
16106 columns,
16107 } if kind == 2 && existing_table == &table => Some(columns),
16108 _ => None,
16109 };
16110 if let Some(existing) = existing {
16111 existing.append(&mut columns);
16112 existing.sort();
16113 existing.dedup();
16114 return;
16115 }
16116 }
16117 columns.sort();
16118 columns.dedup();
16119 let permission = if kind == 0 {
16120 Permission::SelectColumns { table, columns }
16121 } else if kind == 1 {
16122 Permission::InsertColumns { table, columns }
16123 } else {
16124 Permission::UpdateColumns { table, columns }
16125 };
16126 permissions.push(permission);
16127}
16128
16129fn revoke_permission_from(
16130 permissions: &mut Vec<crate::auth::Permission>,
16131 revoked: &crate::auth::Permission,
16132) {
16133 use crate::auth::Permission;
16134 let revoked_columns = match revoked {
16135 Permission::SelectColumns { table, columns } => Some((0, table, columns)),
16136 Permission::InsertColumns { table, columns } => Some((1, table, columns)),
16137 Permission::UpdateColumns { table, columns } => Some((2, table, columns)),
16138 _ => None,
16139 };
16140 let Some((kind, table, columns)) = revoked_columns else {
16141 permissions.retain(|permission| permission != revoked);
16142 return;
16143 };
16144 for permission in permissions.iter_mut() {
16145 let current = match permission {
16146 Permission::SelectColumns {
16147 table: current_table,
16148 columns,
16149 } if kind == 0 && current_table == table => Some(columns),
16150 Permission::InsertColumns {
16151 table: current_table,
16152 columns,
16153 } if kind == 1 && current_table == table => Some(columns),
16154 Permission::UpdateColumns {
16155 table: current_table,
16156 columns,
16157 } if kind == 2 && current_table == table => Some(columns),
16158 _ => None,
16159 };
16160 if let Some(current) = current {
16161 current.retain(|column| !columns.contains(column));
16162 }
16163 }
16164 permissions.retain(|permission| match permission {
16165 Permission::SelectColumns { columns, .. }
16166 | Permission::InsertColumns { columns, .. }
16167 | Permission::UpdateColumns { columns, .. } => !columns.is_empty(),
16168 _ => true,
16169 });
16170}
16171
16172fn validate_security_catalog(
16173 catalog: &Catalog,
16174 security: &crate::security::SecurityCatalog,
16175) -> Result<()> {
16176 let mut policy_names = HashSet::new();
16177 for table in &security.rls_tables {
16178 if catalog.live(table).is_none() {
16179 return Err(MongrelError::NotFound(format!(
16180 "RLS table {table:?} not found"
16181 )));
16182 }
16183 }
16184 for policy in &security.policies {
16185 if !policy_names.insert((policy.table.clone(), policy.name.clone())) {
16186 return Err(MongrelError::InvalidArgument(format!(
16187 "duplicate policy {:?} on {:?}",
16188 policy.name, policy.table
16189 )));
16190 }
16191 let schema = &catalog
16192 .live(&policy.table)
16193 .ok_or_else(|| {
16194 MongrelError::NotFound(format!("policy table {:?} not found", policy.table))
16195 })?
16196 .schema;
16197 if let Some(expression) = &policy.using {
16198 validate_security_expression(expression, schema)?;
16199 }
16200 if let Some(expression) = &policy.with_check {
16201 validate_security_expression(expression, schema)?;
16202 }
16203 }
16204 let mut mask_names = HashSet::new();
16205 for mask in &security.masks {
16206 if !mask_names.insert((mask.table.clone(), mask.name.clone())) {
16207 return Err(MongrelError::InvalidArgument(format!(
16208 "duplicate mask {:?} on {:?}",
16209 mask.name, mask.table
16210 )));
16211 }
16212 let column = catalog
16213 .live(&mask.table)
16214 .and_then(|entry| {
16215 entry
16216 .schema
16217 .columns
16218 .iter()
16219 .find(|column| column.id == mask.column)
16220 })
16221 .ok_or_else(|| {
16222 MongrelError::NotFound(format!(
16223 "mask column {} on {:?} not found",
16224 mask.column, mask.table
16225 ))
16226 })?;
16227 if matches!(
16228 mask.strategy,
16229 crate::security::MaskStrategy::Redact { .. } | crate::security::MaskStrategy::Sha256
16230 ) && !matches!(column.ty, TypeId::Bytes | TypeId::Enum { .. })
16231 {
16232 return Err(MongrelError::InvalidArgument(format!(
16233 "mask {:?} requires a string/bytes column",
16234 mask.name
16235 )));
16236 }
16237 }
16238 Ok(())
16239}
16240
16241fn validate_security_expression(
16242 expression: &crate::security::SecurityExpr,
16243 schema: &Schema,
16244) -> Result<()> {
16245 use crate::security::SecurityExpr;
16246 match expression {
16247 SecurityExpr::True => Ok(()),
16248 SecurityExpr::ColumnEqCurrentUser { column }
16249 | SecurityExpr::ColumnEqValue { column, .. } => {
16250 if schema
16251 .columns
16252 .iter()
16253 .any(|candidate| candidate.id == *column)
16254 {
16255 Ok(())
16256 } else {
16257 Err(MongrelError::InvalidArgument(format!(
16258 "security expression references unknown column id {column}"
16259 )))
16260 }
16261 }
16262 SecurityExpr::And { left, right } | SecurityExpr::Or { left, right } => {
16263 validate_security_expression(left, schema)?;
16264 validate_security_expression(right, schema)
16265 }
16266 SecurityExpr::Not { expression } => validate_security_expression(expression, schema),
16267 }
16268}
16269
16270fn sweep_unreferenced_table_dirs(root: &Path, cat: &Catalog) -> Result<()> {
16272 let referenced = cat
16273 .tables
16274 .iter()
16275 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
16276 .map(|entry| entry.table_id)
16277 .collect::<HashSet<_>>();
16278 let tables_dir = root.join(TABLES_DIR);
16279 let entries = match std::fs::read_dir(&tables_dir) {
16280 Ok(entries) => entries,
16281 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
16282 Err(error) => return Err(error.into()),
16283 };
16284 for entry in entries {
16285 let entry = entry?;
16286 if !entry.file_type()?.is_dir() {
16287 continue;
16288 }
16289 let file_name = entry.file_name();
16290 let Some(name) = file_name.to_str() else {
16291 continue;
16292 };
16293 let Ok(table_id) = name.parse::<u64>() else {
16294 continue;
16295 };
16296 if name != table_id.to_string() {
16297 continue;
16298 }
16299 if !referenced.contains(&table_id) {
16300 crate::durable_file::remove_directory_all(&entry.path())?;
16301 }
16302 }
16303 Ok(())
16304}
16305
16306fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
16311 for entry in &cat.tables {
16312 let txn_dir = root
16313 .join(TABLES_DIR)
16314 .join(entry.table_id.to_string())
16315 .join("_txn");
16316 if txn_dir.exists() {
16317 let _ = std::fs::remove_dir_all(&txn_dir);
16318 }
16319 }
16320}
16321
16322#[cfg(test)]
16323mod write_permission_tests {
16324 use super::*;
16325 use crate::txn::Staged;
16326
16327 struct NoopExternalBridge;
16328
16329 impl ExternalTriggerBridge for NoopExternalBridge {
16330 fn apply_trigger_external_write(
16331 &self,
16332 _entry: &ExternalTableEntry,
16333 base_state: Vec<u8>,
16334 _op: ExternalTriggerWrite,
16335 ) -> Result<ExternalTriggerWriteResult> {
16336 Ok(ExternalTriggerWriteResult::new(base_state))
16337 }
16338 }
16339
16340 fn assert_txn_namespace_full<T>(result: Result<T>) {
16341 assert!(matches!(result, Err(MongrelError::Full(_))));
16342 }
16343
16344 #[test]
16345 fn every_begin_api_preserves_transaction_id_exhaustion_without_wal_mutation() {
16346 let directory = tempfile::tempdir().unwrap();
16347 let database = Database::create(directory.path()).unwrap();
16348 let generation = (*database.next_txn_id.lock() >> 32).saturating_add(1);
16349 *database.next_txn_id.lock() = generation << 32;
16350 let before = crate::wal::SharedWal::replay(directory.path())
16351 .unwrap()
16352 .len();
16353 let bridge = NoopExternalBridge;
16354
16355 assert_txn_namespace_full(database.begin().commit());
16356 assert_txn_namespace_full(database.begin_as(None).commit_with_row_ids());
16357 assert_txn_namespace_full(
16358 database
16359 .begin_with_isolation(crate::txn::IsolationLevel::Serializable)
16360 .commit(),
16361 );
16362 assert_txn_namespace_full(
16363 database
16364 .begin_with_external_trigger_bridge(&bridge)
16365 .commit(),
16366 );
16367 assert_txn_namespace_full(
16368 database
16369 .begin_with_external_trigger_bridge_as(&bridge, None)
16370 .commit_controlled(&crate::ExecutionControl::new(None), || Ok(())),
16371 );
16372
16373 assert_eq!(
16374 crate::wal::SharedWal::replay(directory.path())
16375 .unwrap()
16376 .len(),
16377 before
16378 );
16379 drop(database);
16380 Database::open(directory.path()).unwrap();
16381 }
16382
16383 #[test]
16384 fn recovered_storage_identity_mismatch_does_not_mutate_directory() {
16385 let directory = tempfile::tempdir().unwrap();
16386 let table_dir = directory.path().join("7");
16387 crate::durable_file::create_directory_all(&table_dir).unwrap();
16388 let original_schema = test_schema();
16389 crate::engine::write_schema(&table_dir, &original_schema).unwrap();
16390 let mut manifest = crate::manifest::Manifest::new(8, original_schema.schema_id);
16391 crate::manifest::write_atomic(&table_dir, &mut manifest, None).unwrap();
16392 let schema_path = table_dir.join(crate::engine::SCHEMA_FILENAME);
16393 let original_bytes = std::fs::read(&schema_path).unwrap();
16394
16395 let mut replacement_schema = original_schema;
16396 replacement_schema.schema_id += 1;
16397 assert!(matches!(
16398 ensure_recovered_table_storage(None, None, &table_dir, 7, &replacement_schema, None,),
16399 Err(MongrelError::Conflict(_))
16400 ));
16401
16402 assert_eq!(std::fs::read(schema_path).unwrap(), original_bytes);
16403 assert!(!table_dir.join(crate::engine::WAL_DIR).exists());
16404 assert!(!table_dir.join(crate::engine::RUNS_DIR).exists());
16405 assert_eq!(crate::manifest::read(&table_dir, None).unwrap().table_id, 8);
16406 }
16407
16408 #[test]
16409 fn catalog_table_missing_storage_fails_without_recreating_it() {
16410 let directory = tempfile::tempdir().unwrap();
16411 let table_dir = {
16412 let database = Database::create(directory.path()).unwrap();
16413 database.create_table("docs", test_schema()).unwrap();
16414 directory
16415 .path()
16416 .join(TABLES_DIR)
16417 .join(database.table_id("docs").unwrap().to_string())
16418 };
16419 std::fs::remove_dir_all(&table_dir).unwrap();
16420
16421 assert!(matches!(
16422 Database::open(directory.path()),
16423 Err(MongrelError::NotFound(_))
16424 ));
16425 assert!(!table_dir.exists());
16426 }
16427
16428 #[test]
16429 fn authentication_and_principal_resolution_share_one_catalog_snapshot() {
16430 let directory = tempfile::tempdir().unwrap();
16431 let database = std::sync::Arc::new(
16432 Database::create_with_credentials(directory.path(), "admin", "admin-password").unwrap(),
16433 );
16434 database.create_user("alice", "old-password").unwrap();
16435 let old_identity = database.user_identity("alice").unwrap();
16436 let (verified_tx, verified_rx) = std::sync::mpsc::channel();
16437 let (resume_tx, resume_rx) = std::sync::mpsc::channel();
16438 let (mutation_started_tx, mutation_started_rx) = std::sync::mpsc::channel();
16439 let (mutation_done_tx, mutation_done_rx) = std::sync::mpsc::channel();
16440
16441 std::thread::scope(|scope| {
16442 let authenticate = {
16443 let database = std::sync::Arc::clone(&database);
16444 scope.spawn(move || {
16445 database.authenticate_principal_inner("alice", "old-password", || {
16446 verified_tx.send(()).unwrap();
16447 resume_rx.recv().unwrap();
16448 })
16449 })
16450 };
16451 verified_rx.recv().unwrap();
16452 let mutate = {
16453 let database = std::sync::Arc::clone(&database);
16454 scope.spawn(move || {
16455 mutation_started_tx.send(()).unwrap();
16456 database.drop_user("alice").unwrap();
16457 database.create_user("alice", "new-password").unwrap();
16458 mutation_done_tx.send(()).unwrap();
16459 })
16460 };
16461 mutation_started_rx.recv().unwrap();
16462 assert!(mutation_done_rx
16463 .recv_timeout(std::time::Duration::from_millis(50))
16464 .is_err());
16465 resume_tx.send(()).unwrap();
16466 let principal = authenticate.join().unwrap().unwrap().unwrap();
16467 assert_eq!((principal.user_id, principal.created_epoch), old_identity);
16468 mutate.join().unwrap();
16469 });
16470
16471 assert_ne!(database.user_identity("alice").unwrap(), old_identity);
16472 assert!(database
16473 .authenticate_principal("alice", "old-password")
16474 .unwrap()
16475 .is_none());
16476 assert!(database
16477 .authenticate_principal("alice", "new-password")
16478 .unwrap()
16479 .is_some());
16480 }
16481
16482 #[test]
16483 fn homogeneous_batch_summarizes_to_one_permission_decision() {
16484 let staging = (0..10_050)
16485 .map(|_| {
16486 (
16487 7,
16488 Staged::Put(vec![(2, Value::Int64(2)), (1, Value::Int64(1))]),
16489 )
16490 })
16491 .collect::<Vec<_>>();
16492
16493 let needs = summarize_write_permissions(&staging);
16494 let table = needs.get(&7).unwrap();
16495 assert_eq!(needs.len(), 1);
16496 assert!(table.insert);
16497 assert_eq!(table.insert_columns, [1, 2]);
16498 assert!(!table.update);
16499 assert!(!table.delete);
16500 assert!(!table.truncate);
16501 }
16502
16503 #[test]
16504 fn mixed_writes_union_columns_and_preserve_empty_operations() {
16505 let staging = vec![
16506 (7, Staged::Put(vec![(2, Value::Int64(2))])),
16507 (7, Staged::Put(vec![(1, Value::Int64(1))])),
16508 (
16509 7,
16510 Staged::Update {
16511 row_id: RowId(1),
16512 new_row: vec![(1, Value::Int64(1)), (2, Value::Int64(2))],
16513 changed_columns: vec![2],
16514 },
16515 ),
16516 (7, Staged::Delete(RowId(2))),
16517 (8, Staged::Truncate),
16518 ];
16519
16520 let needs = summarize_write_permissions(&staging);
16521 let table = needs.get(&7).unwrap();
16522 assert_eq!(table.insert_columns, [1, 2]);
16523 assert!(table.update);
16524 assert_eq!(table.update_columns, [2]);
16525 assert!(table.delete);
16526 assert!(needs.get(&8).unwrap().truncate);
16527 }
16528
16529 #[test]
16530 fn final_permission_decisions_do_not_scale_with_rows() {
16531 let credentialless_dir = tempfile::tempdir().unwrap();
16532 let credentialless = Database::create(credentialless_dir.path()).unwrap();
16533 credentialless.create_table("docs", test_schema()).unwrap();
16534 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16535 credentialless
16536 .validate_write_permissions(&puts(credentialless.table_id("docs").unwrap()), None, None)
16537 .unwrap();
16538 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 0));
16539
16540 let authenticated_dir = tempfile::tempdir().unwrap();
16541 let authenticated =
16542 Database::create_with_credentials(authenticated_dir.path(), "admin", "admin-password")
16543 .unwrap();
16544 authenticated.create_table("docs", test_schema()).unwrap();
16545 let admin = authenticated.resolve_principal("admin").unwrap();
16546 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16547 authenticated
16548 .validate_write_permissions(
16549 &puts(authenticated.table_id("docs").unwrap()),
16550 Some(&admin),
16551 None,
16552 )
16553 .unwrap();
16554 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
16555 }
16556
16557 #[test]
16558 fn delete_batch_checks_permission_once_when_staged_and_once_when_committed() {
16559 let dir = tempfile::tempdir().unwrap();
16560 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
16561 db.create_table("docs", test_schema()).unwrap();
16562 let admin = db.resolve_principal("admin").unwrap();
16563 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16564
16565 let mut transaction = db.begin_as(Some(admin));
16566 transaction
16567 .delete_batch("docs", (0..100).map(RowId).collect())
16568 .unwrap();
16569 transaction.commit().unwrap();
16570
16571 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 2));
16572 }
16573
16574 #[test]
16575 fn truncate_validation_checks_admin_once_for_all_tables() {
16576 let dir = tempfile::tempdir().unwrap();
16577 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
16578 db.create_table("first", test_schema()).unwrap();
16579 db.create_table("second", test_schema()).unwrap();
16580 let admin = db.resolve_principal("admin").unwrap();
16581 let staging = vec![
16582 (db.table_id("first").unwrap(), Staged::Truncate),
16583 (db.table_id("second").unwrap(), Staged::Truncate),
16584 ];
16585
16586 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16587 db.validate_write_permissions(&staging, Some(&admin), None)
16588 .unwrap();
16589 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
16590 }
16591
16592 #[test]
16593 fn one_table_commit_batches_structural_work() {
16594 let dir = tempfile::tempdir().unwrap();
16595 let db = Database::create(dir.path()).unwrap();
16596 db.create_table("docs", test_schema()).unwrap();
16597 let table_id = db.table_id("docs").unwrap();
16598
16599 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(0));
16600 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
16601 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
16602 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
16603 db.transaction(|transaction| {
16604 for id in 0..100 {
16605 transaction.put("docs", vec![(1, Value::Int64(id))])?;
16606 }
16607 Ok(())
16608 })
16609 .unwrap();
16610
16611 AUTO_INCREMENT_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 2));
16612 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16613 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16614 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
16615
16616 let puts = crate::wal::SharedWal::replay(dir.path())
16617 .unwrap()
16618 .into_iter()
16619 .filter_map(|record| match record.op {
16620 crate::wal::Op::Put { table_id: id, rows } if id == table_id => Some(
16621 bincode::deserialize::<Vec<crate::memtable::Row>>(&rows)
16622 .unwrap()
16623 .len(),
16624 ),
16625 _ => None,
16626 })
16627 .collect::<Vec<_>>();
16628 assert_eq!(puts, [100]);
16629
16630 let row_ids = db
16631 .table("docs")
16632 .unwrap()
16633 .lock()
16634 .visible_rows(db.snapshot().0)
16635 .unwrap()
16636 .into_iter()
16637 .take(2)
16638 .map(|row| row.row_id)
16639 .collect::<Vec<_>>();
16640 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
16641 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
16642 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
16643 db.transaction(|transaction| {
16644 for row_id in row_ids {
16645 transaction.delete("docs", row_id)?;
16646 }
16647 Ok(())
16648 })
16649 .unwrap();
16650 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16651 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16652 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
16653
16654 let deletes = crate::wal::SharedWal::replay(dir.path())
16655 .unwrap()
16656 .into_iter()
16657 .filter_map(|record| match record.op {
16658 crate::wal::Op::Delete {
16659 table_id: id,
16660 row_ids,
16661 } if id == table_id => Some(row_ids.len()),
16662 _ => None,
16663 })
16664 .collect::<Vec<_>>();
16665 assert_eq!(deletes, [2]);
16666 }
16667
16668 fn puts(table_id: u64) -> Vec<(u64, Staged)> {
16669 (0..10_050)
16670 .map(|id| (table_id, Staged::Put(vec![(1, Value::Int64(id))])))
16671 .collect()
16672 }
16673
16674 fn test_schema() -> Schema {
16675 Schema {
16676 columns: vec![ColumnDef {
16677 id: 1,
16678 name: "id".into(),
16679 ty: TypeId::Int64,
16680 flags: crate::schema::ColumnFlags::empty()
16681 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
16682 default_value: None,
16683 }],
16684 ..Schema::default()
16685 }
16686 }
16687}
16688
16689#[cfg(test)]
16690mod cdc_bounds_tests {
16691 use super::*;
16692
16693 #[test]
16694 fn retained_byte_limit_rejects_without_allocating_payload() {
16695 let mut retained = 0;
16696 let error = charge_cdc_bytes(
16697 &mut retained,
16698 CDC_MAX_RETAINED_BYTES.saturating_add(1),
16699 "CDC retained bytes",
16700 )
16701 .unwrap_err();
16702 assert!(matches!(
16703 error,
16704 MongrelError::ResourceLimitExceeded {
16705 resource: "CDC retained bytes",
16706 ..
16707 }
16708 ));
16709 }
16710
16711 #[test]
16712 fn row_json_estimate_accounts_for_byte_array_expansion() {
16713 let row = crate::memtable::Row::new(RowId(1), Epoch(1))
16714 .with_column(1, Value::Bytes(vec![0; 1024]));
16715 assert!(cdc_row_json_bytes(&row) >= 1024 * std::mem::size_of::<serde_json::Value>());
16716 }
16717}
16718
16719#[cfg(test)]
16720mod generation_metrics_tests {
16721 use super::*;
16722 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
16723
16724 #[test]
16725 fn legacy_cow_fallback_is_measured() {
16726 let dir = tempfile::tempdir().unwrap();
16727 let table = Table::create(
16728 dir.path(),
16729 Schema {
16730 columns: vec![ColumnDef {
16731 id: 1,
16732 name: "id".into(),
16733 ty: TypeId::Int64,
16734 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
16735 default_value: None,
16736 }],
16737 ..Schema::default()
16738 },
16739 1,
16740 )
16741 .unwrap();
16742 let handle = TableHandle::from_table(table);
16743 let held = match &handle.inner {
16744 TableHandleInner::CopyOnWrite(slot) => Arc::clone(&slot.read()),
16745 TableHandleInner::Direct(_) => unreachable!(),
16746 };
16747
16748 handle.lock().set_sync_byte_threshold(1);
16749
16750 let stats = handle.generation_stats();
16751 assert_eq!(stats.cow_clone_count, 1);
16752 assert!(stats.estimated_cow_clone_bytes > 0);
16753 drop(held);
16754 }
16755}
16756
16757#[cfg(test)]
16758mod trigger_engine_tests {
16759 use super::*;
16760
16761 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
16762 WriteEvent {
16763 table: "test".into(),
16764 kind: TriggerEvent::Insert,
16765 new: Some(TriggerRowImage {
16766 columns: new_cells.iter().cloned().collect(),
16767 }),
16768 old: Some(TriggerRowImage {
16769 columns: old_cells.iter().cloned().collect(),
16770 }),
16771 changed_columns: Vec::new(),
16772 op_indices: Vec::new(),
16773 put_idx: None,
16774 trigger_stack: Vec::new(),
16775 }
16776 }
16777
16778 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
16779 WriteEvent {
16780 table: "test".into(),
16781 kind: TriggerEvent::Insert,
16782 new: Some(TriggerRowImage {
16783 columns: new_cells.iter().cloned().collect(),
16784 }),
16785 old: None,
16786 changed_columns: Vec::new(),
16787 op_indices: Vec::new(),
16788 put_idx: None,
16789 trigger_stack: Vec::new(),
16790 }
16791 }
16792
16793 #[test]
16794 fn value_order_int64_vs_float64() {
16795 assert_eq!(
16796 value_order(&Value::Int64(5), &Value::Float64(5.0)),
16797 Some(std::cmp::Ordering::Equal)
16798 );
16799 assert_eq!(
16800 value_order(&Value::Int64(5), &Value::Float64(3.0)),
16801 Some(std::cmp::Ordering::Greater)
16802 );
16803 assert_eq!(
16804 value_order(&Value::Int64(2), &Value::Float64(3.0)),
16805 Some(std::cmp::Ordering::Less)
16806 );
16807 }
16808
16809 #[test]
16810 fn value_order_null_returns_none() {
16811 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
16812 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
16813 assert_eq!(value_order(&Value::Null, &Value::Null), None);
16814 }
16815
16816 #[test]
16817 fn value_order_cross_group_returns_none() {
16818 assert_eq!(
16819 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
16820 None
16821 );
16822 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
16823 assert_eq!(
16824 value_order(
16825 &Value::Embedding(vec![1.0, 2.0]),
16826 &Value::Embedding(vec![1.0, 2.0])
16827 ),
16828 None
16829 );
16830 }
16831
16832 #[test]
16833 fn eval_trigger_expr_ranges_and_booleans() {
16834 let expr = TriggerExpr::And {
16835 left: Box::new(TriggerExpr::Gt {
16836 left: TriggerValue::NewColumn(1),
16837 right: TriggerValue::Literal(Value::Int64(0)),
16838 }),
16839 right: Box::new(TriggerExpr::Lte {
16840 left: TriggerValue::NewColumn(1),
16841 right: TriggerValue::Literal(Value::Int64(100)),
16842 }),
16843 };
16844 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
16845 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
16846 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
16847
16848 let or_expr = TriggerExpr::Or {
16849 left: Box::new(TriggerExpr::Lt {
16850 left: TriggerValue::NewColumn(1),
16851 right: TriggerValue::Literal(Value::Int64(0)),
16852 }),
16853 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
16854 TriggerValue::OldColumn(2),
16855 )))),
16856 };
16857 assert!(eval_trigger_expr(
16858 &or_expr,
16859 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
16860 )
16861 .unwrap());
16862 assert!(!eval_trigger_expr(
16863 &or_expr,
16864 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
16865 )
16866 .unwrap());
16867
16868 assert!(eval_trigger_expr(
16869 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
16870 &event_insert(&[])
16871 )
16872 .unwrap());
16873 assert!(!eval_trigger_expr(
16874 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
16875 &event_insert(&[])
16876 )
16877 .unwrap());
16878 assert!(!eval_trigger_expr(
16879 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
16880 &event_insert(&[])
16881 )
16882 .unwrap());
16883 }
16884}