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 search_for_current_principal(
4352 &self,
4353 table_name: &str,
4354 request: &crate::query::SearchRequest,
4355 ) -> Result<Vec<crate::query::SearchHit>> {
4356 let mut columns = crate::query::condition_columns(&request.must);
4357 for named in &request.retrievers {
4358 columns.push(named.retriever.column_id());
4359 }
4360 if let Some(crate::query::Rerank::ExactVector { embedding_column, .. }) = &request.rerank {
4361 columns.push(*embedding_column);
4362 }
4363 if let Some(proj) = &request.projection {
4364 columns.extend(proj);
4365 }
4366 columns.sort_unstable();
4367 columns.dedup();
4368 self.with_authorized_scored_read_context_at(
4369 table_name,
4370 None,
4371 true,
4372 Some(&ReadAuthorization {
4373 operation: crate::auth::ColumnOperation::Select,
4374 columns: columns.clone(),
4375 permissions: Vec::new(),
4376 }),
4377 None,
4378 None,
4379 |table, snapshot, authorization, principal| {
4380 self.require_columns_for(
4381 table_name,
4382 crate::auth::ColumnOperation::Select,
4383 &columns,
4384 principal,
4385 )?;
4386 let hits = table.search_at_with_candidate_authorization_on_generation(
4387 request,
4388 snapshot,
4389 authorization,
4390 None,
4391 )?;
4392 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
4393 let mut secured: Vec<crate::query::SearchHit> = Vec::with_capacity(hits.len());
4394 for mut hit in hits {
4395 let row = crate::memtable::Row {
4396 row_id: hit.row_id,
4397 committed_epoch: crate::Epoch(0),
4398 columns: hit.cells.iter().cloned().collect::<std::collections::HashMap<u16, crate::Value>>(),
4399 deleted: false,
4400 };
4401 let mut rows = self.secure_rows_for(table_name, vec![row], principal)?;
4402 if let Some(mut row) = rows.pop() {
4403 row.columns.retain(|column, _| allowed_columns.contains(column));
4404 hit.cells = row.columns.into_iter().collect::<Vec<_>>();
4405 hit.cells.sort_by_key(|(column, _)| *column);
4406 secured.push(hit);
4407 }
4408 }
4409 Ok(secured)
4410 },
4411 )
4412 }
4413
4414 pub fn authorized_read_snapshot(
4417 &self,
4418 table: &str,
4419 principal: Option<&crate::auth::Principal>,
4420 ) -> Result<AuthorizedReadSnapshot> {
4421 let (security, security_version, effective_principal) = {
4422 let catalog = self.catalog.read();
4423 (
4424 catalog.security.clone(),
4425 catalog.security_version,
4426 self.principal_for_authorized_read(&catalog, principal, false)?,
4427 )
4428 };
4429 let handle = self.table(table)?;
4430 let (table_snapshot, data_generation, allowed_row_ids) = {
4431 let table_handle = handle.lock();
4432 let table_snapshot = table_handle.snapshot();
4433 let data_generation = table_handle.data_generation();
4434 let allowed = self.allowed_row_ids_locked(
4435 table,
4436 &table_handle,
4437 table_snapshot,
4438 (&security, security_version),
4439 effective_principal.as_ref(),
4440 None,
4441 )?;
4442 (
4443 table_snapshot,
4444 data_generation,
4445 allowed.map(|allowed| (*allowed).clone()),
4446 )
4447 };
4448 Ok(AuthorizedReadSnapshot {
4449 table: table.to_string(),
4450 table_snapshot,
4451 data_generation,
4452 security_version,
4453 allowed_row_ids,
4454 })
4455 }
4456
4457 pub fn authorized_read_snapshot_valid(&self, snapshot: &AuthorizedReadSnapshot) -> bool {
4458 if self.catalog.read().security_version != snapshot.security_version {
4459 return false;
4460 }
4461 self.table(&snapshot.table)
4462 .ok()
4463 .is_some_and(|table| table.lock().data_generation() == snapshot.data_generation)
4464 }
4465
4466 pub fn rls_cache_stats(&self) -> RlsCacheStats {
4467 self.rls_cache.lock().stats()
4468 }
4469
4470 pub fn rows_for(
4472 &self,
4473 table: &str,
4474 principal: Option<&crate::auth::Principal>,
4475 ) -> Result<Vec<crate::memtable::Row>> {
4476 if principal.is_none() && self.principal.read().is_some() {
4477 self.refresh_principal()?;
4478 }
4479 let allowed = self.select_column_ids_for(table, principal)?;
4480 let handle = self.table(table)?;
4481 let rows = {
4482 let table = handle.lock();
4483 table.visible_rows(table.snapshot())?
4484 };
4485 let mut rows = self.secure_rows_for(table, rows, principal)?;
4486 for row in &mut rows {
4487 row.columns.retain(|column, _| allowed.contains(column));
4488 }
4489 Ok(rows)
4490 }
4491
4492 pub fn rows_at_epoch_for_current_principal(
4495 &self,
4496 table_name: &str,
4497 snapshot: Snapshot,
4498 ) -> Result<Vec<crate::memtable::Row>> {
4499 self.with_authorized_read_context(
4500 table_name,
4501 None,
4502 true,
4503 Some(&ReadAuthorization {
4504 operation: crate::auth::ColumnOperation::Select,
4505 columns: Vec::new(),
4506 permissions: Vec::new(),
4507 }),
4508 None,
4509 Some(snapshot),
4510 |table, snapshot, allowed, principal| {
4511 let allowed_columns = self.select_column_ids_for(table_name, principal)?;
4512 let mut rows = table.visible_rows(snapshot)?;
4513 if let Some(allowed) = allowed {
4514 rows.retain(|row| allowed.contains(&row.row_id));
4515 }
4516 rows = self.secure_rows_for(table_name, rows, principal)?;
4517 for row in &mut rows {
4518 row.columns
4519 .retain(|column, _| allowed_columns.contains(column));
4520 }
4521 Ok(rows)
4522 },
4523 )
4524 }
4525
4526 pub fn count_for(
4528 &self,
4529 table: &str,
4530 principal: Option<&crate::auth::Principal>,
4531 ) -> Result<u64> {
4532 if principal.is_none() && self.principal.read().is_some() {
4533 self.refresh_principal()?;
4534 }
4535 self.select_column_ids_for(table, principal)?;
4536 if self.security_active_for(table) {
4537 Ok(self.rows_for(table, principal)?.len() as u64)
4538 } else {
4539 Ok(self.table(table)?.lock().count())
4540 }
4541 }
4542
4543 pub fn put_for(
4545 &self,
4546 table: &str,
4547 mut cells: Vec<(u16, crate::memtable::Value)>,
4548 principal: Option<&crate::auth::Principal>,
4549 ) -> Result<RowId> {
4550 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
4551 self.require_columns_for(
4552 table,
4553 crate::auth::ColumnOperation::Insert,
4554 &columns,
4555 principal,
4556 )?;
4557 let handle = self.table(table)?;
4558 let mut table_handle = handle.lock();
4559 table_handle.fill_auto_inc(&mut cells)?;
4560 table_handle.apply_defaults(&mut cells)?;
4561 let mut row = crate::memtable::Row::new(RowId(0), self.epoch.visible());
4562 row.columns.extend(cells.iter().cloned());
4563 self.check_row_policy_for(
4564 table,
4565 crate::security::PolicyCommand::Insert,
4566 &row,
4567 true,
4568 principal,
4569 )?;
4570 table_handle.put(cells)
4571 }
4572
4573 pub fn check_row_policy_for(
4574 &self,
4575 table: &str,
4576 command: crate::security::PolicyCommand,
4577 row: &crate::memtable::Row,
4578 check_new: bool,
4579 principal: Option<&crate::auth::Principal>,
4580 ) -> Result<()> {
4581 let security = self.catalog.read().security.clone();
4582 if !security.rls_enabled(table) {
4583 return Ok(());
4584 }
4585 let cached = self.principal.read().clone();
4586 let principal = principal
4587 .or(cached.as_ref())
4588 .ok_or(MongrelError::AuthRequired)?;
4589 if security.row_allowed(table, command, row, principal, check_new) {
4590 return Ok(());
4591 }
4592 let required = match command {
4593 crate::security::PolicyCommand::Insert => crate::auth::Permission::Insert {
4594 table: table.to_string(),
4595 },
4596 crate::security::PolicyCommand::Update => crate::auth::Permission::Update {
4597 table: table.to_string(),
4598 },
4599 crate::security::PolicyCommand::Select => crate::auth::Permission::Select {
4600 table: table.to_string(),
4601 },
4602 crate::security::PolicyCommand::Delete | crate::security::PolicyCommand::All => {
4603 crate::auth::Permission::Delete {
4604 table: table.to_string(),
4605 }
4606 }
4607 };
4608 Err(MongrelError::PermissionDenied {
4609 required,
4610 principal: principal.username.clone(),
4611 })
4612 }
4613
4614 pub fn set_materialized_view(
4617 &self,
4618 definition: crate::catalog::MaterializedViewEntry,
4619 ) -> Result<()> {
4620 self.set_materialized_view_with_epoch(definition)
4621 .map(|_| ())
4622 }
4623
4624 pub fn set_materialized_view_with_epoch(
4626 &self,
4627 definition: crate::catalog::MaterializedViewEntry,
4628 ) -> Result<Epoch> {
4629 use crate::wal::DdlOp;
4630 use std::sync::atomic::Ordering;
4631
4632 self.require(&crate::auth::Permission::Ddl)?;
4633 if self.poisoned.load(Ordering::Relaxed) {
4634 return Err(MongrelError::Other(
4635 "database poisoned by fsync error".into(),
4636 ));
4637 }
4638 if definition.name.is_empty() || definition.query.trim().is_empty() {
4639 return Err(MongrelError::InvalidArgument(
4640 "materialized view name and query must not be empty".into(),
4641 ));
4642 }
4643
4644 let _ddl = self.ddl_lock.lock();
4645 let _security_write = self.security_write()?;
4646 self.require(&crate::auth::Permission::Ddl)?;
4647 let table_id = self
4648 .catalog
4649 .read()
4650 .live(&definition.name)
4651 .ok_or_else(|| {
4652 MongrelError::NotFound(format!(
4653 "materialized view table {:?} not found",
4654 definition.name
4655 ))
4656 })?
4657 .table_id;
4658 let definition_json = DdlOp::encode_materialized_view(&definition)?;
4659 let _commit = self.commit_lock.lock();
4660 let epoch = self.epoch.bump_assigned();
4661 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
4662 let txn_id = self.alloc_txn_id()?;
4663 let mut next_catalog = self.catalog.read().clone();
4664 if let Some(existing) = next_catalog
4665 .materialized_views
4666 .iter_mut()
4667 .find(|existing| existing.name == definition.name)
4668 {
4669 *existing = definition.clone();
4670 } else {
4671 next_catalog.materialized_views.push(definition.clone());
4672 }
4673 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
4674 let commit_seq = {
4675 let mut wal = self.shared_wal.lock();
4676 let append: Result<u64> = (|| {
4677 wal.append(
4678 txn_id,
4679 table_id,
4680 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
4681 name: definition.name.clone(),
4682 definition_json,
4683 }),
4684 )?;
4685 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
4686 wal.append_commit(txn_id, epoch, &[])
4687 })();
4688 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
4689 };
4690 self.await_durable_commit(commit_seq, epoch)?;
4691
4692 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
4693 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
4694 Ok(epoch)
4695 }
4696
4697 pub fn root(&self) -> &Path {
4699 self.durable_root.canonical_path()
4700 }
4701
4702 pub fn durable_root(&self) -> Arc<crate::durable_file::DurableRoot> {
4705 Arc::clone(&self.durable_root)
4706 }
4707
4708 #[cfg(feature = "encryption")]
4712 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
4713 self.kek
4714 .as_deref()
4715 .map(|kek| kek.derive_subkey(b"mongreldb/server/idempotency/v1"))
4716 }
4717
4718 #[cfg(not(feature = "encryption"))]
4719 pub fn derive_server_idempotency_key(&self) -> Option<zeroize::Zeroizing<[u8; 32]>> {
4720 None
4721 }
4722
4723 pub fn is_read_only_replica(&self) -> bool {
4724 self.read_only
4725 }
4726
4727 pub fn ensure_consistent_read(&self) -> Result<()> {
4731 self.ensure_owner_process()?;
4732 if self.poisoned.load(Ordering::Relaxed) {
4733 return Err(MongrelError::Other(
4734 "database poisoned by post-commit failure; reopen required".into(),
4735 ));
4736 }
4737 Ok(())
4738 }
4739
4740 pub fn set_replication_wal_retention_segments(&self, segments: usize) {
4741 self.replication_wal_retention_segments
4742 .store(segments, std::sync::atomic::Ordering::Relaxed);
4743 }
4744
4745 pub fn replication_snapshot(&self) -> Result<crate::replication::ReplicationSnapshot> {
4750 let admin = crate::auth::Permission::Admin;
4751 self.require(&admin)?;
4752 let operation_principal = self.principal_snapshot();
4753 let _barrier = self.replication_barrier.write();
4754 let _ddl = self.ddl_lock.lock();
4755 let _security = self.security_coordinator.gate.read();
4756 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4757 let mut handles: Vec<_> = self
4758 .tables
4759 .read()
4760 .iter()
4761 .map(|(id, handle)| (*id, handle.clone()))
4762 .collect();
4763 handles.sort_by_key(|(id, _)| *id);
4764 let _table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
4765 let _commit = self.commit_lock.lock();
4766 let mut wal = self.shared_wal.lock();
4767 wal.group_sync()?;
4768 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
4769 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
4770 let epoch = records
4771 .iter()
4772 .filter_map(|record| match &record.op {
4773 crate::wal::Op::TxnCommit { epoch, .. } => Some(*epoch),
4774 _ => None,
4775 })
4776 .max()
4777 .unwrap_or(0)
4778 .max(self.epoch.committed().0);
4779 let files = crate::replication::capture_files(&self.root)?;
4780 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
4781 drop(wal);
4782 Ok(crate::replication::ReplicationSnapshot::new(
4783 source_id, epoch, files,
4784 ))
4785 }
4786
4787 pub fn hot_backup(&self, destination: impl AsRef<Path>) -> Result<crate::backup::BackupReport> {
4795 let control = crate::ExecutionControl::new(None);
4796 self.hot_backup_controlled(destination, &control, || true)
4797 }
4798
4799 pub(crate) fn hot_backup_to_durable_child(
4800 &self,
4801 parent: &crate::durable_file::DurableRoot,
4802 child: &Path,
4803 control: &crate::ExecutionControl,
4804 ) -> Result<crate::backup::BackupReport> {
4805 let mut components = child.components();
4806 if !matches!(components.next(), Some(std::path::Component::Normal(_)))
4807 || components.next().is_some()
4808 {
4809 return Err(MongrelError::InvalidArgument(
4810 "durable backup child must be one relative path component".into(),
4811 ));
4812 }
4813 let destination_name = child.file_name().ok_or_else(|| {
4814 MongrelError::InvalidArgument("durable backup child has no filename".into())
4815 })?;
4816 let prepared = prepare_backup_destination_in(&self.root, parent, destination_name)?;
4817 self.hot_backup_prepared(prepared, control, || true)
4818 }
4819
4820 #[doc(hidden)]
4823 pub fn hot_backup_controlled<F>(
4824 &self,
4825 destination: impl AsRef<Path>,
4826 control: &crate::ExecutionControl,
4827 before_publish: F,
4828 ) -> Result<crate::backup::BackupReport>
4829 where
4830 F: FnOnce() -> bool,
4831 {
4832 let prepared = prepare_backup_destination(&self.root, destination.as_ref())?;
4833 self.hot_backup_prepared(prepared, control, before_publish)
4834 }
4835
4836 fn hot_backup_prepared<F>(
4837 &self,
4838 mut prepared: PreparedBackupDestination,
4839 control: &crate::ExecutionControl,
4840 before_publish: F,
4841 ) -> Result<crate::backup::BackupReport>
4842 where
4843 F: FnOnce() -> bool,
4844 {
4845 let admin = crate::auth::Permission::Admin;
4846 self.require(&admin)?;
4847 let operation_principal = self.principal_snapshot();
4848 control.checkpoint()?;
4849 let destination = prepared.destination_path.clone();
4850 let mut before_publish = Some(before_publish);
4851
4852 let outcome = (|| {
4853 control.checkpoint()?;
4854 let barrier = self.replication_barrier.write();
4855 let ddl = self.ddl_lock.lock();
4856 let security = self.security_coordinator.gate.read();
4857 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4858 let mut handles: Vec<_> = self
4859 .tables
4860 .read()
4861 .iter()
4862 .map(|(id, handle)| (*id, handle.clone()))
4863 .collect();
4864 handles.sort_by_key(|(id, _)| *id);
4865 let table_guards: Vec<_> = handles.iter().map(|(_, handle)| handle.lock()).collect();
4866 let commit = self.commit_lock.lock();
4867 let mut wal = self.shared_wal.lock();
4868 wal.group_sync()?;
4869 let epoch = self.epoch.committed().0;
4870 let boundary_unix_nanos = current_unix_nanos();
4871
4872 let pin_nonce = std::time::SystemTime::now()
4873 .duration_since(std::time::UNIX_EPOCH)
4874 .unwrap_or_default()
4875 .as_nanos();
4876 let file_pin_root = self
4877 .root
4878 .join(META_DIR)
4879 .join("backup-pins")
4880 .join(format!("{}-{pin_nonce}", std::process::id()));
4881 std::fs::create_dir_all(&file_pin_root)?;
4882 let _file_pins = BackupFilePins {
4883 root: file_pin_root.clone(),
4884 };
4885 let mut run_files = Vec::new();
4886 for (index, (table_id, _)) in handles.iter().enumerate() {
4887 if index % 256 == 0 {
4888 control.checkpoint()?;
4889 }
4890 let table = &table_guards[index];
4891 for (run_index, run) in table.run_refs().iter().enumerate() {
4892 if run_index % 256 == 0 {
4893 control.checkpoint()?;
4894 }
4895 let source = table.run_path(run.run_id as u64);
4896 let relative = Path::new(TABLES_DIR)
4897 .join(table_id.to_string())
4898 .join(crate::engine::RUNS_DIR)
4899 .join(format!("r-{}.sr", run.run_id));
4900 let pinned = file_pin_root.join(format!("{table_id}-{}.sr", run.run_id));
4901 if std::fs::hard_link(&source, &pinned).is_err() {
4902 crate::backup::copy_file_synced(&source, &pinned)?;
4903 }
4904 run_files.push(((*table_id, run.run_id), pinned, relative));
4905 }
4906 }
4907 crate::durable_file::sync_directory(&file_pin_root)?;
4908 let run_keys: Vec<_> = run_files.iter().map(|(key, _, _)| *key).collect();
4909 {
4910 let mut pins = self.backup_pins.lock();
4911 for key in &run_keys {
4912 *pins.entry(*key).or_insert(0) += 1;
4913 }
4914 }
4915 let _run_pins = RunPins {
4916 pins: Arc::clone(&self.backup_pins),
4917 runs: run_keys,
4918 };
4919 let deferred: HashSet<_> = run_files
4920 .iter()
4921 .map(|(_, _, relative)| relative.clone())
4922 .collect();
4923 let mut copied = Vec::new();
4924 copy_backup_boundary(
4925 &self.root,
4926 prepared.stage.as_deref().ok_or_else(|| {
4927 MongrelError::Other("backup staging root was already released".into())
4928 })?,
4929 &deferred,
4930 &mut copied,
4931 Some(control),
4932 )?;
4933
4934 drop(wal);
4935 drop(commit);
4936 drop(table_guards);
4937 drop(security);
4938 drop(ddl);
4939 drop(barrier);
4940
4941 if let Some(hook) = self.backup_hook.lock().as_ref() {
4942 hook();
4943 }
4944 for (index, (_, source, relative)) in run_files.into_iter().enumerate() {
4945 if index % 256 == 0 {
4946 control.checkpoint()?;
4947 }
4948 let mut source = crate::durable_file::open_regular_nofollow(&source)?;
4949 prepared
4950 .stage
4951 .as_deref()
4952 .ok_or_else(|| {
4953 MongrelError::Other("backup staging root was already released".into())
4954 })?
4955 .copy_new_from(&relative, &mut source)?;
4956 copied.push(relative);
4957 }
4958
4959 let manifest = crate::backup::BackupManifest::create_controlled_durable(
4960 prepared.stage.as_deref().ok_or_else(|| {
4961 MongrelError::Other("backup staging root was already released".into())
4962 })?,
4963 epoch,
4964 &copied,
4965 control,
4966 )?;
4967 manifest.write_to_durable(prepared.stage.as_deref().ok_or_else(|| {
4968 MongrelError::Other("backup staging root was already released".into())
4969 })?)?;
4970 control.checkpoint()?;
4971 let publish = before_publish.take().ok_or_else(|| {
4972 MongrelError::Other("backup publication callback already consumed".into())
4973 })?;
4974 if !publish() {
4975 return Err(MongrelError::Cancelled);
4976 }
4977 let final_security = self.security_coordinator.gate.read();
4978 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
4979 drop(prepared.stage.take().ok_or_else(|| {
4983 MongrelError::Other("backup staging root was already released".into())
4984 })?);
4985 let published = std::cell::Cell::new(false);
4986 if let Err(error) = prepared.parent.rename_directory_new_with_after(
4987 Path::new(&prepared.stage_name),
4988 &prepared.parent,
4989 Path::new(&prepared.destination_name),
4990 || published.set(true),
4991 ) {
4992 if published.get() {
4993 return Err(MongrelError::CommitOutcomeUnknown {
4994 epoch,
4995 message: format!("backup publication was not durable: {error}"),
4996 });
4997 }
4998 return Err(error.into());
4999 }
5000 drop(final_security);
5001 Ok(crate::backup::BackupReport {
5002 destination,
5003 epoch,
5004 boundary_unix_nanos,
5005 files: manifest.files.len(),
5006 bytes: manifest.total_bytes(),
5007 })
5008 })();
5009
5010 if outcome.is_err() {
5011 drop(prepared.stage.take());
5012 let _ = prepared
5013 .parent
5014 .remove_directory_all(Path::new(&prepared.stage_name));
5015 }
5016 outcome
5017 }
5018
5019 pub fn replication_batch_since(
5022 &self,
5023 since_epoch: u64,
5024 ) -> Result<crate::replication::ReplicationBatch> {
5025 use crate::wal::Op;
5026
5027 let admin = crate::auth::Permission::Admin;
5028 self.require(&admin)?;
5029 let operation_principal = self.principal_snapshot();
5030
5031 let mut wal = self.shared_wal.lock();
5032 wal.group_sync()?;
5033 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
5034 let records = crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?;
5035 drop(wal);
5036
5037 let commits: HashMap<u64, u64> = records
5038 .iter()
5039 .filter_map(|record| match &record.op {
5040 Op::TxnCommit { epoch, .. } => Some((record.txn_id, *epoch)),
5041 _ => None,
5042 })
5043 .collect();
5044 let earliest_epoch = commits.values().copied().min();
5045 let current_epoch = commits
5046 .values()
5047 .copied()
5048 .max()
5049 .unwrap_or(0)
5050 .max(self.epoch.committed().0);
5051 let selected: HashSet<u64> = commits
5052 .iter()
5053 .filter_map(|(txn_id, epoch)| (*epoch > since_epoch).then_some(*txn_id))
5054 .collect();
5055 let retention_gap = since_epoch < current_epoch
5056 && since_epoch < crate::replication::replication_wal_floor(&self.root)?;
5057 let spilled = records.iter().any(|record| {
5058 selected.contains(&record.txn_id)
5059 && matches!(
5060 &record.op,
5061 Op::TxnCommit { added_runs, .. } if !added_runs.is_empty()
5062 )
5063 });
5064 let records = records
5065 .into_iter()
5066 .filter(|record| record.txn_id != crate::wal::SYSTEM_TXN_ID)
5067 .filter(|record| selected.contains(&record.txn_id))
5068 .collect();
5069 let source_id = crate::replication::replication_identity_durable(&self.durable_root)?;
5070 let batch = crate::replication::ReplicationBatch::complete_for_source(
5071 source_id,
5072 since_epoch,
5073 current_epoch,
5074 earliest_epoch,
5075 retention_gap,
5076 spilled,
5077 records,
5078 )?;
5079 if let Some(hook) = self.replication_hook.lock().as_ref() {
5080 hook();
5081 }
5082 let _security = self.security_coordinator.gate.read();
5083 self.require_exact_principal_current(operation_principal.as_ref(), &admin)?;
5084 Ok(batch)
5085 }
5086
5087 pub fn append_replication_batch(
5091 &self,
5092 batch: &crate::replication::ReplicationBatch,
5093 ) -> Result<u64> {
5094 use crate::wal::Op;
5095
5096 if !self.read_only {
5097 return Err(MongrelError::InvalidArgument(
5098 "replication batches may only target a marked replica".into(),
5099 ));
5100 }
5101 let current = crate::replication::replica_epoch(&self.root)?;
5102 if batch.is_source_bound() {
5103 let source_id = crate::replication::replica_source_id_durable(&self.durable_root)?;
5104 if batch.source_id != source_id {
5105 return Err(MongrelError::Conflict(
5106 "replication batch source does not match follower binding".into(),
5107 ));
5108 }
5109 }
5110 if batch.requires_snapshot {
5111 return Err(MongrelError::Conflict(
5112 "replication snapshot required for this batch".into(),
5113 ));
5114 }
5115 batch.validate_proof()?;
5116 if batch.from_epoch != current {
5117 if batch.from_epoch < current && batch.current_epoch == current {
5118 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
5119 let _wal = self.shared_wal.lock();
5120 let existing: HashSet<(u64, u64)> =
5121 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
5122 .into_iter()
5123 .filter_map(|record| match record.op {
5124 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
5125 _ => None,
5126 })
5127 .collect();
5128 let already_applied = batch.records.iter().all(|record| match &record.op {
5129 Op::TxnCommit { epoch, .. } => existing.contains(&(record.txn_id, *epoch)),
5130 _ => true,
5131 });
5132 if already_applied {
5133 return Ok(current);
5134 }
5135 }
5136 return Err(MongrelError::Conflict(format!(
5137 "replication batch starts at epoch {}, follower is at epoch {current}",
5138 batch.from_epoch
5139 )));
5140 }
5141 if batch.current_epoch < current {
5142 return Err(MongrelError::InvalidArgument(format!(
5143 "replication batch current epoch {} precedes follower epoch {current}",
5144 batch.current_epoch
5145 )));
5146 }
5147 let records = &batch.records;
5148 let mut commits = HashMap::new();
5149 let mut commit_epochs = HashSet::new();
5150 let mut commit_timestamps = HashMap::new();
5151 for record in records {
5152 match &record.op {
5153 Op::TxnCommit { epoch, added_runs } => {
5154 if !added_runs.is_empty() {
5155 return Err(MongrelError::Conflict(
5156 "replication snapshot required for spilled-run transaction".into(),
5157 ));
5158 }
5159 if commits.insert(record.txn_id, *epoch).is_some() {
5160 return Err(MongrelError::InvalidArgument(format!(
5161 "duplicate commit for replication transaction {}",
5162 record.txn_id
5163 )));
5164 }
5165 if *epoch <= current || *epoch > batch.current_epoch {
5166 return Err(MongrelError::InvalidArgument(format!(
5167 "replication commit epoch {epoch} is outside ({current}, {}]",
5168 batch.current_epoch
5169 )));
5170 }
5171 if !commit_epochs.insert(*epoch) {
5172 return Err(MongrelError::InvalidArgument(format!(
5173 "duplicate replication commit epoch {epoch}"
5174 )));
5175 }
5176 }
5177 Op::CommitTimestamp { unix_nanos } => {
5178 commit_timestamps.insert(record.txn_id, *unix_nanos);
5179 }
5180 _ => {}
5181 }
5182 }
5183 for record in records {
5184 if record.txn_id == crate::wal::SYSTEM_TXN_ID
5185 || matches!(&record.op, Op::TxnAbort | Op::Flush { .. })
5186 {
5187 return Err(MongrelError::InvalidArgument(
5188 "replication batch contains a non-committed record".into(),
5189 ));
5190 }
5191 if !commits.contains_key(&record.txn_id) {
5192 return Err(MongrelError::InvalidArgument(format!(
5193 "incomplete replication transaction {}",
5194 record.txn_id
5195 )));
5196 }
5197 }
5198 let target_epoch = commits
5199 .values()
5200 .copied()
5201 .filter(|epoch| *epoch > current)
5202 .max()
5203 .unwrap_or(current);
5204 if target_epoch != batch.current_epoch {
5205 return Err(MongrelError::InvalidArgument(format!(
5206 "replication batch ends at epoch {target_epoch}, expected {}",
5207 batch.current_epoch
5208 )));
5209 }
5210 let mut selected: HashSet<u64> = commits
5211 .iter()
5212 .filter_map(|(txn_id, epoch)| (*epoch > current).then_some(*txn_id))
5213 .collect();
5214 if selected.is_empty() {
5215 return Ok(current);
5216 }
5217 let mut wal = self.shared_wal.lock();
5218 wal.group_sync()?;
5219 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
5220 let existing: HashSet<(u64, u64)> =
5221 crate::wal::SharedWal::replay_with_dek(&self.root, wal_dek.as_ref())?
5222 .into_iter()
5223 .filter_map(|record| match record.op {
5224 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
5225 _ => None,
5226 })
5227 .collect();
5228 selected.retain(|txn_id| {
5229 commits
5230 .get(txn_id)
5231 .is_some_and(|epoch| !existing.contains(&(*txn_id, *epoch)))
5232 });
5233 for record in records {
5234 if !selected.contains(&record.txn_id) {
5235 continue;
5236 }
5237 match &record.op {
5238 Op::TxnCommit { epoch, added_runs } => {
5239 let timestamp = commit_timestamps
5240 .get(&record.txn_id)
5241 .copied()
5242 .unwrap_or_else(current_unix_nanos);
5243 wal.append_commit_at(record.txn_id, Epoch(*epoch), added_runs, timestamp)?;
5244 }
5245 Op::TxnAbort | Op::Flush { .. } | Op::CommitTimestamp { .. } => {}
5246 op => {
5247 wal.append(record.txn_id, 0, op.clone())?;
5248 }
5249 }
5250 }
5251 if !selected.is_empty() {
5252 wal.group_sync()?;
5253 }
5254 drop(wal);
5255
5256 let mut recovered_catalog = self.catalog.read().clone();
5260 if let Err(error) = recover_ddl_from_wal(
5261 &self.root,
5262 Some(&self.durable_root),
5263 &mut recovered_catalog,
5264 self.meta_dek.as_ref(),
5265 wal_dek.as_ref(),
5266 true,
5267 None,
5268 ) {
5269 return Err(MongrelError::DurableCommit {
5270 epoch: target_epoch,
5271 message: format!(
5272 "replication WAL is durable but catalog checkpoint failed: {error}"
5273 ),
5274 });
5275 }
5276 let _security = self.security_coordinator.gate.write();
5277 let old_security_version = self.catalog.read().security_version;
5278 let security_changed = old_security_version != recovered_catalog.security_version
5279 || self.catalog.read().require_auth != recovered_catalog.require_auth;
5280 let require_auth = recovered_catalog.require_auth;
5281 let principal = if security_changed {
5282 None
5283 } else {
5284 self.principal.read().as_ref().and_then(|principal| {
5285 Self::resolve_bound_principal_from_catalog(&recovered_catalog, principal)
5286 })
5287 };
5288 if require_auth {
5289 self.auth_state.set_require_auth(true);
5290 }
5291 self.auth_state.set_principal(principal.clone());
5292 *self.principal.write() = principal;
5293 let security_version = recovered_catalog.security_version;
5294 *self.catalog.write() = recovered_catalog;
5295 self.security_coordinator
5296 .version
5297 .store(security_version, Ordering::Release);
5298 if !require_auth {
5299 self.auth_state.set_require_auth(false);
5300 }
5301 if let Err(error) =
5302 crate::replication::reconcile_replica_epoch_durable(&self.durable_root, target_epoch)
5303 {
5304 return Err(MongrelError::DurableCommit {
5305 epoch: target_epoch,
5306 message: format!(
5307 "replication WAL and catalog are durable but follower watermark failed: {error}"
5308 ),
5309 });
5310 }
5311 Ok(target_epoch)
5312 }
5313
5314 pub fn table_id(&self, name: &str) -> Result<u64> {
5317 let cat = self.catalog.read();
5318 cat.live(name)
5319 .map(|e| e.table_id)
5320 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
5321 }
5322
5323 pub fn table_identity(&self, name: &str) -> Result<(u64, u64)> {
5328 let catalog = self.catalog.read();
5329 catalog
5330 .live(name)
5331 .map(|entry| (entry.table_id, entry.schema.schema_id))
5332 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))
5333 }
5334
5335 pub(crate) fn building_table_id(&self, name: &str) -> Result<u64> {
5336 self.catalog
5337 .read()
5338 .building(name)
5339 .map(|entry| entry.table_id)
5340 .ok_or_else(|| MongrelError::NotFound(format!("building table {name:?} not found")))
5341 }
5342
5343 pub fn procedures(&self) -> Vec<StoredProcedure> {
5344 self.catalog
5345 .read()
5346 .procedures
5347 .iter()
5348 .map(|p| p.procedure.clone())
5349 .collect()
5350 }
5351
5352 pub fn procedure(&self, name: &str) -> Option<StoredProcedure> {
5353 self.catalog
5354 .read()
5355 .procedures
5356 .iter()
5357 .find(|p| p.procedure.name == name)
5358 .map(|p| p.procedure.clone())
5359 }
5360
5361 pub fn create_procedure(&self, procedure: StoredProcedure) -> Result<StoredProcedure> {
5362 self.create_procedure_inner(procedure, None)
5363 }
5364
5365 pub fn create_procedure_controlled<F>(
5366 &self,
5367 procedure: StoredProcedure,
5368 mut before_publish: F,
5369 ) -> Result<StoredProcedure>
5370 where
5371 F: FnMut() -> Result<()>,
5372 {
5373 self.create_procedure_inner(procedure, Some(&mut before_publish))
5374 }
5375
5376 fn create_procedure_inner(
5377 &self,
5378 mut procedure: StoredProcedure,
5379 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5380 ) -> Result<StoredProcedure> {
5381 self.require(&crate::auth::Permission::Ddl)?;
5382 let _g = self.ddl_lock.lock();
5383 let _security_write = self.security_write()?;
5384 self.require(&crate::auth::Permission::Ddl)?;
5385 procedure.validate()?;
5386 self.validate_procedure_references(&procedure)?;
5387 {
5388 let cat = self.catalog.read();
5389 if cat
5390 .procedures
5391 .iter()
5392 .any(|p| p.procedure.name == procedure.name)
5393 {
5394 return Err(MongrelError::InvalidArgument(format!(
5395 "procedure {:?} already exists",
5396 procedure.name
5397 )));
5398 }
5399 }
5400 let commit_lock = Arc::clone(&self.commit_lock);
5401 let _c = commit_lock.lock();
5402 let epoch = self.epoch.bump_assigned();
5403 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5404 procedure.created_epoch = epoch.0;
5405 procedure.updated_epoch = epoch.0;
5406 let mut next_catalog = self.catalog.read().clone();
5407 next_catalog
5408 .procedures
5409 .push(ProcedureEntry::from(procedure.clone()));
5410 next_catalog.db_epoch = epoch.0;
5411 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5412 Ok(procedure)
5413 }
5414
5415 pub fn create_or_replace_procedure(
5416 &self,
5417 procedure: StoredProcedure,
5418 ) -> Result<StoredProcedure> {
5419 self.create_or_replace_procedure_inner(procedure, None)
5420 }
5421
5422 pub fn create_or_replace_procedure_controlled<F>(
5423 &self,
5424 procedure: StoredProcedure,
5425 mut before_publish: F,
5426 ) -> Result<StoredProcedure>
5427 where
5428 F: FnMut() -> Result<()>,
5429 {
5430 self.create_or_replace_procedure_inner(procedure, Some(&mut before_publish))
5431 }
5432
5433 fn create_or_replace_procedure_inner(
5434 &self,
5435 procedure: StoredProcedure,
5436 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5437 ) -> Result<StoredProcedure> {
5438 self.require(&crate::auth::Permission::Ddl)?;
5439 let _g = self.ddl_lock.lock();
5440 let _security_write = self.security_write()?;
5441 self.require(&crate::auth::Permission::Ddl)?;
5442 procedure.validate()?;
5443 self.validate_procedure_references(&procedure)?;
5444 let commit_lock = Arc::clone(&self.commit_lock);
5445 let _c = commit_lock.lock();
5446 let epoch = self.epoch.bump_assigned();
5447 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5448 let mut next_catalog = self.catalog.read().clone();
5449 let replaced = {
5450 let next = match next_catalog
5451 .procedures
5452 .iter()
5453 .position(|p| p.procedure.name == procedure.name)
5454 {
5455 Some(idx) => {
5456 let next = next_catalog.procedures[idx]
5457 .procedure
5458 .replaced(procedure.clone(), epoch.0)?;
5459 next_catalog.procedures[idx] = ProcedureEntry::from(next.clone());
5460 next
5461 }
5462 None => {
5463 let mut next = procedure;
5464 next.created_epoch = epoch.0;
5465 next.updated_epoch = epoch.0;
5466 next_catalog
5467 .procedures
5468 .push(ProcedureEntry::from(next.clone()));
5469 next
5470 }
5471 };
5472 next_catalog.db_epoch = epoch.0;
5473 next
5474 };
5475 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5476 Ok(replaced)
5477 }
5478
5479 pub fn drop_procedure(&self, name: &str) -> Result<()> {
5480 self.drop_procedure_with_epoch(name).map(|_| ())
5481 }
5482
5483 pub fn drop_procedure_with_epoch(&self, name: &str) -> Result<Epoch> {
5484 self.drop_procedure_with_epoch_inner(name, None)
5485 }
5486
5487 pub fn drop_procedure_with_epoch_controlled<F>(
5488 &self,
5489 name: &str,
5490 mut before_publish: F,
5491 ) -> Result<Epoch>
5492 where
5493 F: FnMut() -> Result<()>,
5494 {
5495 self.drop_procedure_with_epoch_inner(name, Some(&mut before_publish))
5496 }
5497
5498 fn drop_procedure_with_epoch_inner(
5499 &self,
5500 name: &str,
5501 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5502 ) -> Result<Epoch> {
5503 self.require(&crate::auth::Permission::Ddl)?;
5504 let _g = self.ddl_lock.lock();
5505 let _security_write = self.security_write()?;
5506 self.require(&crate::auth::Permission::Ddl)?;
5507 let commit_lock = Arc::clone(&self.commit_lock);
5508 let _c = commit_lock.lock();
5509 let epoch = self.epoch.bump_assigned();
5510 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5511 let mut next_catalog = self.catalog.read().clone();
5512 let before = next_catalog.procedures.len();
5513 next_catalog.procedures.retain(|p| p.procedure.name != name);
5514 if next_catalog.procedures.len() == before {
5515 return Err(MongrelError::NotFound(format!(
5516 "procedure {name:?} not found"
5517 )));
5518 }
5519 next_catalog.db_epoch = epoch.0;
5520 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5521 Ok(epoch)
5522 }
5523
5524 pub fn users(&self) -> Vec<crate::auth::UserEntry> {
5529 self.catalog.read().users.clone()
5530 }
5531
5532 pub fn user_identity(&self, username: &str) -> Option<(u64, u64)> {
5535 self.catalog
5536 .read()
5537 .users
5538 .iter()
5539 .find(|user| user.username == username)
5540 .map(|user| (user.id, user.created_epoch))
5541 }
5542
5543 pub fn security_version(&self) -> u64 {
5546 self.catalog.read().security_version
5547 }
5548
5549 pub fn roles(&self) -> Vec<crate::auth::RoleEntry> {
5551 self.catalog.read().roles.clone()
5552 }
5553
5554 pub fn create_user(&self, username: &str, password: &str) -> Result<crate::auth::UserEntry> {
5556 self.require(&crate::auth::Permission::Admin)?;
5557 let hash = crate::auth::hash_password(password).map_err(MongrelError::Other)?;
5558 self.create_user_with_password_hash(username, hash)
5559 }
5560
5561 pub fn create_user_with_password_hash(
5563 &self,
5564 username: &str,
5565 hash: String,
5566 ) -> Result<crate::auth::UserEntry> {
5567 self.create_user_with_password_hash_inner(username, hash, None)
5568 }
5569
5570 pub fn create_user_with_password_hash_controlled<F>(
5571 &self,
5572 username: &str,
5573 hash: String,
5574 mut before_publish: F,
5575 ) -> Result<crate::auth::UserEntry>
5576 where
5577 F: FnMut() -> Result<()>,
5578 {
5579 self.create_user_with_password_hash_inner(username, hash, Some(&mut before_publish))
5580 }
5581
5582 fn create_user_with_password_hash_inner(
5583 &self,
5584 username: &str,
5585 hash: String,
5586 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5587 ) -> Result<crate::auth::UserEntry> {
5588 self.require(&crate::auth::Permission::Admin)?;
5589 let _ddl = self.ddl_lock.lock();
5590 let _security_write = self.security_write()?;
5591 self.require(&crate::auth::Permission::Admin)?;
5592 let _commit = self.commit_lock.lock();
5593 let epoch = self.epoch.bump_assigned();
5594 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5595 let mut next_catalog = self.catalog.read().clone();
5596 if next_catalog.users.iter().any(|u| u.username == username) {
5597 return Err(MongrelError::InvalidArgument(format!(
5598 "user {username:?} already exists"
5599 )));
5600 }
5601 next_catalog.next_user_id = next_catalog.next_user_id.max(1);
5602 let id = next_catalog.next_user_id;
5603 next_catalog.next_user_id = id
5604 .checked_add(1)
5605 .ok_or_else(|| MongrelError::Full("user-id namespace exhausted".into()))?;
5606 let entry = crate::auth::UserEntry {
5607 id,
5608 username: username.into(),
5609 password_hash: hash,
5610 roles: Vec::new(),
5611 is_admin: false,
5612 created_epoch: epoch.0,
5613 };
5614 next_catalog.users.push(entry.clone());
5615 advance_security_version(&mut next_catalog)?;
5616 next_catalog.db_epoch = epoch.0;
5617 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5618 Ok(entry)
5619 }
5620
5621 pub fn drop_user(&self, username: &str) -> Result<()> {
5623 self.drop_user_with_epoch(username).map(|_| ())
5624 }
5625
5626 pub fn drop_user_with_epoch(&self, username: &str) -> Result<Epoch> {
5627 self.drop_user_with_epoch_inner(username, None)
5628 }
5629
5630 pub fn drop_user_with_epoch_controlled<F>(
5631 &self,
5632 username: &str,
5633 mut before_publish: F,
5634 ) -> Result<Epoch>
5635 where
5636 F: FnMut() -> Result<()>,
5637 {
5638 self.drop_user_with_epoch_inner(username, Some(&mut before_publish))
5639 }
5640
5641 fn drop_user_with_epoch_inner(
5642 &self,
5643 username: &str,
5644 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5645 ) -> Result<Epoch> {
5646 self.require(&crate::auth::Permission::Admin)?;
5647 let _ddl = self.ddl_lock.lock();
5648 let _security_write = self.security_write()?;
5649 self.require(&crate::auth::Permission::Admin)?;
5650 let _commit = self.commit_lock.lock();
5651 let epoch = self.epoch.bump_assigned();
5652 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5653 let mut next_catalog = self.catalog.read().clone();
5654 let before = next_catalog.users.len();
5655 next_catalog.users.retain(|u| u.username != username);
5656 if next_catalog.users.len() == before {
5657 return Err(MongrelError::NotFound(format!(
5658 "user {username:?} not found"
5659 )));
5660 }
5661 advance_security_version(&mut next_catalog)?;
5662 next_catalog.db_epoch = epoch.0;
5663 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5664 Ok(epoch)
5665 }
5666
5667 pub fn alter_user_password(&self, username: &str, new_password: &str) -> Result<()> {
5669 self.alter_user_password_with_epoch(username, new_password)
5670 .map(|_| ())
5671 }
5672
5673 pub fn alter_user_password_with_epoch(
5674 &self,
5675 username: &str,
5676 new_password: &str,
5677 ) -> Result<Epoch> {
5678 self.require(&crate::auth::Permission::Admin)?;
5679 let hash = crate::auth::hash_password(new_password).map_err(MongrelError::Other)?;
5680 self.alter_user_password_hash_with_epoch(username, hash)
5681 }
5682
5683 pub fn alter_user_password_hash_with_epoch(
5684 &self,
5685 username: &str,
5686 hash: String,
5687 ) -> Result<Epoch> {
5688 self.alter_user_password_hash_with_epoch_inner(username, hash, None)
5689 }
5690
5691 pub fn alter_user_password_hash_with_epoch_controlled<F>(
5692 &self,
5693 username: &str,
5694 hash: String,
5695 mut before_publish: F,
5696 ) -> Result<Epoch>
5697 where
5698 F: FnMut() -> Result<()>,
5699 {
5700 self.alter_user_password_hash_with_epoch_inner(username, hash, Some(&mut before_publish))
5701 }
5702
5703 fn alter_user_password_hash_with_epoch_inner(
5704 &self,
5705 username: &str,
5706 hash: String,
5707 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5708 ) -> Result<Epoch> {
5709 self.require(&crate::auth::Permission::Admin)?;
5710 let _ddl = self.ddl_lock.lock();
5711 let _security_write = self.security_write()?;
5712 self.require(&crate::auth::Permission::Admin)?;
5713 let _commit = self.commit_lock.lock();
5714 let epoch = self.epoch.bump_assigned();
5715 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5716 let mut next_catalog = self.catalog.read().clone();
5717 let user = next_catalog
5718 .users
5719 .iter_mut()
5720 .find(|u| u.username == username)
5721 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5722 user.password_hash = hash;
5723 advance_security_version(&mut next_catalog)?;
5724 next_catalog.db_epoch = epoch.0;
5725 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5726 Ok(epoch)
5727 }
5728
5729 pub fn verify_user(
5732 &self,
5733 username: &str,
5734 password: &str,
5735 ) -> Result<Option<crate::auth::UserEntry>> {
5736 let cat = self.catalog.read();
5737 let Some(user) = cat.users.iter().find(|u| u.username == username) else {
5738 return Ok(None);
5739 };
5740 if user.password_hash.is_empty() {
5741 return Ok(None);
5742 }
5743 let ok = crate::auth::verify_password(password, &user.password_hash)
5744 .map_err(MongrelError::Other)?;
5745 if ok {
5746 Ok(Some(user.clone()))
5747 } else {
5748 Ok(None)
5749 }
5750 }
5751
5752 pub fn authenticate_principal(
5756 &self,
5757 username: &str,
5758 password: &str,
5759 ) -> Result<Option<crate::auth::Principal>> {
5760 self.authenticate_principal_inner(username, password, || {})
5761 }
5762
5763 fn authenticate_principal_inner<F>(
5764 &self,
5765 username: &str,
5766 password: &str,
5767 after_verify: F,
5768 ) -> Result<Option<crate::auth::Principal>>
5769 where
5770 F: FnOnce(),
5771 {
5772 let catalog = self.catalog.read();
5773 let Some(user) = catalog.users.iter().find(|user| user.username == username) else {
5774 return Ok(None);
5775 };
5776 if user.password_hash.is_empty()
5777 || !crate::auth::verify_password(password, &user.password_hash)
5778 .map_err(MongrelError::Other)?
5779 {
5780 return Ok(None);
5781 }
5782 after_verify();
5783 Ok(Self::resolve_user_principal_from_catalog(&catalog, user))
5784 }
5785
5786 pub fn set_user_admin(&self, username: &str, is_admin: bool) -> Result<()> {
5788 self.set_user_admin_with_epoch(username, is_admin)
5789 .map(|_| ())
5790 }
5791
5792 pub fn set_user_admin_with_epoch(
5793 &self,
5794 username: &str,
5795 is_admin: bool,
5796 ) -> Result<Option<Epoch>> {
5797 self.set_user_admin_with_epoch_inner(username, is_admin, None)
5798 }
5799
5800 pub fn set_user_admin_with_epoch_controlled<F>(
5801 &self,
5802 username: &str,
5803 is_admin: bool,
5804 mut before_publish: F,
5805 ) -> Result<Option<Epoch>>
5806 where
5807 F: FnMut() -> Result<()>,
5808 {
5809 self.set_user_admin_with_epoch_inner(username, is_admin, Some(&mut before_publish))
5810 }
5811
5812 fn set_user_admin_with_epoch_inner(
5813 &self,
5814 username: &str,
5815 is_admin: bool,
5816 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5817 ) -> Result<Option<Epoch>> {
5818 self.require(&crate::auth::Permission::Admin)?;
5819 let _ddl = self.ddl_lock.lock();
5820 let _security_write = self.security_write()?;
5821 self.require(&crate::auth::Permission::Admin)?;
5822 let _commit = self.commit_lock.lock();
5823 let mut next_catalog = self.catalog.read().clone();
5824 let user = next_catalog
5825 .users
5826 .iter_mut()
5827 .find(|u| u.username == username)
5828 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5829 if user.is_admin == is_admin {
5830 return Ok(None);
5831 }
5832 user.is_admin = is_admin;
5833 let epoch = self.epoch.bump_assigned();
5834 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5835 advance_security_version(&mut next_catalog)?;
5836 next_catalog.db_epoch = epoch.0;
5837 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5838 Ok(Some(epoch))
5839 }
5840
5841 pub fn create_role(&self, name: &str) -> Result<crate::auth::RoleEntry> {
5843 self.create_role_inner(name, None)
5844 }
5845
5846 pub fn create_role_controlled<F>(
5847 &self,
5848 name: &str,
5849 mut before_publish: F,
5850 ) -> Result<crate::auth::RoleEntry>
5851 where
5852 F: FnMut() -> Result<()>,
5853 {
5854 self.create_role_inner(name, Some(&mut before_publish))
5855 }
5856
5857 fn create_role_inner(
5858 &self,
5859 name: &str,
5860 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5861 ) -> Result<crate::auth::RoleEntry> {
5862 self.require(&crate::auth::Permission::Admin)?;
5863 let _ddl = self.ddl_lock.lock();
5864 let _security_write = self.security_write()?;
5865 self.require(&crate::auth::Permission::Admin)?;
5866 let _commit = self.commit_lock.lock();
5867 let epoch = self.epoch.bump_assigned();
5868 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5869 let mut next_catalog = self.catalog.read().clone();
5870 if next_catalog.roles.iter().any(|r| r.name == name) {
5871 return Err(MongrelError::InvalidArgument(format!(
5872 "role {name:?} already exists"
5873 )));
5874 }
5875 let entry = crate::auth::RoleEntry {
5876 name: name.into(),
5877 permissions: Vec::new(),
5878 created_epoch: epoch.0,
5879 };
5880 next_catalog.roles.push(entry.clone());
5881 advance_security_version(&mut next_catalog)?;
5882 next_catalog.db_epoch = epoch.0;
5883 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5884 Ok(entry)
5885 }
5886
5887 pub fn drop_role(&self, name: &str) -> Result<()> {
5889 self.drop_role_with_epoch(name).map(|_| ())
5890 }
5891
5892 pub fn drop_role_with_epoch(&self, name: &str) -> Result<Epoch> {
5893 self.drop_role_with_epoch_inner(name, None)
5894 }
5895
5896 pub fn drop_role_with_epoch_controlled<F>(
5897 &self,
5898 name: &str,
5899 mut before_publish: F,
5900 ) -> Result<Epoch>
5901 where
5902 F: FnMut() -> Result<()>,
5903 {
5904 self.drop_role_with_epoch_inner(name, Some(&mut before_publish))
5905 }
5906
5907 fn drop_role_with_epoch_inner(
5908 &self,
5909 name: &str,
5910 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5911 ) -> Result<Epoch> {
5912 self.require(&crate::auth::Permission::Admin)?;
5913 let _ddl = self.ddl_lock.lock();
5914 let _security_write = self.security_write()?;
5915 self.require(&crate::auth::Permission::Admin)?;
5916 let _commit = self.commit_lock.lock();
5917 let epoch = self.epoch.bump_assigned();
5918 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5919 let mut next_catalog = self.catalog.read().clone();
5920 let before = next_catalog.roles.len();
5921 next_catalog.roles.retain(|r| r.name != name);
5922 if next_catalog.roles.len() == before {
5923 return Err(MongrelError::NotFound(format!("role {name:?} not found")));
5924 }
5925 for user in &mut next_catalog.users {
5926 user.roles.retain(|r| r != name);
5927 }
5928 advance_security_version(&mut next_catalog)?;
5929 next_catalog.db_epoch = epoch.0;
5930 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5931 Ok(epoch)
5932 }
5933
5934 pub fn grant_role(&self, username: &str, role_name: &str) -> Result<()> {
5936 self.grant_role_with_epoch(username, role_name).map(|_| ())
5937 }
5938
5939 pub fn grant_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
5940 self.grant_role_with_epoch_inner(username, role_name, None)
5941 }
5942
5943 pub fn grant_role_with_epoch_controlled<F>(
5944 &self,
5945 username: &str,
5946 role_name: &str,
5947 mut before_publish: F,
5948 ) -> Result<Option<Epoch>>
5949 where
5950 F: FnMut() -> Result<()>,
5951 {
5952 self.grant_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
5953 }
5954
5955 fn grant_role_with_epoch_inner(
5956 &self,
5957 username: &str,
5958 role_name: &str,
5959 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
5960 ) -> Result<Option<Epoch>> {
5961 self.require(&crate::auth::Permission::Admin)?;
5962 let _ddl = self.ddl_lock.lock();
5963 let _security_write = self.security_write()?;
5964 self.require(&crate::auth::Permission::Admin)?;
5965 let _commit = self.commit_lock.lock();
5966 let mut next_catalog = self.catalog.read().clone();
5967 if !next_catalog.roles.iter().any(|r| r.name == role_name) {
5968 return Err(MongrelError::NotFound(format!(
5969 "role {role_name:?} not found"
5970 )));
5971 }
5972 let user = next_catalog
5973 .users
5974 .iter_mut()
5975 .find(|u| u.username == username)
5976 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
5977 if user.roles.iter().any(|role| role == role_name) {
5978 return Ok(None);
5979 }
5980 user.roles.push(role_name.into());
5981 let epoch = self.epoch.bump_assigned();
5982 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
5983 advance_security_version(&mut next_catalog)?;
5984 next_catalog.db_epoch = epoch.0;
5985 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
5986 Ok(Some(epoch))
5987 }
5988
5989 pub fn revoke_role(&self, username: &str, role_name: &str) -> Result<()> {
5991 self.revoke_role_with_epoch(username, role_name).map(|_| ())
5992 }
5993
5994 pub fn revoke_role_with_epoch(&self, username: &str, role_name: &str) -> Result<Option<Epoch>> {
5995 self.revoke_role_with_epoch_inner(username, role_name, None)
5996 }
5997
5998 pub fn revoke_role_with_epoch_controlled<F>(
5999 &self,
6000 username: &str,
6001 role_name: &str,
6002 mut before_publish: F,
6003 ) -> Result<Option<Epoch>>
6004 where
6005 F: FnMut() -> Result<()>,
6006 {
6007 self.revoke_role_with_epoch_inner(username, role_name, Some(&mut before_publish))
6008 }
6009
6010 fn revoke_role_with_epoch_inner(
6011 &self,
6012 username: &str,
6013 role_name: &str,
6014 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6015 ) -> Result<Option<Epoch>> {
6016 self.require(&crate::auth::Permission::Admin)?;
6017 let _ddl = self.ddl_lock.lock();
6018 let _security_write = self.security_write()?;
6019 self.require(&crate::auth::Permission::Admin)?;
6020 let _commit = self.commit_lock.lock();
6021 let mut next_catalog = self.catalog.read().clone();
6022 let user = next_catalog
6023 .users
6024 .iter_mut()
6025 .find(|u| u.username == username)
6026 .ok_or_else(|| MongrelError::NotFound(format!("user {username:?} not found")))?;
6027 let before = user.roles.len();
6028 user.roles.retain(|r| r != role_name);
6029 if user.roles.len() == before {
6030 return Ok(None);
6031 }
6032 let epoch = self.epoch.bump_assigned();
6033 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6034 advance_security_version(&mut next_catalog)?;
6035 next_catalog.db_epoch = epoch.0;
6036 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6037 Ok(Some(epoch))
6038 }
6039
6040 pub fn grant_permission(
6042 &self,
6043 role_name: &str,
6044 permission: crate::auth::Permission,
6045 ) -> Result<()> {
6046 self.grant_permission_with_epoch(role_name, permission)
6047 .map(|_| ())
6048 }
6049
6050 pub fn grant_permission_with_epoch(
6051 &self,
6052 role_name: &str,
6053 permission: crate::auth::Permission,
6054 ) -> Result<Option<Epoch>> {
6055 self.grant_permission_with_epoch_inner(role_name, permission, None)
6056 }
6057
6058 pub fn grant_permission_with_epoch_controlled<F>(
6059 &self,
6060 role_name: &str,
6061 permission: crate::auth::Permission,
6062 mut before_publish: F,
6063 ) -> Result<Option<Epoch>>
6064 where
6065 F: FnMut() -> Result<()>,
6066 {
6067 self.grant_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
6068 }
6069
6070 fn grant_permission_with_epoch_inner(
6071 &self,
6072 role_name: &str,
6073 permission: crate::auth::Permission,
6074 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6075 ) -> Result<Option<Epoch>> {
6076 self.require(&crate::auth::Permission::Admin)?;
6077 let _ddl = self.ddl_lock.lock();
6078 let _security_write = self.security_write()?;
6079 self.require(&crate::auth::Permission::Admin)?;
6080 let _commit = self.commit_lock.lock();
6081 let mut next_catalog = self.catalog.read().clone();
6082 let role = next_catalog
6083 .roles
6084 .iter_mut()
6085 .find(|r| r.name == role_name)
6086 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
6087 let before = role.permissions.clone();
6088 merge_permission(&mut role.permissions, permission);
6089 if role.permissions == before {
6090 return Ok(None);
6091 }
6092 let epoch = self.epoch.bump_assigned();
6093 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6094 advance_security_version(&mut next_catalog)?;
6095 next_catalog.db_epoch = epoch.0;
6096 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6097 Ok(Some(epoch))
6098 }
6099
6100 pub fn revoke_permission(
6102 &self,
6103 role_name: &str,
6104 permission: crate::auth::Permission,
6105 ) -> Result<()> {
6106 self.revoke_permission_with_epoch(role_name, permission)
6107 .map(|_| ())
6108 }
6109
6110 pub fn revoke_permission_with_epoch(
6111 &self,
6112 role_name: &str,
6113 permission: crate::auth::Permission,
6114 ) -> Result<Option<Epoch>> {
6115 self.revoke_permission_with_epoch_inner(role_name, permission, None)
6116 }
6117
6118 pub fn revoke_permission_with_epoch_controlled<F>(
6119 &self,
6120 role_name: &str,
6121 permission: crate::auth::Permission,
6122 mut before_publish: F,
6123 ) -> Result<Option<Epoch>>
6124 where
6125 F: FnMut() -> Result<()>,
6126 {
6127 self.revoke_permission_with_epoch_inner(role_name, permission, Some(&mut before_publish))
6128 }
6129
6130 fn revoke_permission_with_epoch_inner(
6131 &self,
6132 role_name: &str,
6133 permission: crate::auth::Permission,
6134 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6135 ) -> Result<Option<Epoch>> {
6136 self.require(&crate::auth::Permission::Admin)?;
6137 let _ddl = self.ddl_lock.lock();
6138 let _security_write = self.security_write()?;
6139 self.require(&crate::auth::Permission::Admin)?;
6140 let _commit = self.commit_lock.lock();
6141 let mut next_catalog = self.catalog.read().clone();
6142 let role = next_catalog
6143 .roles
6144 .iter_mut()
6145 .find(|r| r.name == role_name)
6146 .ok_or_else(|| MongrelError::NotFound(format!("role {role_name:?} not found")))?;
6147 let before = role.permissions.clone();
6148 revoke_permission_from(&mut role.permissions, &permission);
6149 if role.permissions == before {
6150 return Ok(None);
6151 }
6152 let epoch = self.epoch.bump_assigned();
6153 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6154 advance_security_version(&mut next_catalog)?;
6155 next_catalog.db_epoch = epoch.0;
6156 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6157 Ok(Some(epoch))
6158 }
6159
6160 pub fn resolve_principal(&self, username: &str) -> Option<crate::auth::Principal> {
6163 let cat = self.catalog.read();
6164 Self::resolve_principal_from_catalog(&cat, username)
6165 }
6166
6167 pub fn resolve_current_principal(
6170 &self,
6171 principal: &crate::auth::Principal,
6172 ) -> Option<crate::auth::Principal> {
6173 Self::resolve_bound_principal_from_catalog(&self.catalog.read(), principal)
6174 }
6175
6176 fn resolve_principal_from_catalog(
6181 cat: &Catalog,
6182 username: &str,
6183 ) -> Option<crate::auth::Principal> {
6184 let user = cat.users.iter().find(|u| u.username == username)?;
6185 Self::resolve_user_principal_from_catalog(cat, user)
6186 }
6187
6188 fn resolve_bound_principal_from_catalog(
6189 cat: &Catalog,
6190 principal: &crate::auth::Principal,
6191 ) -> Option<crate::auth::Principal> {
6192 let user = cat.users.iter().find(|user| {
6193 user.id == principal.user_id
6194 && user.created_epoch == principal.created_epoch
6195 && user.username == principal.username
6196 })?;
6197 Self::resolve_user_principal_from_catalog(cat, user)
6198 }
6199
6200 fn resolve_user_principal_from_catalog(
6201 cat: &Catalog,
6202 user: &crate::auth::UserEntry,
6203 ) -> Option<crate::auth::Principal> {
6204 let mut permissions = Vec::new();
6205 for role_name in &user.roles {
6206 if let Some(role) = cat.roles.iter().find(|r| &r.name == role_name) {
6207 permissions.extend(role.permissions.iter().cloned());
6208 }
6209 }
6210 Some(crate::auth::Principal {
6211 user_id: user.id,
6212 created_epoch: user.created_epoch,
6213 username: user.username.clone(),
6214 is_admin: user.is_admin,
6215 roles: user.roles.clone(),
6216 permissions,
6217 })
6218 }
6219
6220 pub fn check_permission(&self, username: &str, permission: &crate::auth::Permission) -> bool {
6222 match self.resolve_principal(username) {
6223 Some(p) => p.has_permission(permission),
6224 None => false,
6225 }
6226 }
6227
6228 pub fn require_auth_enabled(&self) -> bool {
6232 self.catalog.read().require_auth
6233 }
6234
6235 pub fn principal(&self) -> Option<crate::auth::Principal> {
6239 self.principal.read().clone()
6240 }
6241
6242 fn table_auth_checker(&self) -> Option<Arc<dyn crate::auth_state::TableAuthChecker>> {
6248 Some(Arc::new(crate::auth_state::DefaultTableAuthChecker::new(
6249 self.auth_state.clone(),
6250 )))
6251 }
6252
6253 pub fn refresh_principal(&self) -> Result<()> {
6267 let previous = match self.principal.read().clone() {
6268 Some(principal) => principal,
6269 None => return Ok(()),
6270 };
6271 let observed_version = self.security_coordinator.version.load(Ordering::Acquire);
6272 self.refresh_security_catalog_if_stale(observed_version)?;
6273 let cat = self.catalog.read();
6274 match Self::resolve_bound_principal_from_catalog(&cat, &previous) {
6275 Some(p) => {
6276 *self.principal.write() = Some(p.clone());
6277 self.auth_state.set_principal(Some(p));
6281 Ok(())
6282 }
6283 None => Err(MongrelError::InvalidCredentials {
6284 username: previous.username,
6285 }),
6286 }
6287 }
6288
6289 pub fn security_catalog_disk_read_count(&self) -> u64 {
6292 self.security_catalog_disk_reads.load(Ordering::Relaxed)
6293 }
6294
6295 pub fn enable_auth(&self, admin_username: &str, admin_password: &str) -> Result<()> {
6307 let password_hash =
6308 crate::auth::hash_password(admin_password).map_err(MongrelError::Other)?;
6309 let _ddl = self.ddl_lock.lock();
6310 let _security_write = self.security_write()?;
6311 let _commit = self.commit_lock.lock();
6312 let epoch = self.epoch.bump_assigned();
6313 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6314 let mut next_catalog = self.catalog.read().clone();
6315 if next_catalog.require_auth {
6316 return Err(MongrelError::InvalidArgument(
6317 "database already has require_auth enabled".into(),
6318 ));
6319 }
6320 if next_catalog
6321 .users
6322 .iter()
6323 .any(|u| u.username == admin_username)
6324 {
6325 return Err(MongrelError::InvalidArgument(format!(
6326 "user {admin_username:?} already exists"
6327 )));
6328 }
6329 next_catalog.next_user_id = next_catalog.next_user_id.max(1);
6330 let id = next_catalog.next_user_id;
6331 next_catalog.next_user_id = id
6332 .checked_add(1)
6333 .ok_or_else(|| MongrelError::Full("user-id namespace exhausted".into()))?;
6334 next_catalog.users.push(crate::auth::UserEntry {
6335 id,
6336 username: admin_username.to_string(),
6337 password_hash,
6338 roles: Vec::new(),
6339 is_admin: true,
6340 created_epoch: epoch.0,
6341 });
6342 next_catalog.require_auth = true;
6343 advance_security_version(&mut next_catalog)?;
6344 next_catalog.db_epoch = epoch.0;
6345 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
6346 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
6350 let principal = crate::auth::Principal {
6351 user_id: id,
6352 created_epoch: epoch.0,
6353 username: admin_username.to_string(),
6354 is_admin: true,
6355 roles: Vec::new(),
6356 permissions: Vec::new(),
6357 };
6358 *self.principal.write() = Some(principal.clone());
6359 self.auth_state.set_principal(Some(principal));
6360 }
6361 publish
6362 }
6363
6364 pub fn disable_auth(&self) -> Result<()> {
6381 let _ddl = self.ddl_lock.lock();
6382 let _security_write = self.security_write()?;
6383 let _commit = self.commit_lock.lock();
6384 let epoch = self.epoch.bump_assigned();
6385 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6386 let mut next_catalog = self.catalog.read().clone();
6387 if !next_catalog.require_auth {
6388 return Err(MongrelError::InvalidArgument(
6389 "database does not have require_auth enabled".into(),
6390 ));
6391 }
6392 next_catalog.require_auth = false;
6393 advance_security_version(&mut next_catalog)?;
6394 next_catalog.db_epoch = epoch.0;
6395 let publish = self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, None);
6396 if publish.is_ok() || matches!(&publish, Err(MongrelError::CommitOutcomeUnknown { .. })) {
6398 *self.principal.write() = None;
6399 }
6400 publish
6401 }
6402
6403 pub fn require(&self, perm: &crate::auth::Permission) -> Result<()> {
6410 self.ensure_owner_process()?;
6411 if self.read_only && !matches!(perm, crate::auth::Permission::Select { .. }) {
6412 return Err(MongrelError::ReadOnlyReplica);
6413 }
6414 if self.principal.read().is_some() {
6415 self.refresh_principal().map_err(|error| match error {
6416 MongrelError::InvalidCredentials { .. } => MongrelError::AuthRequired,
6417 error => error,
6418 })?;
6419 }
6420 if !self.catalog.read().require_auth {
6421 return Ok(());
6422 }
6423 let guard = self.principal.read();
6424 let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
6425 if p.has_permission(perm) {
6426 Ok(())
6427 } else {
6428 Err(MongrelError::PermissionDenied {
6429 required: perm.clone(),
6430 principal: p.username.clone(),
6431 })
6432 }
6433 }
6434
6435 pub fn require_table(
6440 &self,
6441 table: &str,
6442 perm: crate::auth_state::RequiredPermission,
6443 ) -> Result<()> {
6444 self.require(&perm.into_permission(table))
6445 }
6446
6447 pub fn triggers(&self) -> Vec<StoredTrigger> {
6448 self.catalog
6449 .read()
6450 .triggers
6451 .iter()
6452 .map(|t| t.trigger.clone())
6453 .collect()
6454 }
6455
6456 pub fn trigger(&self, name: &str) -> Option<StoredTrigger> {
6457 self.catalog
6458 .read()
6459 .triggers
6460 .iter()
6461 .find(|t| t.trigger.name == name)
6462 .map(|t| t.trigger.clone())
6463 }
6464
6465 pub fn create_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
6466 self.create_trigger_inner(trigger, None, None)
6467 }
6468
6469 pub fn create_trigger_controlled<F>(
6470 &self,
6471 trigger: StoredTrigger,
6472 mut before_publish: F,
6473 ) -> Result<StoredTrigger>
6474 where
6475 F: FnMut() -> Result<()>,
6476 {
6477 self.create_trigger_inner(trigger, None, Some(&mut before_publish))
6478 }
6479
6480 pub fn create_trigger_as_controlled<F>(
6481 &self,
6482 trigger: StoredTrigger,
6483 principal: Option<&crate::auth::Principal>,
6484 mut before_publish: F,
6485 ) -> Result<StoredTrigger>
6486 where
6487 F: FnMut() -> Result<()>,
6488 {
6489 self.create_trigger_inner(trigger, principal, Some(&mut before_publish))
6490 }
6491
6492 fn create_trigger_inner(
6493 &self,
6494 mut trigger: StoredTrigger,
6495 principal: Option<&crate::auth::Principal>,
6496 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6497 ) -> Result<StoredTrigger> {
6498 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6499 let _g = self.ddl_lock.lock();
6500 let _security_write = self.security_write()?;
6501 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6502 trigger.validate()?;
6503 self.validate_trigger_references(&trigger)
6504 .map_err(trigger_validation_error)?;
6505 {
6506 let cat = self.catalog.read();
6507 if cat.triggers.iter().any(|t| t.trigger.name == trigger.name) {
6508 return Err(MongrelError::TriggerValidation(format!(
6509 "trigger {:?} already exists",
6510 trigger.name
6511 )));
6512 }
6513 }
6514 let commit_lock = Arc::clone(&self.commit_lock);
6515 let _c = commit_lock.lock();
6516 let epoch = self.epoch.bump_assigned();
6517 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6518 trigger.created_epoch = epoch.0;
6519 trigger.updated_epoch = epoch.0;
6520 let mut next_catalog = self.catalog.read().clone();
6521 next_catalog
6522 .triggers
6523 .push(TriggerEntry::from(trigger.clone()));
6524 next_catalog.db_epoch = epoch.0;
6525 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6526 Ok(trigger)
6527 }
6528
6529 pub fn create_or_replace_trigger(&self, trigger: StoredTrigger) -> Result<StoredTrigger> {
6530 self.create_or_replace_trigger_inner(trigger, None, None)
6531 }
6532
6533 pub fn create_or_replace_trigger_controlled<F>(
6534 &self,
6535 trigger: StoredTrigger,
6536 mut before_publish: F,
6537 ) -> Result<StoredTrigger>
6538 where
6539 F: FnMut() -> Result<()>,
6540 {
6541 self.create_or_replace_trigger_inner(trigger, None, Some(&mut before_publish))
6542 }
6543
6544 pub fn create_or_replace_trigger_as_controlled<F>(
6545 &self,
6546 trigger: StoredTrigger,
6547 principal: Option<&crate::auth::Principal>,
6548 mut before_publish: F,
6549 ) -> Result<StoredTrigger>
6550 where
6551 F: FnMut() -> Result<()>,
6552 {
6553 self.create_or_replace_trigger_inner(trigger, principal, Some(&mut before_publish))
6554 }
6555
6556 fn create_or_replace_trigger_inner(
6557 &self,
6558 trigger: StoredTrigger,
6559 principal: Option<&crate::auth::Principal>,
6560 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6561 ) -> Result<StoredTrigger> {
6562 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6563 let _g = self.ddl_lock.lock();
6564 let _security_write = self.security_write()?;
6565 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6566 trigger.validate()?;
6567 self.validate_trigger_references(&trigger)
6568 .map_err(trigger_validation_error)?;
6569 let commit_lock = Arc::clone(&self.commit_lock);
6570 let _c = commit_lock.lock();
6571 let epoch = self.epoch.bump_assigned();
6572 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6573 let mut next_catalog = self.catalog.read().clone();
6574 let replaced = {
6575 let next = match next_catalog
6576 .triggers
6577 .iter()
6578 .position(|t| t.trigger.name == trigger.name)
6579 {
6580 Some(idx) => {
6581 let next = next_catalog.triggers[idx]
6582 .trigger
6583 .replaced(trigger.clone(), epoch.0)?;
6584 next_catalog.triggers[idx] = TriggerEntry::from(next.clone());
6585 next
6586 }
6587 None => {
6588 let mut next = trigger;
6589 next.created_epoch = epoch.0;
6590 next.updated_epoch = epoch.0;
6591 next_catalog.triggers.push(TriggerEntry::from(next.clone()));
6592 next
6593 }
6594 };
6595 next_catalog.db_epoch = epoch.0;
6596 next
6597 };
6598 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6599 Ok(replaced)
6600 }
6601
6602 pub fn drop_trigger(&self, name: &str) -> Result<()> {
6603 self.drop_trigger_with_epoch(name).map(|_| ())
6604 }
6605
6606 pub fn drop_trigger_with_epoch(&self, name: &str) -> Result<Epoch> {
6608 self.drop_triggers_with_epoch(&[name.to_string()])
6609 }
6610
6611 pub fn drop_trigger_with_epoch_controlled<F>(
6612 &self,
6613 name: &str,
6614 before_publish: F,
6615 ) -> Result<Epoch>
6616 where
6617 F: FnMut() -> Result<()>,
6618 {
6619 self.drop_triggers_with_epoch_controlled(&[name.to_string()], before_publish)
6620 }
6621
6622 pub fn drop_triggers_with_epoch(&self, names: &[String]) -> Result<Epoch> {
6624 self.drop_triggers_with_epoch_inner(names, None, None)
6625 }
6626
6627 pub fn drop_triggers_with_epoch_controlled<F>(
6628 &self,
6629 names: &[String],
6630 mut before_publish: F,
6631 ) -> Result<Epoch>
6632 where
6633 F: FnMut() -> Result<()>,
6634 {
6635 self.drop_triggers_with_epoch_inner(names, None, Some(&mut before_publish))
6636 }
6637
6638 pub fn drop_triggers_with_epoch_as_controlled<F>(
6639 &self,
6640 names: &[String],
6641 principal: Option<&crate::auth::Principal>,
6642 mut before_publish: F,
6643 ) -> Result<Epoch>
6644 where
6645 F: FnMut() -> Result<()>,
6646 {
6647 self.drop_triggers_with_epoch_inner(names, principal, Some(&mut before_publish))
6648 }
6649
6650 fn drop_triggers_with_epoch_inner(
6651 &self,
6652 names: &[String],
6653 principal: Option<&crate::auth::Principal>,
6654 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6655 ) -> Result<Epoch> {
6656 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6657 if names.is_empty() {
6658 return Err(MongrelError::InvalidArgument(
6659 "at least one trigger name is required".into(),
6660 ));
6661 }
6662 let _g = self.ddl_lock.lock();
6663 let _security_write = self.security_write()?;
6664 self.require_for(principal, &crate::auth::Permission::Ddl)?;
6665 {
6666 let cat = self.catalog.read();
6667 for name in names {
6668 if !cat.triggers.iter().any(|t| t.trigger.name == *name) {
6669 return Err(MongrelError::NotFound(format!(
6670 "trigger {name:?} not found"
6671 )));
6672 }
6673 }
6674 }
6675 let commit_lock = Arc::clone(&self.commit_lock);
6676 let _c = commit_lock.lock();
6677 let epoch = self.epoch.bump_assigned();
6678 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6679 let mut next_catalog = self.catalog.read().clone();
6680 next_catalog
6681 .triggers
6682 .retain(|trigger| !names.contains(&trigger.trigger.name));
6683 next_catalog.db_epoch = epoch.0;
6684 self.publish_catalog_candidate(next_catalog, epoch, &mut _epoch_guard, before_publish)?;
6685 Ok(epoch)
6686 }
6687
6688 pub fn external_tables(&self) -> Vec<ExternalTableEntry> {
6689 self.catalog.read().external_tables.clone()
6690 }
6691
6692 pub fn external_table(&self, name: &str) -> Option<ExternalTableEntry> {
6693 self.catalog
6694 .read()
6695 .external_tables
6696 .iter()
6697 .find(|t| t.name == name)
6698 .cloned()
6699 }
6700
6701 pub fn create_external_table(&self, entry: ExternalTableEntry) -> Result<ExternalTableEntry> {
6702 self.create_external_table_inner(entry, None)
6703 }
6704
6705 pub fn create_external_table_controlled<F>(
6706 &self,
6707 entry: ExternalTableEntry,
6708 mut before_publish: F,
6709 ) -> Result<ExternalTableEntry>
6710 where
6711 F: FnMut() -> Result<()>,
6712 {
6713 self.create_external_table_inner(entry, Some(&mut before_publish))
6714 }
6715
6716 fn create_external_table_inner(
6717 &self,
6718 mut entry: ExternalTableEntry,
6719 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6720 ) -> Result<ExternalTableEntry> {
6721 self.require(&crate::auth::Permission::Ddl)?;
6722 let _g = self.ddl_lock.lock();
6723 let _security_write = self.security_write()?;
6724 self.require(&crate::auth::Permission::Ddl)?;
6725 entry.validate()?;
6726 {
6727 let cat = self.catalog.read();
6728 if cat.live(&entry.name).is_some()
6729 || cat.external_tables.iter().any(|t| t.name == entry.name)
6730 {
6731 return Err(MongrelError::InvalidArgument(format!(
6732 "table {:?} already exists",
6733 entry.name
6734 )));
6735 }
6736 }
6737 let commit_lock = Arc::clone(&self.commit_lock);
6738 let _c = commit_lock.lock();
6739 crate::durable_file::create_directory(&self.root.join(VTAB_DIR))?;
6743 crate::durable_file::remove_directory_all(&self.root.join(VTAB_DIR).join(&entry.name))?;
6744 let epoch = self.epoch.bump_assigned();
6745 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6746 entry.created_epoch = epoch.0;
6747 let mut next_catalog = self.catalog.read().clone();
6748 next_catalog.external_tables.push(entry.clone());
6749 next_catalog.db_epoch = epoch.0;
6750 self.publish_catalog_candidate_with_prelude(
6751 next_catalog,
6752 epoch,
6753 &mut _epoch_guard,
6754 before_publish,
6755 vec![(
6756 EXTERNAL_TABLE_ID,
6757 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
6758 name: entry.name.clone(),
6759 generation_epoch: epoch.0,
6760 }),
6761 )],
6762 )?;
6763 Ok(entry)
6764 }
6765
6766 pub fn drop_external_table(&self, name: &str) -> Result<()> {
6767 self.drop_external_table_with_epoch(name).map(|_| ())
6768 }
6769
6770 pub fn drop_external_table_with_epoch(&self, name: &str) -> Result<Epoch> {
6772 self.drop_external_table_with_epoch_inner(name, None)
6773 }
6774
6775 pub fn drop_external_table_with_epoch_controlled<F>(
6776 &self,
6777 name: &str,
6778 mut before_publish: F,
6779 ) -> Result<Epoch>
6780 where
6781 F: FnMut() -> Result<()>,
6782 {
6783 self.drop_external_table_with_epoch_inner(name, Some(&mut before_publish))
6784 }
6785
6786 fn drop_external_table_with_epoch_inner(
6787 &self,
6788 name: &str,
6789 before_publish: Option<&mut dyn FnMut() -> Result<()>>,
6790 ) -> Result<Epoch> {
6791 self.require(&crate::auth::Permission::Ddl)?;
6792 let _g = self.ddl_lock.lock();
6793 let _security_write = self.security_write()?;
6794 self.require(&crate::auth::Permission::Ddl)?;
6795 let commit_lock = Arc::clone(&self.commit_lock);
6796 let _c = commit_lock.lock();
6797 let epoch = self.epoch.bump_assigned();
6798 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
6799 let mut next_catalog = self.catalog.read().clone();
6800 let before = next_catalog.external_tables.len();
6801 next_catalog.external_tables.retain(|t| t.name != name);
6802 if next_catalog.external_tables.len() == before {
6803 return Err(MongrelError::NotFound(format!(
6804 "external table {name:?} not found"
6805 )));
6806 }
6807 next_catalog.db_epoch = epoch.0;
6808 self.publish_catalog_candidate_with_prelude(
6809 next_catalog,
6810 epoch,
6811 &mut _epoch_guard,
6812 before_publish,
6813 vec![(
6814 EXTERNAL_TABLE_ID,
6815 crate::wal::Op::Ddl(crate::wal::DdlOp::ResetExternalTableState {
6816 name: name.to_string(),
6817 generation_epoch: epoch.0,
6818 }),
6819 )],
6820 )?;
6821 let state_dir = self.root.join(VTAB_DIR).join(name);
6822 if let Err(error) = crate::durable_file::remove_directory_all(&state_dir) {
6823 return Err(MongrelError::DurableCommit {
6824 epoch: epoch.0,
6825 message: format!(
6826 "external table was dropped but connector-state cleanup failed: {error}"
6827 ),
6828 });
6829 }
6830 Ok(epoch)
6831 }
6832
6833 pub fn commit_external_table_state(&self, name: &str, state: &[u8]) -> Result<Epoch> {
6834 let txn_id = self.alloc_txn_id()?;
6835 let (principal, catalog_bound) = self.transaction_principal_snapshot();
6836 self.commit_transaction_with_external_states(
6837 txn_id,
6838 self.epoch.visible(),
6839 Vec::new(),
6840 vec![(name.to_string(), state.to_vec())],
6841 Vec::new(),
6842 principal,
6843 catalog_bound,
6844 None,
6845 )
6846 .map(|(epoch, _)| epoch)
6847 }
6848
6849 pub fn trigger_config(&self) -> TriggerConfig {
6850 use std::sync::atomic::Ordering;
6851 TriggerConfig {
6852 recursive_triggers: self.trigger_recursive.load(Ordering::Relaxed),
6853 max_depth: self.trigger_max_depth.load(Ordering::Relaxed),
6854 max_loop_iterations: self.trigger_max_loop_iterations.load(Ordering::Relaxed),
6855 }
6856 }
6857
6858 pub fn set_trigger_config(&self, config: TriggerConfig) -> Result<()> {
6859 use std::sync::atomic::Ordering;
6860 if config.max_depth == 0 {
6861 return Err(MongrelError::InvalidArgument(
6862 "trigger max_depth must be greater than 0".into(),
6863 ));
6864 }
6865 self.trigger_recursive
6866 .store(config.recursive_triggers, Ordering::Relaxed);
6867 self.trigger_max_depth
6868 .store(config.max_depth, Ordering::Relaxed);
6869 self.trigger_max_loop_iterations
6870 .store(config.max_loop_iterations, Ordering::Relaxed);
6871 Ok(())
6872 }
6873
6874 pub fn set_recursive_triggers(&self, recursive: bool) {
6875 use std::sync::atomic::Ordering;
6876 self.trigger_recursive.store(recursive, Ordering::Relaxed);
6877 }
6878
6879 pub fn subscribe_changes(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent> {
6883 self.notify.subscribe()
6884 }
6885
6886 pub fn subscribe_change_commits(&self) -> tokio::sync::broadcast::Receiver<()> {
6887 self.change_wake.subscribe()
6888 }
6889
6890 pub fn change_events_since(&self, last_event_id: Option<&str>) -> Result<CdcBatch> {
6895 let control = crate::ExecutionControl::new(None);
6896 self.change_events_since_controlled(last_event_id, &control)
6897 }
6898
6899 pub fn change_events_since_controlled(
6901 &self,
6902 last_event_id: Option<&str>,
6903 control: &crate::ExecutionControl,
6904 ) -> Result<CdcBatch> {
6905 use crate::wal::Op;
6906
6907 control.checkpoint()?;
6908 let resume = match last_event_id {
6909 Some(id) => {
6910 let (epoch, index) = id.split_once(':').ok_or_else(|| {
6911 MongrelError::InvalidArgument(format!(
6912 "invalid CDC event id {id:?}; expected <epoch>:<index>"
6913 ))
6914 })?;
6915 Some((
6916 epoch.parse::<u64>().map_err(|error| {
6917 MongrelError::InvalidArgument(format!("invalid CDC epoch: {error}"))
6918 })?,
6919 index.parse::<u32>().map_err(|error| {
6920 MongrelError::InvalidArgument(format!("invalid CDC index: {error}"))
6921 })?,
6922 ))
6923 }
6924 None => None,
6925 };
6926
6927 let mut wal = self.shared_wal.lock();
6928 wal.group_sync()?;
6929 let wal_dek = crate::encryption::wal_dek_for(self.kek.as_deref());
6930 let records = crate::wal::SharedWal::replay_with_dek_controlled(
6931 &self.root,
6932 wal_dek.as_ref(),
6933 control,
6934 CDC_MAX_WAL_RECORDS,
6935 CDC_MAX_WAL_REPLAY_BYTES,
6936 )?;
6937 drop(wal);
6938 control.checkpoint()?;
6939
6940 let mut commits: HashMap<u64, (u64, Vec<crate::wal::AddedRun>)> = HashMap::new();
6941 let mut spilled_payloads: HashMap<(u64, u64), Vec<&[u8]>> = HashMap::new();
6942 for (index, record) in records.iter().enumerate() {
6943 if index % 256 == 0 {
6944 control.checkpoint()?;
6945 }
6946 if let Op::TxnCommit { epoch, added_runs } = &record.op {
6947 commits.insert(record.txn_id, (*epoch, added_runs.clone()));
6948 }
6949 if let Op::SpilledRows { table_id, rows } = &record.op {
6950 spilled_payloads
6951 .entry((record.txn_id, *table_id))
6952 .or_default()
6953 .push(rows);
6954 }
6955 }
6956 let earliest_epoch = commits.values().map(|(epoch, _)| *epoch).min();
6957 let current_epoch = self.epoch.committed().0;
6958 let retention_floor = crate::replication::replication_wal_floor(&self.root)?;
6959 let gap = resume.is_some_and(|(epoch, _)| {
6960 retention_floor != 0 && epoch <= retention_floor && epoch <= current_epoch
6961 });
6962 if gap {
6963 return Ok(CdcBatch {
6964 events: Vec::new(),
6965 current_epoch,
6966 earliest_epoch,
6967 gap: true,
6968 });
6969 }
6970
6971 let table_names: HashMap<u64, String> = self
6972 .catalog
6973 .read()
6974 .tables
6975 .iter()
6976 .map(|entry| (entry.table_id, entry.name.clone()))
6977 .collect();
6978 let mut before_images: HashMap<(u64, u64, u64), crate::memtable::Row> = HashMap::new();
6979 let mut retained_bytes = 0_usize;
6980 for (index, record) in records.iter().enumerate() {
6981 if index % 256 == 0 {
6982 control.checkpoint()?;
6983 }
6984 if !commits.contains_key(&record.txn_id) {
6985 continue;
6986 }
6987 let Op::BeforeImage {
6988 table_id,
6989 row_id,
6990 row,
6991 } = &record.op
6992 else {
6993 continue;
6994 };
6995 if row.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
6996 return Err(MongrelError::ResourceLimitExceeded {
6997 resource: "CDC before-image bytes",
6998 requested: row.len(),
6999 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
7000 });
7001 }
7002 let before: crate::memtable::Row = bincode::deserialize(row)?;
7003 if before_images.len() >= CDC_MAX_ROWS {
7004 return Err(MongrelError::ResourceLimitExceeded {
7005 resource: "CDC before-image rows",
7006 requested: before_images.len().saturating_add(1),
7007 limit: CDC_MAX_ROWS,
7008 });
7009 }
7010 charge_cdc_bytes(
7011 &mut retained_bytes,
7012 cdc_row_storage_bytes(&before),
7013 "CDC retained bytes",
7014 )?;
7015 before_images.insert((record.txn_id, *table_id, row_id.0), before);
7016 }
7017 let mut operation_indices: HashMap<u64, u32> = HashMap::new();
7018 let mut events = Vec::new();
7019 let mut decoded_rows = before_images.len();
7020 for (record_index, record) in records.iter().enumerate() {
7021 if record_index % 256 == 0 {
7022 control.checkpoint()?;
7023 }
7024 let Some((commit_epoch, _)) = commits.get(&record.txn_id) else {
7025 continue;
7026 };
7027 let event = match &record.op {
7028 Op::Put { table_id, rows } => {
7029 if rows.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
7030 return Err(MongrelError::ResourceLimitExceeded {
7031 resource: "CDC inline row bytes",
7032 requested: rows.len(),
7033 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
7034 });
7035 }
7036 let rows: Vec<crate::memtable::Row> = bincode::deserialize(rows)?;
7037 decoded_rows = decoded_rows.saturating_add(rows.len());
7038 if decoded_rows > CDC_MAX_ROWS {
7039 return Err(MongrelError::ResourceLimitExceeded {
7040 resource: "CDC decoded rows",
7041 requested: decoded_rows,
7042 limit: CDC_MAX_ROWS,
7043 });
7044 }
7045 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(512);
7046 let mut peak_bytes = retained_bytes;
7047 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
7048 let data = serde_json::to_value(rows)
7049 .map_err(|error| MongrelError::Other(format!("CDC JSON: {error}")))?;
7050 Some((*table_id, "put", data, event_bytes))
7051 }
7052 Op::Delete { table_id, row_ids } => {
7053 let before = row_ids
7054 .iter()
7055 .filter_map(|row_id| {
7056 before_images
7057 .get(&(record.txn_id, *table_id, row_id.0))
7058 .cloned()
7059 })
7060 .collect::<Vec<_>>();
7061 let event_bytes = cdc_rows_json_bytes(&before)
7062 .saturating_add(
7063 row_ids
7064 .len()
7065 .saturating_mul(std::mem::size_of::<serde_json::Value>()),
7066 )
7067 .saturating_add(512);
7068 let mut peak_bytes = retained_bytes;
7069 charge_cdc_bytes(&mut peak_bytes, event_bytes, "CDC retained event bytes")?;
7070 Some((
7071 *table_id,
7072 "delete",
7073 serde_json::json!({
7074 "row_ids": row_ids.iter().map(|row_id| row_id.0).collect::<Vec<_>>(),
7075 "before": before,
7076 }),
7077 event_bytes,
7078 ))
7079 }
7080 Op::TruncateTable { table_id } => {
7081 Some((*table_id, "truncate", serde_json::Value::Null, 512))
7082 }
7083 _ => None,
7084 };
7085 if let Some((table_id, op, data, event_bytes)) = event {
7086 let index = operation_indices.entry(record.txn_id).or_insert(0);
7087 let event_position = (*commit_epoch, *index);
7088 *index = index.saturating_add(1);
7089 if resume.is_some_and(|position| event_position <= position) {
7090 continue;
7091 }
7092 if events.len() >= CDC_MAX_EVENTS {
7093 return Err(MongrelError::ResourceLimitExceeded {
7094 resource: "CDC events",
7095 requested: events.len().saturating_add(1),
7096 limit: CDC_MAX_EVENTS,
7097 });
7098 }
7099 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
7100 events.push(ChangeEvent {
7101 id: Some(format!("{}:{}", event_position.0, event_position.1)),
7102 channel: "changes".into(),
7103 table_id: Some(table_id),
7104 table: table_names.get(&table_id).cloned().unwrap_or_default(),
7105 op: op.into(),
7106 epoch: *commit_epoch,
7107 txn_id: Some(record.txn_id),
7108 message: None,
7109 data: Some(data),
7110 });
7111 }
7112 if let Op::TxnCommit { added_runs, .. } = &record.op {
7113 for run in added_runs {
7114 control.checkpoint()?;
7115 let index = operation_indices.entry(record.txn_id).or_insert(0);
7116 let event_position = (*commit_epoch, *index);
7117 *index = index.saturating_add(1);
7118 if resume.is_some_and(|position| event_position <= position) {
7119 continue;
7120 }
7121 let mut rows = if let Some(payloads) =
7122 spilled_payloads.get(&(record.txn_id, run.table_id))
7123 {
7124 let mut rows = Vec::new();
7125 for payload in payloads {
7126 control.checkpoint()?;
7127 if payload.len() > CDC_MAX_INLINE_PAYLOAD_BYTES {
7128 return Err(MongrelError::ResourceLimitExceeded {
7129 resource: "CDC spilled row bytes",
7130 requested: payload.len(),
7131 limit: CDC_MAX_INLINE_PAYLOAD_BYTES,
7132 });
7133 }
7134 let chunk: Vec<crate::memtable::Row> = bincode::deserialize(payload)?;
7135 if decoded_rows
7136 .saturating_add(rows.len())
7137 .saturating_add(chunk.len())
7138 > CDC_MAX_ROWS
7139 {
7140 return Err(MongrelError::ResourceLimitExceeded {
7141 resource: "CDC decoded rows",
7142 requested: decoded_rows
7143 .saturating_add(rows.len())
7144 .saturating_add(chunk.len()),
7145 limit: CDC_MAX_ROWS,
7146 });
7147 }
7148 rows.extend(chunk);
7149 }
7150 rows
7151 } else {
7152 let Some(handle) = self.tables.read().get(&run.table_id).cloned() else {
7153 return Ok(CdcBatch {
7154 events: Vec::new(),
7155 current_epoch,
7156 earliest_epoch,
7157 gap: true,
7158 });
7159 };
7160 let table = handle.lock();
7161 let mut reader = match table.open_reader(run.run_id) {
7162 Ok(reader) => reader,
7163 Err(_) => {
7164 return Ok(CdcBatch {
7165 events: Vec::new(),
7166 current_epoch,
7167 earliest_epoch,
7168 gap: true,
7169 })
7170 }
7171 };
7172 let remaining = CDC_MAX_ROWS.saturating_sub(decoded_rows);
7173 let rows = reader.all_rows_controlled(control, remaining)?;
7174 drop(reader);
7175 drop(table);
7176 rows
7177 };
7178 for row in &mut rows {
7179 row.committed_epoch = Epoch(*commit_epoch);
7180 }
7181 decoded_rows = decoded_rows.saturating_add(rows.len());
7182 let event_bytes = cdc_rows_json_bytes(&rows).saturating_add(768);
7183 charge_cdc_bytes(&mut retained_bytes, event_bytes, "CDC retained event bytes")?;
7184 if events.len() >= CDC_MAX_EVENTS {
7185 return Err(MongrelError::ResourceLimitExceeded {
7186 resource: "CDC events",
7187 requested: events.len().saturating_add(1),
7188 limit: CDC_MAX_EVENTS,
7189 });
7190 }
7191 events.push(ChangeEvent {
7192 id: Some(format!("{}:{}", event_position.0, event_position.1)),
7193 channel: "changes".into(),
7194 table_id: Some(run.table_id),
7195 table: table_names.get(&run.table_id).cloned().unwrap_or_default(),
7196 op: "put_run".into(),
7197 epoch: *commit_epoch,
7198 txn_id: Some(record.txn_id),
7199 message: None,
7200 data: Some(serde_json::json!({
7201 "run_id": run.run_id.to_string(),
7202 "row_count": run.row_count,
7203 "min_row_id": run.min_row_id,
7204 "max_row_id": run.max_row_id,
7205 "rows": rows,
7206 })),
7207 });
7208 }
7209 }
7210 }
7211 control.checkpoint()?;
7212 Ok(CdcBatch {
7213 events,
7214 current_epoch,
7215 earliest_epoch,
7216 gap: false,
7217 })
7218 }
7219
7220 pub fn notify(&self, channel: &str, message: Option<String>) {
7223 let _ = self.notify.send(ChangeEvent {
7224 id: None,
7225 channel: channel.to_string(),
7226 table_id: None,
7227 table: String::new(),
7228 op: "notify".into(),
7229 epoch: self.epoch.visible().0,
7230 txn_id: None,
7231 message,
7232 data: None,
7233 });
7234 }
7235
7236 pub fn call_procedure(
7237 &self,
7238 name: &str,
7239 args: HashMap<String, crate::Value>,
7240 ) -> Result<ProcedureCallResult> {
7241 self.call_procedure_as(name, args, None)
7242 }
7243
7244 pub fn call_procedure_as(
7245 &self,
7246 name: &str,
7247 args: HashMap<String, crate::Value>,
7248 principal: Option<&crate::auth::Principal>,
7249 ) -> Result<ProcedureCallResult> {
7250 let control = crate::ExecutionControl::new(None);
7251 self.call_procedure_as_controlled(name, args, principal, &control, || true)
7252 }
7253
7254 #[doc(hidden)]
7257 pub fn call_procedure_as_bound(
7258 &self,
7259 expected: &StoredProcedure,
7260 args: HashMap<String, crate::Value>,
7261 principal: Option<&crate::auth::Principal>,
7262 ) -> Result<ProcedureCallResult> {
7263 self.require_for(principal, &crate::auth::Permission::All)?;
7264 let procedure = self.procedure(&expected.name).ok_or_else(|| {
7265 MongrelError::NotFound(format!("procedure {:?} not found", expected.name))
7266 })?;
7267 if &procedure != expected {
7268 return Err(MongrelError::Conflict(format!(
7269 "procedure {:?} changed after request authorization",
7270 expected.name
7271 )));
7272 }
7273 let control = crate::ExecutionControl::new(None);
7274 self.execute_procedure_as_controlled(procedure, args, principal, &control, || true)
7275 }
7276
7277 #[doc(hidden)]
7282 pub fn call_procedure_as_controlled<F>(
7283 &self,
7284 name: &str,
7285 args: HashMap<String, crate::Value>,
7286 principal: Option<&crate::auth::Principal>,
7287 control: &crate::ExecutionControl,
7288 before_commit: F,
7289 ) -> Result<ProcedureCallResult>
7290 where
7291 F: FnOnce() -> bool,
7292 {
7293 self.require_for(principal, &crate::auth::Permission::All)?;
7297 let procedure = self
7298 .procedure(name)
7299 .ok_or_else(|| MongrelError::NotFound(format!("procedure {name:?} not found")))?;
7300 self.execute_procedure_as_controlled(procedure, args, principal, control, before_commit)
7301 }
7302
7303 fn execute_procedure_as_controlled<F>(
7304 &self,
7305 procedure: StoredProcedure,
7306 args: HashMap<String, crate::Value>,
7307 principal: Option<&crate::auth::Principal>,
7308 control: &crate::ExecutionControl,
7309 before_commit: F,
7310 ) -> Result<ProcedureCallResult>
7311 where
7312 F: FnOnce() -> bool,
7313 {
7314 let args = bind_procedure_args(&procedure, args)?;
7315 let has_writes = procedure.body.steps.iter().any(ProcedureStep::is_write);
7316 let mut outputs: HashMap<String, ProcedureCallOutput> = HashMap::new();
7317 if has_writes {
7318 let mut tx = self.begin_as(principal.cloned());
7319 let run = (|| {
7320 for (step_index, step) in procedure.body.steps.iter().enumerate() {
7321 if step_index % 256 == 0 {
7322 control.checkpoint()?;
7323 }
7324 let output = self.execute_procedure_step(
7325 step,
7326 &args,
7327 &outputs,
7328 Some(&mut tx),
7329 principal,
7330 Some(control),
7331 )?;
7332 outputs.insert(step.id().to_string(), output);
7333 }
7334 control.checkpoint()?;
7335 eval_return_output(&procedure.body.return_value, &args, &outputs)
7336 })();
7337 match run {
7338 Ok(output) => {
7339 control.checkpoint()?;
7340 if !before_commit() {
7341 tx.rollback();
7342 return Err(MongrelError::Cancelled);
7343 }
7344 let epoch = tx.commit()?.0;
7345 Ok(ProcedureCallResult {
7346 epoch: Some(epoch),
7347 output,
7348 })
7349 }
7350 Err(e) => {
7351 tx.rollback();
7352 Err(e)
7353 }
7354 }
7355 } else {
7356 for (step_index, step) in procedure.body.steps.iter().enumerate() {
7357 if step_index % 256 == 0 {
7358 control.checkpoint()?;
7359 }
7360 let output = self.execute_procedure_step(
7361 step,
7362 &args,
7363 &outputs,
7364 None,
7365 principal,
7366 Some(control),
7367 )?;
7368 outputs.insert(step.id().to_string(), output);
7369 }
7370 control.checkpoint()?;
7371 Ok(ProcedureCallResult {
7372 epoch: None,
7373 output: eval_return_output(&procedure.body.return_value, &args, &outputs)?,
7374 })
7375 }
7376 }
7377
7378 fn execute_procedure_step(
7379 &self,
7380 step: &ProcedureStep,
7381 args: &HashMap<String, crate::Value>,
7382 outputs: &HashMap<String, ProcedureCallOutput>,
7383 tx: Option<&mut crate::txn::Transaction<'_>>,
7384 principal: Option<&crate::auth::Principal>,
7385 control: Option<&crate::ExecutionControl>,
7386 ) -> Result<ProcedureCallOutput> {
7387 if let Some(control) = control {
7388 control.checkpoint()?;
7389 }
7390 match step {
7391 ProcedureStep::NativeQuery {
7392 table,
7393 conditions,
7394 projection,
7395 limit,
7396 ..
7397 } => {
7398 let mut q = crate::Query::new();
7399 for condition in conditions {
7400 q = q.and(eval_condition(condition, args, outputs)?);
7401 }
7402 let fallback_control = crate::ExecutionControl::new(None);
7403 let query_control = control.unwrap_or(&fallback_control);
7404 let mut rows = self.query_for_principal_controlled(
7405 table,
7406 &q,
7407 projection.as_deref(),
7408 principal,
7409 false,
7410 query_control,
7411 )?;
7412 if let Some(limit) = limit {
7413 rows.truncate(*limit);
7414 }
7415 let mut output = Vec::with_capacity(rows.len());
7416 for (row_index, row) in rows.into_iter().enumerate() {
7417 if row_index % 256 == 0 {
7418 if let Some(control) = control {
7419 control.checkpoint()?;
7420 }
7421 }
7422 output.push(ProcedureCallRow {
7423 row_id: Some(row.row_id),
7424 columns: row.columns,
7425 });
7426 }
7427 Ok(ProcedureCallOutput::Rows(output))
7428 }
7429 ProcedureStep::Put {
7430 table,
7431 cells,
7432 returning,
7433 ..
7434 } => {
7435 let tx = tx.ok_or_else(|| {
7436 MongrelError::InvalidArgument(
7437 "write procedure step requires a transaction".into(),
7438 )
7439 })?;
7440 let cells = eval_cells(cells, args, outputs)?;
7441 if *returning {
7442 let out = tx.put_returning(table, cells)?;
7443 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
7444 row_id: None,
7445 columns: out.row.columns.into_iter().collect(),
7446 }))
7447 } else {
7448 tx.put(table, cells)?;
7449 Ok(ProcedureCallOutput::Null)
7450 }
7451 }
7452 ProcedureStep::Upsert {
7453 table,
7454 cells,
7455 update_cells,
7456 returning,
7457 ..
7458 } => {
7459 let tx = tx.ok_or_else(|| {
7460 MongrelError::InvalidArgument(
7461 "write procedure step requires a transaction".into(),
7462 )
7463 })?;
7464 let cells = eval_cells(cells, args, outputs)?;
7465 let action = match update_cells {
7466 Some(update_cells) => {
7467 crate::UpsertAction::DoUpdate(eval_cells(update_cells, args, outputs)?)
7468 }
7469 None => crate::UpsertAction::DoNothing,
7470 };
7471 let out = tx.upsert(table, cells, action)?;
7472 if *returning {
7473 Ok(ProcedureCallOutput::Row(ProcedureCallRow {
7474 row_id: None,
7475 columns: out.row.columns.into_iter().collect(),
7476 }))
7477 } else {
7478 Ok(ProcedureCallOutput::Null)
7479 }
7480 }
7481 ProcedureStep::DeleteByPk { table, pk, .. } => {
7482 let tx = tx.ok_or_else(|| {
7483 MongrelError::InvalidArgument(
7484 "write procedure step requires a transaction".into(),
7485 )
7486 })?;
7487 let pk = eval_value(pk, args, outputs)?;
7488 let handle = self.table(table)?;
7489 let row_id = handle.lock().lookup_pk(&pk.encode_key()).ok_or_else(|| {
7490 MongrelError::NotFound("procedure delete_by_pk target not found".into())
7491 })?;
7492 tx.delete(table, row_id)?;
7493 Ok(ProcedureCallOutput::Scalar(crate::Value::Bool(true)))
7494 }
7495 ProcedureStep::DeleteRows { .. } => Err(MongrelError::InvalidArgument(
7496 "DeleteRows procedure step is not supported by the core executor yet".into(),
7497 )),
7498 ProcedureStep::SqlQuery { .. } => Err(MongrelError::InvalidArgument(
7499 "SqlQuery procedure step must be executed by mongreldb-query".into(),
7500 )),
7501 }
7502 }
7503
7504 fn validate_procedure_references(&self, procedure: &StoredProcedure) -> Result<()> {
7505 let cat = self.catalog.read();
7506 for step in &procedure.body.steps {
7507 let Some(table_name) = step.table() else {
7508 continue;
7509 };
7510 let schema = &cat
7511 .live(table_name)
7512 .ok_or_else(|| {
7513 MongrelError::InvalidArgument(format!(
7514 "procedure {:?} references unknown table {table_name:?}",
7515 procedure.name
7516 ))
7517 })?
7518 .schema;
7519 match step {
7520 ProcedureStep::NativeQuery {
7521 conditions,
7522 projection,
7523 ..
7524 } => {
7525 for condition in conditions {
7526 validate_condition_columns(condition, schema)?;
7527 }
7528 if let Some(projection) = projection {
7529 for id in projection {
7530 validate_column_id(*id, schema)?;
7531 }
7532 }
7533 }
7534 ProcedureStep::Put { cells, .. } => {
7535 for cell in cells {
7536 validate_column_id(cell.column_id, schema)?;
7537 }
7538 }
7539 ProcedureStep::Upsert {
7540 cells,
7541 update_cells,
7542 ..
7543 } => {
7544 for cell in cells {
7545 validate_column_id(cell.column_id, schema)?;
7546 }
7547 if let Some(update_cells) = update_cells {
7548 for cell in update_cells {
7549 validate_column_id(cell.column_id, schema)?;
7550 }
7551 }
7552 }
7553 ProcedureStep::DeleteByPk { .. } => {
7554 if schema.primary_key().is_none() {
7555 return Err(MongrelError::InvalidArgument(format!(
7556 "procedure {:?} references DeleteByPk on table {table_name:?} without a primary key",
7557 procedure.name
7558 )));
7559 }
7560 }
7561 ProcedureStep::DeleteRows { .. } | ProcedureStep::SqlQuery { .. } => {}
7562 }
7563 }
7564 Ok(())
7565 }
7566
7567 fn validate_trigger_references(&self, trigger: &StoredTrigger) -> Result<()> {
7568 let cat = self.catalog.read();
7569 let target_schema = match &trigger.target {
7570 TriggerTarget::Table(target_name) => cat
7571 .live(target_name)
7572 .ok_or_else(|| {
7573 MongrelError::InvalidArgument(format!(
7574 "trigger {:?} references unknown target table {target_name:?}",
7575 trigger.name
7576 ))
7577 })?
7578 .schema
7579 .clone(),
7580 TriggerTarget::View(_) => Schema {
7581 columns: trigger.target_columns.clone(),
7582 ..Schema::default()
7583 },
7584 };
7585 for col in &trigger.update_of {
7586 if target_schema.column(col).is_none() {
7587 return Err(MongrelError::InvalidArgument(format!(
7588 "trigger {:?} UPDATE OF references unknown column {col:?}",
7589 trigger.name
7590 )));
7591 }
7592 }
7593 if let Some(expr) = &trigger.when {
7594 validate_trigger_expr(expr, &target_schema, trigger.event)?;
7595 }
7596 let mut select_schemas: HashMap<String, &Schema> = HashMap::new();
7597 for step in &trigger.program.steps {
7598 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before
7599 {
7600 return Err(MongrelError::InvalidArgument(
7601 "SetNew trigger steps are only valid in BEFORE triggers".into(),
7602 ));
7603 }
7604 validate_trigger_step(
7605 step,
7606 &cat,
7607 &target_schema,
7608 trigger.event,
7609 &mut select_schemas,
7610 )?;
7611 }
7612 Ok(())
7613 }
7614
7615 pub fn begin(&self) -> crate::txn::Transaction<'_> {
7617 self.begin_with_isolation(crate::txn::IsolationLevel::default())
7618 }
7619
7620 fn transaction_principal_snapshot(&self) -> (Option<crate::auth::Principal>, bool) {
7621 let principal = self.principal.read().clone();
7622 let catalog_bound = principal.as_ref().is_some_and(|principal| {
7623 let catalog = self.catalog.read();
7624 catalog.require_auth || principal.user_id != 0
7625 });
7626 (principal, catalog_bound)
7627 }
7628
7629 pub fn begin_as(
7630 &self,
7631 principal: Option<crate::auth::Principal>,
7632 ) -> crate::txn::Transaction<'_> {
7633 let catalog_bound = principal.as_ref().is_some_and(|principal| {
7634 let catalog = self.catalog.read();
7635 catalog.require_auth || principal.user_id != 0
7636 });
7637 let txn_id = self.alloc_txn_id();
7638 let read = Snapshot::at(self.epoch.visible());
7639 crate::txn::Transaction::new(self, txn_id, read).with_principal(principal, catalog_bound)
7640 }
7641
7642 pub fn begin_with_isolation(
7644 &self,
7645 level: crate::txn::IsolationLevel,
7646 ) -> crate::txn::Transaction<'_> {
7647 let txn_id = self.alloc_txn_id();
7648 let epoch = match level {
7649 crate::txn::IsolationLevel::ReadCommitted => self.epoch.visible(),
7650 _ => self.epoch.visible(),
7651 };
7652 let read = Snapshot::at(epoch);
7653 let (principal, catalog_bound) = self.transaction_principal_snapshot();
7654 crate::txn::Transaction::new(self, txn_id, read).with_principal(principal, catalog_bound)
7655 }
7656
7657 pub fn begin_with_external_trigger_bridge<'a>(
7660 &'a self,
7661 bridge: &'a dyn ExternalTriggerBridge,
7662 ) -> crate::txn::Transaction<'a> {
7663 let txn_id = self.alloc_txn_id();
7664 let read = Snapshot::at(self.epoch.visible());
7665 let (principal, catalog_bound) = self.transaction_principal_snapshot();
7666 crate::txn::Transaction::new(self, txn_id, read)
7667 .with_external_trigger_bridge(bridge)
7668 .with_principal(principal, catalog_bound)
7669 }
7670
7671 pub fn begin_with_external_trigger_bridge_as<'a>(
7672 &'a self,
7673 bridge: &'a dyn ExternalTriggerBridge,
7674 principal: Option<crate::auth::Principal>,
7675 ) -> crate::txn::Transaction<'a> {
7676 let catalog_bound = principal.as_ref().is_some_and(|principal| {
7677 let catalog = self.catalog.read();
7678 catalog.require_auth || principal.user_id != 0
7679 });
7680 let txn_id = self.alloc_txn_id();
7681 let read = Snapshot::at(self.epoch.visible());
7682 crate::txn::Transaction::new(self, txn_id, read)
7683 .with_external_trigger_bridge(bridge)
7684 .with_principal(principal, catalog_bound)
7685 }
7686
7687 pub fn transaction<T>(
7689 &self,
7690 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7691 ) -> Result<T> {
7692 let mut tx = self.begin();
7693 match f(&mut tx) {
7694 Ok(out) => {
7695 tx.commit()?;
7696 Ok(out)
7697 }
7698 Err(e) => {
7699 tx.rollback();
7700 Err(e)
7701 }
7702 }
7703 }
7704
7705 pub fn transaction_with_row_ids<T>(
7706 &self,
7707 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7708 ) -> Result<(T, Vec<RowId>)> {
7709 let mut tx = self.begin();
7710 match f(&mut tx) {
7711 Ok(output) => {
7712 let (_, row_ids) = tx.commit_with_row_ids()?;
7713 Ok((output, row_ids))
7714 }
7715 Err(error) => {
7716 tx.rollback();
7717 Err(error)
7718 }
7719 }
7720 }
7721
7722 pub fn transaction_for_current_principal<T>(
7723 &self,
7724 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7725 ) -> Result<T> {
7726 if self.principal.read().is_some() {
7727 self.refresh_principal()?;
7728 }
7729 let mut transaction = self.begin_as(self.principal.read().clone());
7730 match f(&mut transaction) {
7731 Ok(output) => {
7732 transaction.commit()?;
7733 Ok(output)
7734 }
7735 Err(error) => {
7736 transaction.rollback();
7737 Err(error)
7738 }
7739 }
7740 }
7741
7742 pub fn transaction_for_current_principal_with_epoch<T>(
7743 &self,
7744 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7745 ) -> Result<(Epoch, T)> {
7746 if self.principal.read().is_some() {
7747 self.refresh_principal()?;
7748 }
7749 let mut transaction = self.begin_as(self.principal.read().clone());
7750 match f(&mut transaction) {
7751 Ok(output) => {
7752 let epoch = transaction.commit()?;
7753 Ok((epoch, output))
7754 }
7755 Err(error) => {
7756 transaction.rollback();
7757 Err(error)
7758 }
7759 }
7760 }
7761
7762 pub fn transaction_with_row_ids_for_current_principal<T>(
7763 &self,
7764 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7765 ) -> Result<(T, Vec<RowId>)> {
7766 if self.principal.read().is_some() {
7767 self.refresh_principal()?;
7768 }
7769 let mut transaction = self.begin_as(self.principal.read().clone());
7770 match f(&mut transaction) {
7771 Ok(output) => {
7772 let (_, row_ids) = transaction.commit_with_row_ids()?;
7773 Ok((output, row_ids))
7774 }
7775 Err(error) => {
7776 transaction.rollback();
7777 Err(error)
7778 }
7779 }
7780 }
7781
7782 pub fn transaction_with_external_trigger_bridge<'a, T>(
7785 &'a self,
7786 bridge: &'a dyn ExternalTriggerBridge,
7787 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7788 ) -> Result<T> {
7789 let mut tx = self.begin_with_external_trigger_bridge(bridge);
7790 match f(&mut tx) {
7791 Ok(out) => {
7792 tx.commit()?;
7793 Ok(out)
7794 }
7795 Err(e) => {
7796 tx.rollback();
7797 Err(e)
7798 }
7799 }
7800 }
7801
7802 pub fn transaction_with_external_trigger_bridge_as<'a, T>(
7803 &'a self,
7804 bridge: &'a dyn ExternalTriggerBridge,
7805 principal: Option<crate::auth::Principal>,
7806 f: impl FnOnce(&mut crate::txn::Transaction) -> Result<T>,
7807 ) -> Result<T> {
7808 let mut tx = self.begin_with_external_trigger_bridge_as(bridge, principal);
7809 match f(&mut tx) {
7810 Ok(output) => {
7811 tx.commit()?;
7812 Ok(output)
7813 }
7814 Err(error) => {
7815 tx.rollback();
7816 Err(error)
7817 }
7818 }
7819 }
7820
7821 pub(crate) fn register_active(&self, epoch: Epoch) -> crate::txn::ActiveTxnGuard<'_> {
7824 self.active_txns.register(epoch)
7825 }
7826
7827 fn fill_auto_increment_for_staging(
7828 &self,
7829 staging: &mut [(u64, crate::txn::Staged)],
7830 control: Option<&crate::ExecutionControl>,
7831 ) -> Result<()> {
7832 let mut puts_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
7833 for (index, (table_id, staged)) in staging.iter().enumerate() {
7834 commit_prepare_checkpoint(control, index)?;
7835 if matches!(staged, crate::txn::Staged::Put(_)) {
7836 puts_by_table.entry(*table_id).or_default().push(index);
7837 }
7838 }
7839
7840 let tables = self.tables.read();
7841 for (table_index, (table_id, indexes)) in puts_by_table.into_iter().enumerate() {
7842 commit_prepare_checkpoint(control, table_index)?;
7843 if let Some(handle) = tables.get(&table_id) {
7844 #[cfg(test)]
7845 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
7846 let mut t = handle.lock();
7847 for (fill_index, index) in indexes.into_iter().enumerate() {
7848 commit_prepare_checkpoint(control, fill_index)?;
7849 if let crate::txn::Staged::Put(cells) = &mut staging[index].1 {
7850 t.fill_auto_inc(cells)?;
7851 }
7852 }
7853 }
7854 }
7855 Ok(())
7856 }
7857
7858 fn expand_table_triggers(
7859 &self,
7860 staging: &mut Vec<(u64, crate::txn::Staged)>,
7861 read_epoch: Epoch,
7862 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
7863 external_states: &mut Vec<(String, Vec<u8>)>,
7864 control: Option<&crate::ExecutionControl>,
7865 ) -> Result<()> {
7866 commit_prepare_checkpoint(control, 0)?;
7867 let mut external_writes = Vec::new();
7868 let config = self.trigger_config();
7869 if config.recursive_triggers {
7870 let chunk = std::mem::take(staging);
7871 let stacks = vec![Vec::new(); chunk.len()];
7872 *staging = self.expand_trigger_chunk(
7873 chunk,
7874 stacks,
7875 read_epoch,
7876 0,
7877 config.max_depth,
7878 &mut external_writes,
7879 &config,
7880 control,
7881 )?;
7882 self.apply_external_trigger_writes(
7883 external_writes,
7884 external_trigger_bridge,
7885 external_states,
7886 staging,
7887 control,
7888 )?;
7889 return Ok(());
7890 }
7891
7892 let mut expansion =
7893 self.expand_table_triggers_once(staging, read_epoch, None, &config, control)?;
7894 if !expansion.before.is_empty() {
7895 let mut final_staging = expansion.before;
7896 final_staging.extend(filter_ignored_staging(
7897 std::mem::take(staging),
7898 &expansion.ignored_indices,
7899 ));
7900 *staging = final_staging;
7901 } else if !expansion.ignored_indices.is_empty() {
7902 *staging = filter_ignored_staging(std::mem::take(staging), &expansion.ignored_indices);
7903 }
7904 staging.append(&mut expansion.after);
7905 external_writes.append(&mut expansion.before_external);
7906 external_writes.append(&mut expansion.after_external);
7907 self.apply_external_trigger_writes(
7908 external_writes,
7909 external_trigger_bridge,
7910 external_states,
7911 staging,
7912 control,
7913 )?;
7914 Ok(())
7915 }
7916
7917 #[allow(clippy::too_many_arguments)]
7918 fn expand_trigger_chunk(
7919 &self,
7920 mut chunk: Vec<(u64, crate::txn::Staged)>,
7921 stacks: Vec<Vec<String>>,
7922 read_epoch: Epoch,
7923 depth: u32,
7924 max_depth: u32,
7925 external_writes: &mut Vec<ExternalTriggerWrite>,
7926 config: &TriggerConfig,
7927 control: Option<&crate::ExecutionControl>,
7928 ) -> Result<Vec<(u64, crate::txn::Staged)>> {
7929 if chunk.is_empty() {
7930 return Ok(Vec::new());
7931 }
7932 commit_prepare_checkpoint(control, 0)?;
7933 self.fill_auto_increment_for_staging(&mut chunk, control)?;
7934 let expansion = self.expand_table_triggers_once(
7935 &mut chunk,
7936 read_epoch,
7937 Some(&stacks),
7938 config,
7939 control,
7940 )?;
7941 if depth >= max_depth && (!expansion.before.is_empty() || !expansion.after.is_empty()) {
7942 let stack = expansion
7943 .before_stacks
7944 .first()
7945 .or_else(|| expansion.after_stacks.first())
7946 .cloned()
7947 .unwrap_or_default();
7948 return Err(MongrelError::TriggerValidation(format!(
7949 "trigger recursion exceeded max depth {max_depth}; trigger stack: {}",
7950 Self::format_trigger_stack(&stack)
7951 )));
7952 }
7953
7954 let mut out = Vec::new();
7955 external_writes.extend(expansion.before_external);
7956 out.extend(self.expand_trigger_chunk(
7957 expansion.before,
7958 expansion.before_stacks,
7959 read_epoch,
7960 depth + 1,
7961 max_depth,
7962 external_writes,
7963 config,
7964 control,
7965 )?);
7966 out.extend(filter_ignored_staging(chunk, &expansion.ignored_indices));
7967 external_writes.extend(expansion.after_external);
7968 out.extend(self.expand_trigger_chunk(
7969 expansion.after,
7970 expansion.after_stacks,
7971 read_epoch,
7972 depth + 1,
7973 max_depth,
7974 external_writes,
7975 config,
7976 control,
7977 )?);
7978 Ok(out)
7979 }
7980
7981 fn apply_external_trigger_writes(
7982 &self,
7983 writes: Vec<ExternalTriggerWrite>,
7984 bridge: Option<&dyn ExternalTriggerBridge>,
7985 external_states: &mut Vec<(String, Vec<u8>)>,
7986 staging: &mut Vec<(u64, crate::txn::Staged)>,
7987 control: Option<&crate::ExecutionControl>,
7988 ) -> Result<()> {
7989 if writes.is_empty() {
7990 return Ok(());
7991 }
7992 let bridge = bridge.ok_or_else(|| {
7993 MongrelError::TriggerValidation(
7994 "trigger program wrote an external table, but this transaction has no external trigger bridge".into(),
7995 )
7996 })?;
7997 for (write_index, write) in writes.into_iter().enumerate() {
7998 commit_prepare_checkpoint(control, write_index)?;
7999 let table = write.table().to_string();
8000 let entry = self.external_table(&table).ok_or_else(|| {
8001 MongrelError::NotFound(format!("external table {table:?} not found"))
8002 })?;
8003 let base_state = current_external_state_bytes(&self.root, external_states, &table)?;
8004 let result = bridge.apply_trigger_external_write(&entry, base_state, write)?;
8005 external_states.push((table, result.state));
8006 for (base_index, base_write) in result.base_writes.into_iter().enumerate() {
8007 commit_prepare_checkpoint(control, base_index)?;
8008 match base_write {
8009 ExternalTriggerBaseWrite::Put { table, cells } => {
8010 let table_id = self.table_id(&table)?;
8011 staging.push((table_id, crate::txn::Staged::Put(cells)));
8012 }
8013 ExternalTriggerBaseWrite::Delete { table, row_id } => {
8014 let table_id = self.table_id(&table)?;
8015 staging.push((table_id, crate::txn::Staged::Delete(row_id)));
8016 }
8017 }
8018 }
8019 }
8020 dedup_external_states_in_place(external_states);
8021 Ok(())
8022 }
8023
8024 fn expand_table_triggers_once(
8025 &self,
8026 staging: &mut Vec<(u64, crate::txn::Staged)>,
8027 read_epoch: Epoch,
8028 trigger_stacks: Option<&[Vec<String>]>,
8029 config: &TriggerConfig,
8030 control: Option<&crate::ExecutionControl>,
8031 ) -> Result<TriggerExpansion> {
8032 commit_prepare_checkpoint(control, 0)?;
8033 let triggers: Vec<StoredTrigger> = self
8034 .catalog
8035 .read()
8036 .triggers
8037 .iter()
8038 .filter(|entry| {
8039 entry.trigger.enabled
8040 && matches!(
8041 entry.trigger.timing,
8042 TriggerTiming::Before | TriggerTiming::After
8043 )
8044 && matches!(entry.trigger.target, TriggerTarget::Table(_))
8045 })
8046 .map(|entry| entry.trigger.clone())
8047 .collect();
8048 if triggers.is_empty() || staging.is_empty() {
8049 return Ok(TriggerExpansion::default());
8050 }
8051
8052 let before_triggers = triggers
8053 .iter()
8054 .filter(|trigger| trigger.timing == TriggerTiming::Before)
8055 .cloned()
8056 .collect::<Vec<_>>();
8057 let after_triggers = triggers
8058 .iter()
8059 .filter(|trigger| trigger.timing == TriggerTiming::After)
8060 .cloned()
8061 .collect::<Vec<_>>();
8062
8063 let mut before_added = Vec::new();
8064 let mut before_stacks = Vec::new();
8065 let mut before_external = Vec::new();
8066 let mut ignored_indices = std::collections::BTreeSet::new();
8067 if !before_triggers.is_empty() {
8068 let before_events =
8069 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?;
8070 let mut out = TriggerProgramOutput {
8071 added: &mut before_added,
8072 added_stacks: &mut before_stacks,
8073 added_external: &mut before_external,
8074 ignored_indices: &mut ignored_indices,
8075 };
8076 self.execute_triggers_for_events(
8077 &before_triggers,
8078 &before_events,
8079 Some(staging),
8080 &mut out,
8081 config,
8082 read_epoch,
8083 control,
8084 )?;
8085 }
8086
8087 let after_events = if after_triggers.is_empty() {
8088 Vec::new()
8089 } else {
8090 self.trigger_events_for_staging(staging, read_epoch, trigger_stacks, control)?
8091 .into_iter()
8092 .filter(|event| {
8093 !event
8094 .op_indices
8095 .iter()
8096 .any(|idx| ignored_indices.contains(idx))
8097 })
8098 .collect()
8099 };
8100
8101 let mut after_added = Vec::new();
8102 let mut after_stacks = Vec::new();
8103 let mut after_external = Vec::new();
8104 let mut out = TriggerProgramOutput {
8105 added: &mut after_added,
8106 added_stacks: &mut after_stacks,
8107 added_external: &mut after_external,
8108 ignored_indices: &mut ignored_indices,
8109 };
8110 self.execute_triggers_for_events(
8111 &after_triggers,
8112 &after_events,
8113 None,
8114 &mut out,
8115 config,
8116 read_epoch,
8117 control,
8118 )?;
8119 Ok(TriggerExpansion {
8120 before: before_added,
8121 before_stacks,
8122 before_external,
8123 after: after_added,
8124 after_stacks,
8125 after_external,
8126 ignored_indices,
8127 })
8128 }
8129
8130 #[allow(clippy::too_many_arguments)]
8131 fn execute_triggers_for_events(
8132 &self,
8133 triggers: &[StoredTrigger],
8134 events: &[WriteEvent],
8135 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
8136 out: &mut TriggerProgramOutput<'_>,
8137 config: &TriggerConfig,
8138 read_epoch: Epoch,
8139 control: Option<&crate::ExecutionControl>,
8140 ) -> Result<()> {
8141 let mut checkpoint_index = 0_usize;
8142 for event in events {
8143 for trigger in triggers {
8144 commit_prepare_checkpoint(control, checkpoint_index)?;
8145 checkpoint_index += 1;
8146 if event
8147 .op_indices
8148 .iter()
8149 .any(|idx| out.ignored_indices.contains(idx))
8150 {
8151 break;
8152 }
8153 let matches = {
8154 let cat = self.catalog.read();
8155 trigger_matches_event(trigger, event, &cat)?
8156 };
8157 if !matches {
8158 continue;
8159 }
8160 if let Some(when) = &trigger.when {
8161 if !eval_trigger_expr(when, event)? {
8162 continue;
8163 }
8164 }
8165 let trigger_stack = Self::trigger_stack_with(&event.trigger_stack, &trigger.name);
8166 if event.trigger_stack.iter().any(|name| name == &trigger.name) {
8167 return Err(MongrelError::TriggerValidation(format!(
8168 "trigger recursion cycle detected; trigger stack: {}",
8169 Self::format_trigger_stack(&trigger_stack)
8170 )));
8171 }
8172 let outcome = match staging.as_mut() {
8173 Some(staging) => self.execute_trigger_program(
8174 trigger,
8175 event,
8176 Some(&mut **staging),
8177 out,
8178 &trigger_stack,
8179 config,
8180 read_epoch,
8181 control,
8182 )?,
8183 None => self.execute_trigger_program(
8184 trigger,
8185 event,
8186 None,
8187 out,
8188 &trigger_stack,
8189 config,
8190 read_epoch,
8191 control,
8192 )?,
8193 };
8194 if outcome == TriggerProgramOutcome::Ignore {
8195 out.ignored_indices.extend(event.op_indices.iter().copied());
8196 break;
8197 }
8198 }
8199 }
8200 Ok(())
8201 }
8202
8203 fn trigger_events_for_staging(
8204 &self,
8205 staging: &[(u64, crate::txn::Staged)],
8206 read_epoch: Epoch,
8207 trigger_stacks: Option<&[Vec<String>]>,
8208 control: Option<&crate::ExecutionControl>,
8209 ) -> Result<Vec<WriteEvent>> {
8210 use crate::txn::Staged;
8211 use std::collections::{HashMap, VecDeque};
8212
8213 let snapshot = Snapshot::at(read_epoch);
8214 let cat = self.catalog.read();
8215 let mut table_names = HashMap::new();
8216 let mut table_schemas = HashMap::new();
8217 for entry in cat
8218 .tables
8219 .iter()
8220 .filter(|entry| matches!(entry.state, TableState::Live))
8221 {
8222 table_names.insert(entry.table_id, entry.name.clone());
8223 table_schemas.insert(entry.table_id, entry.schema.clone());
8224 }
8225 drop(cat);
8226
8227 let mut old_rows: HashMap<usize, TriggerRowImage> = HashMap::new();
8228 let mut delete_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
8229 let mut put_by_key: HashMap<(u64, Vec<u8>), VecDeque<usize>> = HashMap::new();
8230
8231 for (idx, (table_id, staged)) in staging.iter().enumerate() {
8232 commit_prepare_checkpoint(control, idx)?;
8233 let Some(schema) = table_schemas.get(table_id) else {
8234 continue;
8235 };
8236 let Some(pk) = schema.primary_key() else {
8237 continue;
8238 };
8239 match staged {
8240 Staged::Delete(row_id) => {
8241 let handle = self.table_by_id(*table_id)?;
8242 let Some(row) = handle.lock().get(*row_id, snapshot) else {
8243 continue;
8244 };
8245 let Some(pk_value) = row.columns.get(&pk.id) else {
8246 continue;
8247 };
8248 old_rows.insert(idx, TriggerRowImage::from_row(row.clone()));
8249 delete_by_key
8250 .entry((*table_id, pk_value.encode_key()))
8251 .or_default()
8252 .push_back(idx);
8253 }
8254 Staged::Put(cells) => {
8255 if let Some((_, value)) = cells.iter().find(|(id, _)| *id == pk.id) {
8256 put_by_key
8257 .entry((*table_id, value.encode_key()))
8258 .or_default()
8259 .push_back(idx);
8260 }
8261 }
8262 Staged::Update { row_id, .. } => {
8263 let handle = self.table_by_id(*table_id)?;
8264 let row = handle.lock().get(*row_id, snapshot);
8265 if let Some(row) = row {
8266 old_rows.insert(idx, TriggerRowImage::from_row(row));
8267 }
8268 }
8269 Staged::Truncate => {}
8270 }
8271 }
8272
8273 let mut paired_delete = std::collections::HashSet::new();
8274 let mut paired_put = std::collections::HashSet::new();
8275 let mut events = Vec::new();
8276
8277 for (pair_index, (key, deletes)) in delete_by_key.iter_mut().enumerate() {
8278 commit_prepare_checkpoint(control, pair_index)?;
8279 let Some(puts) = put_by_key.get_mut(key) else {
8280 continue;
8281 };
8282 while let (Some(delete_idx), Some(put_idx)) = (deletes.pop_front(), puts.pop_front()) {
8283 paired_delete.insert(delete_idx);
8284 paired_put.insert(put_idx);
8285 let (table_id, _) = &staging[put_idx];
8286 let Some(table_name) = table_names.get(table_id).cloned() else {
8287 continue;
8288 };
8289 let old = old_rows.get(&delete_idx).cloned();
8290 let new = match &staging[put_idx].1 {
8291 Staged::Put(cells) => Some(TriggerRowImage::from_cells(cells)),
8292 _ => None,
8293 };
8294 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
8295 events.push(WriteEvent {
8296 table: table_name,
8297 kind: TriggerEvent::Update,
8298 old,
8299 new,
8300 changed_columns,
8301 op_indices: vec![delete_idx, put_idx],
8302 put_idx: Some(put_idx),
8303 trigger_stack: Self::trigger_stack_for_indices(
8304 trigger_stacks,
8305 &[delete_idx, put_idx],
8306 ),
8307 });
8308 }
8309 }
8310
8311 for (idx, (table_id, staged)) in staging.iter().enumerate() {
8312 commit_prepare_checkpoint(control, idx)?;
8313 let Some(table_name) = table_names.get(table_id).cloned() else {
8314 continue;
8315 };
8316 match staged {
8317 Staged::Put(cells) if !paired_put.contains(&idx) => {
8318 let new = Some(TriggerRowImage::from_cells(cells));
8319 let changed_columns = cells.iter().map(|(id, _)| *id).collect();
8320 events.push(WriteEvent {
8321 table: table_name,
8322 kind: TriggerEvent::Insert,
8323 old: None,
8324 new,
8325 changed_columns,
8326 op_indices: vec![idx],
8327 put_idx: Some(idx),
8328 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
8329 });
8330 }
8331 Staged::Delete(row_id) if !paired_delete.contains(&idx) => {
8332 let old = match old_rows.get(&idx).cloned() {
8333 Some(old) => Some(old),
8334 None => {
8335 let handle = self.table_by_id(*table_id)?;
8336 let row = handle.lock().get(*row_id, snapshot);
8337 row.map(TriggerRowImage::from_row)
8338 }
8339 };
8340 let Some(old) = old else {
8341 continue;
8342 };
8343 let changed_columns = old.columns.keys().copied().collect();
8344 events.push(WriteEvent {
8345 table: table_name,
8346 kind: TriggerEvent::Delete,
8347 old: Some(old),
8348 new: None,
8349 changed_columns,
8350 op_indices: vec![idx],
8351 put_idx: None,
8352 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
8353 });
8354 }
8355 Staged::Update { new_row: cells, .. } => {
8356 let old = old_rows.get(&idx).cloned();
8357 let new = Some(TriggerRowImage::from_cells(cells));
8358 let changed_columns = changed_columns(old.as_ref(), new.as_ref());
8359 events.push(WriteEvent {
8360 table: table_name,
8361 kind: TriggerEvent::Update,
8362 old,
8363 new,
8364 changed_columns,
8365 op_indices: vec![idx],
8366 put_idx: Some(idx),
8367 trigger_stack: Self::trigger_stack_for_indices(trigger_stacks, &[idx]),
8368 });
8369 }
8370 Staged::Truncate => {}
8371 _ => {}
8372 }
8373 }
8374
8375 Ok(events)
8376 }
8377
8378 #[allow(clippy::too_many_arguments)]
8379 fn execute_trigger_program(
8380 &self,
8381 trigger: &StoredTrigger,
8382 event: &WriteEvent,
8383 staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
8384 out: &mut TriggerProgramOutput<'_>,
8385 trigger_stack: &[String],
8386 config: &TriggerConfig,
8387 read_epoch: Epoch,
8388 control: Option<&crate::ExecutionControl>,
8389 ) -> Result<TriggerProgramOutcome> {
8390 let mut event = event.clone();
8391 let mut select_results: HashMap<String, Vec<TriggerRowImage>> = HashMap::new();
8392 self.execute_trigger_steps(
8393 trigger,
8394 &trigger.program.steps,
8395 &mut event,
8396 staging,
8397 out,
8398 trigger_stack,
8399 config,
8400 &mut select_results,
8401 0,
8402 None,
8403 read_epoch,
8404 control,
8405 )
8406 }
8407
8408 #[allow(clippy::too_many_arguments)]
8409 fn execute_trigger_steps(
8410 &self,
8411 trigger: &StoredTrigger,
8412 steps: &[TriggerStep],
8413 event: &mut WriteEvent,
8414 mut staging: Option<&mut Vec<(u64, crate::txn::Staged)>>,
8415 out: &mut TriggerProgramOutput<'_>,
8416 trigger_stack: &[String],
8417 config: &TriggerConfig,
8418 select_results: &mut HashMap<String, Vec<TriggerRowImage>>,
8419 depth: u32,
8420 selected: Option<&TriggerRowImage>,
8421 read_epoch: Epoch,
8422 control: Option<&crate::ExecutionControl>,
8423 ) -> Result<TriggerProgramOutcome> {
8424 let _ = depth;
8425 for (step_index, step) in steps.iter().enumerate() {
8426 commit_prepare_checkpoint(control, step_index)?;
8427 match step {
8428 TriggerStep::SetNew { cells } => {
8429 if trigger.timing != TriggerTiming::Before {
8430 return Err(MongrelError::InvalidArgument(
8431 "SetNew trigger step is only valid in BEFORE triggers".into(),
8432 ));
8433 }
8434 let put_idx = event.put_idx.ok_or_else(|| {
8435 MongrelError::InvalidArgument(
8436 "SetNew trigger step requires INSERT or UPDATE NEW row".into(),
8437 )
8438 })?;
8439 let staging = staging.as_deref_mut().ok_or_else(|| {
8440 MongrelError::InvalidArgument(
8441 "SetNew trigger step requires mutable trigger staging".into(),
8442 )
8443 })?;
8444 let mut update_changed_columns = None;
8445 let row_cells = match staging.get_mut(put_idx).map(|(_, op)| op) {
8446 Some(crate::txn::Staged::Put(cells)) => cells,
8447 Some(crate::txn::Staged::Update {
8448 new_row,
8449 changed_columns,
8450 ..
8451 }) => {
8452 update_changed_columns = Some(changed_columns);
8453 new_row
8454 }
8455 _ => {
8456 return Err(MongrelError::InvalidArgument(
8457 "SetNew trigger step target row is not mutable".into(),
8458 ))
8459 }
8460 };
8461 for (column_id, value) in eval_trigger_cells(cells, event, selected)? {
8462 row_cells.retain(|(id, _)| *id != column_id);
8463 row_cells.push((column_id, value.clone()));
8464 if let Some(changed_columns) = &mut update_changed_columns {
8465 changed_columns.push(column_id);
8466 }
8467 if let Some(new) = &mut event.new {
8468 new.columns.insert(column_id, value);
8469 }
8470 }
8471 row_cells.sort_by_key(|(id, _)| *id);
8472 if let Some(changed_columns) = update_changed_columns {
8473 changed_columns.sort_unstable();
8474 changed_columns.dedup();
8475 }
8476 }
8477 TriggerStep::Insert { table, cells } => {
8478 let cells = eval_trigger_cells(cells, event, selected)?;
8479 if let Ok(table_id) = self.table_id(table) {
8480 out.added.push((table_id, crate::txn::Staged::Put(cells)));
8481 out.added_stacks.push(trigger_stack.to_vec());
8482 } else if self.external_table(table).is_some() {
8483 out.added_external.push(ExternalTriggerWrite::Insert {
8484 table: table.clone(),
8485 cells,
8486 });
8487 } else {
8488 return Err(MongrelError::NotFound(format!(
8489 "trigger {:?} insert target {table:?} not found",
8490 trigger.name
8491 )));
8492 }
8493 }
8494 TriggerStep::UpdateByPk { table, pk, cells } => {
8495 let pk = eval_trigger_value(pk, event, selected)?;
8496 let cells = eval_trigger_cells(cells, event, selected)?;
8497 if self.external_table(table).is_some() {
8498 out.added_external.push(ExternalTriggerWrite::UpdateByPk {
8499 table: table.clone(),
8500 pk,
8501 cells,
8502 });
8503 } else {
8504 let row_id = self
8505 .table(table)?
8506 .lock()
8507 .lookup_pk(&pk.encode_key())
8508 .ok_or_else(|| {
8509 MongrelError::NotFound(format!(
8510 "trigger {:?} update target not found",
8511 trigger.name
8512 ))
8513 })?;
8514 let handle = self.table(table)?;
8515 let snapshot = Snapshot::at(self.epoch.visible());
8516 let old = handle.lock().get(row_id, snapshot).ok_or_else(|| {
8517 MongrelError::NotFound(format!(
8518 "trigger {:?} update target not visible",
8519 trigger.name
8520 ))
8521 })?;
8522 let mut changed_columns = cells
8523 .iter()
8524 .map(|(column_id, _)| *column_id)
8525 .collect::<Vec<_>>();
8526 changed_columns.sort_unstable();
8527 changed_columns.dedup();
8528 let mut merged = old.columns;
8529 for (column_id, value) in cells {
8530 merged.insert(column_id, value);
8531 }
8532 out.added.push((
8533 self.table_id(table)?,
8534 crate::txn::Staged::Update {
8535 row_id,
8536 new_row: merged.into_iter().collect(),
8537 changed_columns,
8538 },
8539 ));
8540 out.added_stacks.push(trigger_stack.to_vec());
8541 }
8542 }
8543 TriggerStep::DeleteByPk { table, pk } => {
8544 let pk = eval_trigger_value(pk, event, selected)?;
8545 if self.external_table(table).is_some() {
8546 out.added_external.push(ExternalTriggerWrite::DeleteByPk {
8547 table: table.clone(),
8548 pk,
8549 });
8550 } else {
8551 let row_id = self
8552 .table(table)?
8553 .lock()
8554 .lookup_pk(&pk.encode_key())
8555 .ok_or_else(|| {
8556 MongrelError::NotFound(format!(
8557 "trigger {:?} delete target not found",
8558 trigger.name
8559 ))
8560 })?;
8561 out.added
8562 .push((self.table_id(table)?, crate::txn::Staged::Delete(row_id)));
8563 out.added_stacks.push(trigger_stack.to_vec());
8564 }
8565 }
8566 TriggerStep::Select {
8567 id,
8568 table,
8569 conditions,
8570 } => {
8571 let schema = self.table(table)?.lock().schema().clone();
8572 let snapshot = Snapshot::at(read_epoch);
8573 let handle = self.table(table)?;
8574 let rows = match control {
8575 Some(control) => {
8576 handle.lock().visible_rows_controlled(snapshot, control)?
8577 }
8578 None => handle.lock().visible_rows(snapshot)?,
8579 };
8580 let mut matched = Vec::new();
8581 for (row_index, row) in rows.into_iter().enumerate() {
8582 commit_prepare_checkpoint(control, row_index)?;
8583 let image = TriggerRowImage::from_row(row);
8584 let passes = conditions
8585 .iter()
8586 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
8587 .collect::<Result<Vec<_>>>()?
8588 .into_iter()
8589 .all(|b| b);
8590 if passes {
8591 matched.push(image);
8592 }
8593 }
8594 if let Some(pk) = schema.primary_key() {
8595 matched.sort_by(|a, b| {
8596 let av = a.columns.get(&pk.id).unwrap_or(&Value::Null);
8597 let bv = b.columns.get(&pk.id).unwrap_or(&Value::Null);
8598 value_order(av, bv).unwrap_or(std::cmp::Ordering::Equal)
8599 });
8600 }
8601 select_results.insert(id.clone(), matched);
8602 }
8603 TriggerStep::Foreach { id, steps } => {
8604 let rows = select_results.get(id).ok_or_else(|| {
8605 MongrelError::InvalidArgument(format!(
8606 "trigger {:?} foreach references unknown select id {id:?}",
8607 trigger.name
8608 ))
8609 })?;
8610 if rows.len() > config.max_loop_iterations as usize {
8611 return Err(MongrelError::InvalidArgument(format!(
8612 "trigger {:?} foreach exceeded max_loop_iterations ({})",
8613 trigger.name, config.max_loop_iterations
8614 )));
8615 }
8616 for (row_index, row) in rows.clone().into_iter().enumerate() {
8617 commit_prepare_checkpoint(control, row_index)?;
8618 let result = self.execute_trigger_steps(
8619 trigger,
8620 steps,
8621 event,
8622 staging.as_deref_mut(),
8623 out,
8624 trigger_stack,
8625 config,
8626 select_results,
8627 depth + 1,
8628 Some(&row),
8629 read_epoch,
8630 control,
8631 )?;
8632 if result == TriggerProgramOutcome::Ignore {
8633 return Ok(TriggerProgramOutcome::Ignore);
8634 }
8635 }
8636 }
8637 TriggerStep::DeleteWhere { table, conditions } => {
8638 let schema = self.table(table)?.lock().schema().clone();
8639 let snapshot = Snapshot::at(read_epoch);
8640 let handle = self.table(table)?;
8641 let rows = match control {
8642 Some(control) => {
8643 handle.lock().visible_rows_controlled(snapshot, control)?
8644 }
8645 None => handle.lock().visible_rows(snapshot)?,
8646 };
8647 let table_id = self.table_id(table)?;
8648 let mut to_delete = Vec::new();
8649 for (row_index, row) in rows.into_iter().enumerate() {
8650 commit_prepare_checkpoint(control, row_index)?;
8651 let image = TriggerRowImage::from_row(row.clone());
8652 let passes = conditions
8653 .iter()
8654 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
8655 .collect::<Result<Vec<_>>>()?
8656 .into_iter()
8657 .all(|b| b);
8658 if passes {
8659 to_delete.push((table_id, row.row_id));
8660 }
8661 }
8662 for (row_index, (table_id, row_id)) in to_delete.into_iter().enumerate() {
8663 commit_prepare_checkpoint(control, row_index)?;
8664 out.added
8665 .push((table_id, crate::txn::Staged::Delete(row_id)));
8666 out.added_stacks.push(trigger_stack.to_vec());
8667 }
8668 }
8669 TriggerStep::UpdateWhere {
8670 table,
8671 conditions,
8672 cells,
8673 } => {
8674 let schema = self.table(table)?.lock().schema().clone();
8675 let snapshot = Snapshot::at(read_epoch);
8676 let handle = self.table(table)?;
8677 let rows = match control {
8678 Some(control) => {
8679 handle.lock().visible_rows_controlled(snapshot, control)?
8680 }
8681 None => handle.lock().visible_rows(snapshot)?,
8682 };
8683 let table_id = self.table_id(table)?;
8684 let mut changed_columns =
8685 cells.iter().map(|cell| cell.column_id).collect::<Vec<_>>();
8686 changed_columns.sort_unstable();
8687 changed_columns.dedup();
8688 let mut to_update = Vec::new();
8689 for (row_index, row) in rows.into_iter().enumerate() {
8690 commit_prepare_checkpoint(control, row_index)?;
8691 let image = TriggerRowImage::from_row(row.clone());
8692 let passes = conditions
8693 .iter()
8694 .map(|cond| eval_trigger_condition(cond, event, &image, &schema))
8695 .collect::<Result<Vec<_>>>()?
8696 .into_iter()
8697 .all(|b| b);
8698 if passes {
8699 let new_cells = cells
8700 .iter()
8701 .map(|cell| {
8702 Ok((
8703 cell.column_id,
8704 eval_trigger_value(&cell.value, event, Some(&image))?,
8705 ))
8706 })
8707 .collect::<Result<Vec<_>>>()?;
8708 let mut merged = row.columns.clone();
8709 for (column_id, value) in new_cells {
8710 merged.insert(column_id, value);
8711 }
8712 to_update.push((table_id, row.row_id, merged));
8713 }
8714 }
8715 for (row_index, (table_id, row_id, merged)) in to_update.into_iter().enumerate()
8716 {
8717 commit_prepare_checkpoint(control, row_index)?;
8718 out.added.push((
8719 table_id,
8720 crate::txn::Staged::Update {
8721 row_id,
8722 new_row: merged.into_iter().collect(),
8723 changed_columns: changed_columns.clone(),
8724 },
8725 ));
8726 out.added_stacks.push(trigger_stack.to_vec());
8727 }
8728 }
8729 TriggerStep::Raise { action, message } => match action {
8730 TriggerRaiseAction::Ignore => return Ok(TriggerProgramOutcome::Ignore),
8731 TriggerRaiseAction::Abort
8732 | TriggerRaiseAction::Fail
8733 | TriggerRaiseAction::Rollback => {
8734 let message = eval_trigger_value(message, event, selected)?;
8735 return Err(MongrelError::TriggerValidation(format!(
8736 "trigger {:?} raised: {}; trigger stack: {}",
8737 trigger.name,
8738 trigger_message(message),
8739 Self::format_trigger_stack(trigger_stack)
8740 )));
8741 }
8742 },
8743 }
8744 }
8745 Ok(TriggerProgramOutcome::Continue)
8746 }
8747
8748 fn trigger_stack_for_indices(stacks: Option<&[Vec<String>]>, indices: &[usize]) -> Vec<String> {
8749 let Some(stacks) = stacks else {
8750 return Vec::new();
8751 };
8752 let mut out = Vec::new();
8753 for idx in indices {
8754 let Some(stack) = stacks.get(*idx) else {
8755 continue;
8756 };
8757 for name in stack {
8758 if !out.iter().any(|existing| existing == name) {
8759 out.push(name.clone());
8760 }
8761 }
8762 }
8763 out
8764 }
8765
8766 fn trigger_stack_with(stack: &[String], trigger_name: &str) -> Vec<String> {
8767 let mut out = stack.to_vec();
8768 out.push(trigger_name.to_string());
8769 out
8770 }
8771
8772 fn format_trigger_stack(stack: &[String]) -> String {
8773 if stack.is_empty() {
8774 "<root>".into()
8775 } else {
8776 stack.join(" -> ")
8777 }
8778 }
8779
8780 fn validate_constraints(
8795 &self,
8796 staging: &mut Vec<(u64, crate::txn::Staged)>,
8797 read_epoch: Epoch,
8798 control: Option<&crate::ExecutionControl>,
8799 ) -> Result<()> {
8800 use crate::constraint::{encode_composite_key, validate_checks, FkAction};
8801 use crate::memtable::Row;
8802 use crate::txn::Staged;
8803 use std::collections::HashSet;
8804
8805 commit_prepare_checkpoint(control, 0)?;
8806 let snapshot = Snapshot::at(read_epoch);
8807 let cat = self.catalog.read();
8808
8809 let live: Vec<(u64, &str, &crate::schema::Schema)> = cat
8811 .tables
8812 .iter()
8813 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
8814 .map(|e| (e.table_id, e.name.as_str(), &e.schema))
8815 .collect();
8816
8817 let any_constraints = live.iter().any(|(_, _, s)| !s.constraints.is_empty());
8819 if !any_constraints {
8820 return Ok(());
8821 }
8822
8823 let mut rows_cache: HashMap<u64, Vec<Row>> = HashMap::new();
8825 let mut load_rows = |table_id: u64| -> Result<Vec<Row>> {
8826 if let Some(r) = rows_cache.get(&table_id) {
8827 return Ok(r.clone());
8828 }
8829 let handle = self.table_by_id(table_id)?;
8830 let rows = match control {
8831 Some(control) => handle.lock().visible_rows_controlled(snapshot, control)?,
8832 None => handle.lock().visible_rows(snapshot)?,
8833 };
8834 rows_cache.insert(table_id, rows.clone());
8835 Ok(rows)
8836 };
8837
8838 let mut processed_updates = HashSet::new();
8843 type PendingUpdate = (usize, u64, crate::rowid::RowId, Vec<(u16, Value)>);
8844 let mut update_pass = 0_usize;
8845 loop {
8846 commit_prepare_checkpoint(control, update_pass)?;
8847 update_pass += 1;
8848 let updates: Vec<PendingUpdate> = staging
8849 .iter()
8850 .enumerate()
8851 .filter_map(|(index, (table_id, op))| match op {
8852 Staged::Update {
8853 row_id,
8854 new_row: cells,
8855 ..
8856 } if !processed_updates.contains(&index) => {
8857 Some((index, *table_id, *row_id, cells.clone()))
8858 }
8859 _ => None,
8860 })
8861 .collect();
8862 if updates.is_empty() {
8863 break;
8864 }
8865 let mut new_ops = Vec::new();
8866 for (update_index, (index, table_id, row_id, new_cells)) in
8867 updates.into_iter().enumerate()
8868 {
8869 commit_prepare_checkpoint(control, update_index)?;
8870 processed_updates.insert(index);
8871 let Some(tname) = live
8872 .iter()
8873 .find(|(id, _, _)| *id == table_id)
8874 .map(|(_, name, _)| *name)
8875 else {
8876 continue;
8877 };
8878 let Some(old_row) = self.table_by_id(table_id)?.lock().get(row_id, snapshot) else {
8879 continue;
8880 };
8881 let new_map: HashMap<u16, Value> = new_cells.iter().cloned().collect();
8882 for (child_id, _child_name, child_schema) in &live {
8883 for fk in &child_schema.constraints.foreign_keys {
8884 if fk.ref_table != tname {
8885 continue;
8886 }
8887 let Some(old_key) = encode_composite_key(&fk.ref_columns, &old_row.columns)
8888 else {
8889 continue;
8890 };
8891 if encode_composite_key(&fk.ref_columns, &new_map).as_deref()
8892 == Some(old_key.as_slice())
8893 {
8894 continue;
8895 }
8896 if fk.on_update == FkAction::Restrict {
8897 continue;
8898 }
8899 let child_rows = load_rows(*child_id)?;
8900 for (child_index, child) in child_rows.into_iter().enumerate() {
8901 commit_prepare_checkpoint(control, child_index)?;
8902 if encode_composite_key(&fk.columns, &child.columns).as_deref()
8903 != Some(old_key.as_slice())
8904 {
8905 continue;
8906 }
8907 if staging.iter().any(|(id, op)| {
8908 *id == *child_id
8909 && matches!(op, Staged::Delete(id) if *id == child.row_id)
8910 }) {
8911 continue;
8912 }
8913 let mut cells: Vec<(u16, Value)> = child
8914 .columns
8915 .iter()
8916 .map(|(column_id, value)| (*column_id, value.clone()))
8917 .collect();
8918 for (child_column, parent_column) in
8919 fk.columns.iter().zip(&fk.ref_columns)
8920 {
8921 cells.retain(|(column_id, _)| column_id != child_column);
8922 let value = match fk.on_update {
8923 FkAction::Cascade => {
8924 new_map.get(parent_column).cloned().unwrap_or(Value::Null)
8925 }
8926 FkAction::SetNull => Value::Null,
8927 FkAction::Restrict => {
8928 return Err(MongrelError::Other(
8929 "restricted foreign-key update reached cascade preparation"
8930 .into(),
8931 ));
8932 }
8933 };
8934 cells.push((*child_column, value));
8935 }
8936 cells.sort_by_key(|(column_id, _)| *column_id);
8937 if let Some(existing_index) = staging.iter().position(|(id, op)| {
8938 *id == *child_id
8939 && matches!(op, Staged::Update { row_id, .. } if *row_id == child.row_id)
8940 }) {
8941 if let Staged::Update {
8942 new_row: existing,
8943 changed_columns,
8944 ..
8945 } = &mut staging[existing_index].1 {
8946 changed_columns.extend(fk.columns.iter().copied());
8947 changed_columns.sort_unstable();
8948 changed_columns.dedup();
8949 if *existing != cells {
8950 *existing = cells;
8951 processed_updates.remove(&existing_index);
8952 }
8953 }
8954 } else {
8955 new_ops.push((
8956 *child_id,
8957 Staged::Update {
8958 row_id: child.row_id,
8959 new_row: cells,
8960 changed_columns: fk.columns.clone(),
8961 },
8962 ));
8963 }
8964 }
8965 }
8966 }
8967 }
8968 staging.extend(new_ops);
8969 }
8970
8971 let mut cascaded: HashSet<(u64, u64)> = HashSet::new();
8976 let mut cascade_pass = 0_usize;
8977 loop {
8978 commit_prepare_checkpoint(control, cascade_pass)?;
8979 cascade_pass += 1;
8980 let mut new_ops: Vec<(u64, Staged)> = Vec::new();
8981 let deletes: Vec<(u64, crate::rowid::RowId)> = staging
8982 .iter()
8983 .filter_map(|(t, op)| match op {
8984 Staged::Delete(rid) => Some((*t, *rid)),
8985 _ => None,
8986 })
8987 .collect();
8988 for (delete_index, (table_id, rid)) in deletes.into_iter().enumerate() {
8989 commit_prepare_checkpoint(control, delete_index)?;
8990 if !cascaded.insert((table_id, rid.0)) {
8991 continue;
8992 }
8993 let Some(tname) = live
8994 .iter()
8995 .find(|(t, _, _)| *t == table_id)
8996 .map(|(_, n, _)| *n)
8997 else {
8998 continue;
8999 };
9000 let parent_handle = self.table_by_id(table_id)?;
9001 let Some(parent_row) = parent_handle.lock().get(rid, snapshot) else {
9002 continue;
9003 };
9004 for (child_id, _child_name, child_schema) in &live {
9005 for fk in &child_schema.constraints.foreign_keys {
9006 if fk.ref_table != tname {
9007 continue;
9008 }
9009 let Some(parent_key) =
9010 encode_composite_key(&fk.ref_columns, &parent_row.columns)
9011 else {
9012 continue;
9013 };
9014 let key_preserved = staging.iter().any(|(t, op)| {
9023 if *t != table_id {
9024 return false;
9025 }
9026 let Staged::Put(cells) = op else {
9027 return false;
9028 };
9029 let map: HashMap<u16, crate::memtable::Value> =
9030 cells.iter().cloned().collect();
9031 encode_composite_key(&fk.ref_columns, &map).as_deref()
9032 == Some(parent_key.as_slice())
9033 });
9034 if key_preserved {
9035 continue;
9036 }
9037 match fk.on_delete {
9038 FkAction::Restrict => continue,
9039 FkAction::Cascade => {
9040 let child_rows = load_rows(*child_id)?;
9041 for (child_index, cr) in child_rows.iter().enumerate() {
9042 commit_prepare_checkpoint(control, child_index)?;
9043 if !cascaded.contains(&(*child_id, cr.row_id.0))
9044 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
9045 == Some(parent_key.as_slice())
9046 {
9047 new_ops.push((*child_id, Staged::Delete(cr.row_id)));
9048 }
9049 }
9050 }
9051 FkAction::SetNull => {
9052 let child_rows = load_rows(*child_id)?;
9053 for (child_index, cr) in child_rows.iter().enumerate() {
9054 commit_prepare_checkpoint(control, child_index)?;
9055 if !cascaded.contains(&(*child_id, cr.row_id.0))
9056 && encode_composite_key(&fk.columns, &cr.columns).as_deref()
9057 == Some(parent_key.as_slice())
9058 {
9059 let mut cells: Vec<(u16, crate::memtable::Value)> = cr
9062 .columns
9063 .iter()
9064 .map(|(k, v)| (*k, v.clone()))
9065 .collect();
9066 for cid in &fk.columns {
9067 cells.retain(|(k, _)| k != cid);
9068 cells.push((*cid, crate::memtable::Value::Null));
9069 }
9070 new_ops.push((
9071 *child_id,
9072 Staged::Update {
9073 row_id: cr.row_id,
9074 new_row: cells,
9075 changed_columns: fk.columns.clone(),
9076 },
9077 ));
9078 }
9079 }
9080 }
9081 }
9082 }
9083 }
9084 }
9085 if new_ops.is_empty() {
9086 break;
9087 }
9088 staging.extend(new_ops);
9089 }
9090
9091 let staged_deletes: HashSet<(u64, u64)> = staging
9095 .iter()
9096 .filter_map(|(t, op)| match op {
9097 Staged::Delete(rid) | Staged::Update { row_id: rid, .. } => Some((*t, rid.0)),
9098 _ => None,
9099 })
9100 .collect();
9101
9102 let mut seen_unique: HashSet<(u64, u16, Vec<u8>)> = HashSet::new();
9104
9105 for (operation_index, (table_id, op)) in staging.iter().enumerate() {
9107 commit_prepare_checkpoint(control, operation_index)?;
9108 let Some((_, tname, schema)) = live.iter().find(|(t, _, _)| t == table_id).copied()
9109 else {
9110 continue;
9111 };
9112 let cells_map: HashMap<u16, crate::memtable::Value>;
9113 match op {
9114 Staged::Put(cells) | Staged::Update { new_row: cells, .. } => {
9115 cells_map = cells.iter().cloned().collect();
9116
9117 if !schema.constraints.checks.is_empty() {
9119 validate_checks(&schema.constraints.checks, &cells_map)?;
9120 }
9121
9122 for uc in &schema.constraints.uniques {
9124 let Some(key) = encode_composite_key(&uc.columns, &cells_map) else {
9125 continue; };
9127 let marker = (*table_id, uc.id, key.clone());
9128 if !seen_unique.insert(marker) {
9129 return Err(MongrelError::Conflict(format!(
9130 "UNIQUE constraint '{}' on table '{tname}' violated within batch",
9131 uc.name
9132 )));
9133 }
9134 let rows = load_rows(*table_id)?;
9135 for (row_index, r) in rows.iter().enumerate() {
9136 commit_prepare_checkpoint(control, row_index)?;
9137 if staged_deletes.contains(&(*table_id, r.row_id.0)) {
9140 continue;
9141 }
9142 if let Some(theirs) = encode_composite_key(&uc.columns, &r.columns) {
9143 if theirs == key {
9144 return Err(MongrelError::Conflict(format!(
9145 "UNIQUE constraint '{}' on table '{tname}' violated",
9146 uc.name
9147 )));
9148 }
9149 }
9150 }
9151 }
9152
9153 for fk in &schema.constraints.foreign_keys {
9155 let Some(child_key) = encode_composite_key(&fk.columns, &cells_map) else {
9156 continue; };
9158 let Some(parent_id) = cat
9159 .tables
9160 .iter()
9161 .find(|t| t.name == fk.ref_table)
9162 .map(|t| t.table_id)
9163 else {
9164 return Err(MongrelError::InvalidArgument(format!(
9165 "FOREIGN KEY '{}' references unknown table '{}'",
9166 fk.name, fk.ref_table
9167 )));
9168 };
9169 let parent_rows = load_rows(parent_id)?;
9170 let mut found = false;
9171 for (row_index, r) in parent_rows.iter().enumerate() {
9172 commit_prepare_checkpoint(control, row_index)?;
9173 if staged_deletes.contains(&(parent_id, r.row_id.0)) {
9174 continue;
9175 }
9176 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &r.columns) {
9177 if pkey == child_key {
9178 found = true;
9179 break;
9180 }
9181 }
9182 }
9183 if !found {
9190 for (staged_index, (st_table, st_op)) in staging.iter().enumerate() {
9191 commit_prepare_checkpoint(control, staged_index)?;
9192 if *st_table != parent_id {
9193 continue;
9194 }
9195 if let Staged::Put(pcells)
9196 | Staged::Update {
9197 new_row: pcells, ..
9198 } = st_op
9199 {
9200 let pmap: HashMap<u16, crate::memtable::Value> =
9201 pcells.iter().cloned().collect();
9202 if let Some(pkey) = encode_composite_key(&fk.ref_columns, &pmap)
9203 {
9204 if pkey == child_key {
9205 found = true;
9206 break;
9207 }
9208 }
9209 }
9210 }
9211 }
9212 if !found {
9213 return Err(MongrelError::Conflict(format!(
9214 "FOREIGN KEY '{}' on table '{tname}' has no matching parent in '{}'",
9215 fk.name, fk.ref_table
9216 )));
9217 }
9218 }
9219
9220 if let Staged::Update { row_id, .. } = op {
9225 let parent_handle = self.table_by_id(*table_id)?;
9226 let Some(old_parent) = parent_handle.lock().get(*row_id, snapshot) else {
9227 continue;
9228 };
9229 for (child_id, child_name, child_schema) in &live {
9230 for fk in &child_schema.constraints.foreign_keys {
9231 if fk.ref_table != tname || fk.on_update != FkAction::Restrict {
9232 continue;
9233 }
9234 let Some(old_key) =
9235 encode_composite_key(&fk.ref_columns, &old_parent.columns)
9236 else {
9237 continue;
9238 };
9239 if encode_composite_key(&fk.ref_columns, &cells_map).as_deref()
9240 == Some(old_key.as_slice())
9241 {
9242 continue;
9243 }
9244 for (child_index, child) in
9245 load_rows(*child_id)?.into_iter().enumerate()
9246 {
9247 commit_prepare_checkpoint(control, child_index)?;
9248 if encode_composite_key(&fk.columns, &child.columns).as_deref()
9249 != Some(old_key.as_slice())
9250 {
9251 continue;
9252 }
9253 let replacement = staging.iter().find_map(|(id, op)| {
9254 if *id != *child_id {
9255 return None;
9256 }
9257 match op {
9258 Staged::Delete(id) if *id == child.row_id => Some(None),
9259 Staged::Update {
9260 row_id,
9261 new_row: cells,
9262 ..
9263 } if *row_id == child.row_id => {
9264 let map: HashMap<u16, Value> =
9265 cells.iter().cloned().collect();
9266 Some(encode_composite_key(&fk.columns, &map))
9267 }
9268 _ => None,
9269 }
9270 });
9271 if replacement.is_some_and(|key| {
9272 key.as_deref() != Some(old_key.as_slice())
9273 }) {
9274 continue;
9275 }
9276 return Err(MongrelError::Conflict(format!(
9277 "FOREIGN KEY '{}' on table '{child_name}' restricts update (parent key referenced)",
9278 fk.name
9279 )));
9280 }
9281 }
9282 }
9283 }
9284 }
9285 Staged::Delete(rid) => {
9286 let parent_handle = self.table_by_id(*table_id)?;
9290 let Some(parent_row) = parent_handle.lock().get(*rid, snapshot) else {
9291 continue;
9292 };
9293 for (child_id, child_name, child_schema) in &live {
9294 for fk in &child_schema.constraints.foreign_keys {
9295 if fk.ref_table != tname || fk.on_delete != FkAction::Restrict {
9296 continue;
9297 }
9298 let Some(parent_key) =
9299 encode_composite_key(&fk.ref_columns, &parent_row.columns)
9300 else {
9301 continue;
9302 };
9303 let child_rows = load_rows(*child_id)?;
9304 for (row_index, r) in child_rows.iter().enumerate() {
9305 commit_prepare_checkpoint(control, row_index)?;
9306 if staged_deletes.contains(&(*child_id, r.row_id.0)) {
9309 continue;
9310 }
9311 if let Some(ck) = encode_composite_key(&fk.columns, &r.columns) {
9312 if ck == parent_key {
9313 return Err(MongrelError::Conflict(format!(
9314 "FOREIGN KEY '{}' on table '{child_name}' restricts delete (parent referenced)",
9315 fk.name
9316 )));
9317 }
9318 }
9319 }
9320 }
9321 }
9322 }
9323 Staged::Truncate => {
9324 for (child_id, child_name, child_schema) in &live {
9328 for fk in &child_schema.constraints.foreign_keys {
9329 if fk.ref_table != tname {
9330 continue;
9331 }
9332 let child_rows = load_rows(*child_id)?;
9333 if child_rows
9334 .iter()
9335 .any(|r| encode_composite_key(&fk.columns, &r.columns).is_some())
9336 {
9337 return Err(MongrelError::Conflict(format!(
9338 "FOREIGN KEY '{}' on table '{child_name}' restricts truncate of '{tname}'",
9339 fk.name
9340 )));
9341 }
9342 }
9343 }
9344 }
9345 }
9346 }
9347 Ok(())
9348 }
9349
9350 fn validate_write_permissions(
9351 &self,
9352 staging: &[(u64, crate::txn::Staged)],
9353 principal: Option<&crate::auth::Principal>,
9354 control: Option<&crate::ExecutionControl>,
9355 ) -> Result<()> {
9356 commit_prepare_checkpoint(control, 0)?;
9357 if principal.is_none() && !self.auth_state.require_auth() {
9358 return Ok(());
9359 }
9360 let principal = principal.ok_or(MongrelError::AuthRequired)?;
9361 let needs = summarize_write_permissions(staging);
9362 let catalog = self.catalog.read();
9363
9364 if needs.values().any(|need| need.truncate) {
9365 self.require_for(Some(principal), &crate::auth::Permission::Admin)?;
9366 }
9367 for (need_index, (table_id, need)) in needs.into_iter().enumerate() {
9368 commit_prepare_checkpoint(control, need_index)?;
9369 let entry = catalog
9370 .tables
9371 .iter()
9372 .find(|entry| {
9373 entry.table_id == table_id
9374 && matches!(entry.state, TableState::Live | TableState::Building { .. })
9375 })
9376 .ok_or_else(|| {
9377 MongrelError::NotFound(format!(
9378 "live table {table_id} not found during write validation"
9379 ))
9380 })?;
9381 if matches!(entry.state, TableState::Building { .. }) {
9382 self.require_for(Some(principal), &crate::auth::Permission::Ddl)?;
9383 continue;
9384 }
9385 if need.insert {
9386 Self::require_columns_for_principal(
9387 &entry.name,
9388 &entry.schema,
9389 crate::auth::ColumnOperation::Insert,
9390 &need.insert_columns,
9391 principal,
9392 )?;
9393 }
9394 if need.update {
9395 Self::require_columns_for_principal(
9396 &entry.name,
9397 &entry.schema,
9398 crate::auth::ColumnOperation::Update,
9399 &need.update_columns,
9400 principal,
9401 )?;
9402 }
9403 if need.delete {
9404 self.require_for(
9405 Some(principal),
9406 &crate::auth::Permission::Delete {
9407 table: entry.name.clone(),
9408 },
9409 )?;
9410 }
9411 }
9412 Ok(())
9413 }
9414
9415 fn validate_security_writes(
9416 &self,
9417 staging: &[(u64, crate::txn::Staged)],
9418 read_epoch: Epoch,
9419 explicit_principal: Option<&crate::auth::Principal>,
9420 control: Option<&crate::ExecutionControl>,
9421 ) -> Result<()> {
9422 commit_prepare_checkpoint(control, 0)?;
9423 use crate::security::PolicyCommand;
9424 use crate::txn::Staged;
9425
9426 let catalog = self.catalog.read();
9427 if catalog.security.rls_tables.is_empty() {
9428 return Ok(());
9429 }
9430 let security = catalog.security.clone();
9431 let table_names = catalog
9432 .tables
9433 .iter()
9434 .filter(|entry| matches!(entry.state, TableState::Live))
9435 .map(|entry| (entry.table_id, entry.name.clone()))
9436 .collect::<HashMap<_, _>>();
9437 drop(catalog);
9438 if !staging.iter().any(|(table_id, _)| {
9439 table_names
9440 .get(table_id)
9441 .is_some_and(|table| security.rls_enabled(table))
9442 }) {
9443 return Ok(());
9444 }
9445 let principal = explicit_principal.ok_or(MongrelError::AuthRequired)?;
9446
9447 for (operation_index, (table_id, operation)) in staging.iter().enumerate() {
9448 commit_prepare_checkpoint(control, operation_index)?;
9449 let Some(table) = table_names.get(table_id) else {
9450 continue;
9451 };
9452 if !security.rls_enabled(table) || principal.is_admin {
9453 continue;
9454 }
9455 let denied = |command| MongrelError::PermissionDenied {
9456 required: match command {
9457 PolicyCommand::Insert => crate::auth::Permission::Insert {
9458 table: table.clone(),
9459 },
9460 PolicyCommand::Update => crate::auth::Permission::Update {
9461 table: table.clone(),
9462 },
9463 PolicyCommand::Delete | PolicyCommand::All | PolicyCommand::Select => {
9464 crate::auth::Permission::Delete {
9465 table: table.clone(),
9466 }
9467 }
9468 },
9469 principal: principal.username.clone(),
9470 };
9471 match operation {
9472 Staged::Put(cells) => {
9473 let mut row = crate::memtable::Row::new(RowId(0), Epoch(read_epoch.0));
9474 row.columns.extend(cells.iter().cloned());
9475 if !security.row_allowed(table, PolicyCommand::Insert, &row, principal, true) {
9476 return Err(denied(PolicyCommand::Insert));
9477 }
9478 }
9479 Staged::Update {
9480 row_id,
9481 new_row: cells,
9482 ..
9483 } => {
9484 let old = self
9485 .table_by_id(*table_id)?
9486 .lock()
9487 .get(*row_id, Snapshot::at(read_epoch))
9488 .ok_or_else(|| {
9489 MongrelError::NotFound(format!("row {} not found", row_id.0))
9490 })?;
9491 if !security.row_allowed(table, PolicyCommand::Update, &old, principal, false) {
9492 return Err(denied(PolicyCommand::Update));
9493 }
9494 let mut new = crate::memtable::Row::new(*row_id, Epoch(read_epoch.0));
9495 new.columns.extend(cells.iter().cloned());
9496 if !security.row_allowed(table, PolicyCommand::Update, &new, principal, true) {
9497 return Err(denied(PolicyCommand::Update));
9498 }
9499 }
9500 Staged::Delete(row_id) => {
9501 let old = self
9502 .table_by_id(*table_id)?
9503 .lock()
9504 .get(*row_id, Snapshot::at(read_epoch))
9505 .ok_or_else(|| {
9506 MongrelError::NotFound(format!("row {} not found", row_id.0))
9507 })?;
9508 if !security.row_allowed(table, PolicyCommand::Delete, &old, principal, false) {
9509 return Err(denied(PolicyCommand::Delete));
9510 }
9511 }
9512 Staged::Truncate => return Err(denied(PolicyCommand::Delete)),
9513 }
9514 }
9515 Ok(())
9516 }
9517
9518 #[allow(clippy::too_many_arguments)]
9525 pub(crate) fn commit_transaction_with_external_states(
9526 &self,
9527 txn_id: u64,
9528 read_epoch: Epoch,
9529 staging: Vec<(u64, crate::txn::Staged)>,
9530 external_states: Vec<(String, Vec<u8>)>,
9531 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
9532 security_principal: Option<crate::auth::Principal>,
9533 principal_catalog_bound: bool,
9534 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9535 ) -> Result<(Epoch, Vec<RowId>)> {
9536 self.commit_transaction_with_external_states_inner(
9537 txn_id,
9538 read_epoch,
9539 staging,
9540 external_states,
9541 materialized_view_updates,
9542 security_principal,
9543 principal_catalog_bound,
9544 external_trigger_bridge,
9545 None,
9546 None,
9547 )
9548 }
9549
9550 #[allow(clippy::too_many_arguments)]
9551 pub(crate) fn commit_transaction_with_external_states_controlled(
9552 &self,
9553 txn_id: u64,
9554 read_epoch: Epoch,
9555 staging: Vec<(u64, crate::txn::Staged)>,
9556 external_states: Vec<(String, Vec<u8>)>,
9557 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
9558 security_principal: Option<crate::auth::Principal>,
9559 principal_catalog_bound: bool,
9560 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9561 control: &crate::ExecutionControl,
9562 before_commit: &mut dyn FnMut() -> Result<()>,
9563 ) -> Result<(Epoch, Vec<RowId>)> {
9564 self.commit_transaction_with_external_states_inner(
9565 txn_id,
9566 read_epoch,
9567 staging,
9568 external_states,
9569 materialized_view_updates,
9570 security_principal,
9571 principal_catalog_bound,
9572 external_trigger_bridge,
9573 Some(control),
9574 Some(before_commit),
9575 )
9576 }
9577
9578 #[allow(clippy::too_many_arguments)]
9579 fn commit_transaction_with_external_states_inner(
9580 &self,
9581 txn_id: u64,
9582 read_epoch: Epoch,
9583 mut staging: Vec<(u64, crate::txn::Staged)>,
9584 external_states: Vec<(String, Vec<u8>)>,
9585 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
9586 mut security_principal: Option<crate::auth::Principal>,
9587 principal_catalog_bound: bool,
9588 external_trigger_bridge: Option<&dyn ExternalTriggerBridge>,
9589 control: Option<&crate::ExecutionControl>,
9590 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
9591 ) -> Result<(Epoch, Vec<RowId>)> {
9592 use crate::memtable::Row;
9593 use crate::txn::{Staged, StagedOp, WriteKey};
9594 use crate::wal::Op;
9595 use std::collections::hash_map::DefaultHasher;
9596 use std::hash::{Hash, Hasher};
9597 use std::sync::atomic::Ordering;
9598
9599 if txn_id == crate::wal::SYSTEM_TXN_ID {
9600 return Err(MongrelError::Full(
9601 "per-open transaction id namespace exhausted; reopen the database".into(),
9602 ));
9603 }
9604 if self.read_only {
9605 return Err(MongrelError::ReadOnlyReplica);
9606 }
9607 commit_prepare_checkpoint(control, 0)?;
9608 let observed_security_version = self.security_coordinator.version.load(Ordering::Acquire);
9609 self.refresh_security_catalog_if_stale(observed_security_version)?;
9610 let trigger_binding = trigger_catalog_binding(&self.catalog.read());
9611 if self.auth_state.require_auth() && security_principal.is_none() {
9612 return Err(MongrelError::AuthRequired);
9613 }
9614 {
9615 let catalog = self.catalog.read();
9616 if catalog.require_auth
9617 || principal_catalog_bound
9618 || security_principal
9619 .as_ref()
9620 .is_some_and(|principal| principal.user_id != 0)
9621 {
9622 let principal = security_principal
9623 .as_ref()
9624 .ok_or(MongrelError::AuthRequired)?;
9625 security_principal =
9626 Self::resolve_bound_principal_from_catalog(&catalog, principal);
9627 if security_principal.is_none() {
9628 return Err(MongrelError::AuthRequired);
9629 }
9630 }
9631 }
9632 let _replication_guard = self.replication_barrier.read();
9633 if self.poisoned.load(Ordering::Relaxed) {
9634 return Err(MongrelError::Other(
9635 "database poisoned by fsync error".into(),
9636 ));
9637 }
9638 let mut external_states = dedup_external_states(external_states);
9639 if !external_states.is_empty() {
9640 let cat = self.catalog.read();
9641 for (name, _) in &external_states {
9642 if !cat.external_tables.iter().any(|entry| entry.name == *name) {
9643 return Err(MongrelError::NotFound(format!(
9644 "external table {name:?} not found"
9645 )));
9646 }
9647 }
9648 }
9649 let prepared_materialized_views = {
9650 let mut deduplicated = HashMap::new();
9651 for (definition_index, definition) in materialized_view_updates.into_iter().enumerate()
9652 {
9653 commit_prepare_checkpoint(control, definition_index)?;
9654 if definition.name.is_empty() || definition.query.trim().is_empty() {
9655 return Err(MongrelError::InvalidArgument(
9656 "materialized view name and query must not be empty".into(),
9657 ));
9658 }
9659 deduplicated.insert(definition.name.clone(), definition);
9660 }
9661 let catalog = self.catalog.read();
9662 let mut prepared = Vec::with_capacity(deduplicated.len());
9663 for (definition_index, definition) in deduplicated.into_values().enumerate() {
9664 commit_prepare_checkpoint(control, definition_index)?;
9665 let table_id = catalog
9666 .live(&definition.name)
9667 .ok_or_else(|| {
9668 MongrelError::NotFound(format!(
9669 "materialized view table {:?} not found",
9670 definition.name
9671 ))
9672 })?
9673 .table_id;
9674 prepared.push((table_id, definition));
9675 }
9676 prepared.sort_by(|left, right| left.1.name.cmp(&right.1.name));
9677 prepared
9678 };
9679
9680 self.fill_auto_increment_for_staging(&mut staging, control)?;
9683 self.expand_table_triggers(
9684 &mut staging,
9685 read_epoch,
9686 external_trigger_bridge,
9687 &mut external_states,
9688 control,
9689 )?;
9690 self.fill_auto_increment_for_staging(&mut staging, control)?;
9691 external_states = dedup_external_states(external_states);
9692 let expected_external_generations = {
9693 let catalog = self.catalog.read();
9694 let mut generations = HashMap::with_capacity(external_states.len());
9695 for (name, _) in &external_states {
9696 let entry = catalog
9697 .external_tables
9698 .iter()
9699 .find(|entry| entry.name == *name)
9700 .ok_or_else(|| {
9701 MongrelError::NotFound(format!("external table {name:?} not found"))
9702 })?;
9703 generations.insert(name.clone(), entry.created_epoch);
9704 }
9705 generations
9706 };
9707
9708 self.validate_constraints(&mut staging, read_epoch, control)?;
9713 self.validate_write_permissions(&staging, security_principal.as_ref(), control)?;
9714 self.validate_security_writes(&staging, read_epoch, security_principal.as_ref(), control)?;
9715 let mut normalized = Vec::with_capacity(staging.len() * 2);
9716 for (staged_index, (table_id, op)) in staging.into_iter().enumerate() {
9717 commit_prepare_checkpoint(control, staged_index)?;
9718 match op {
9719 crate::txn::Staged::Update {
9720 row_id,
9721 new_row: cells,
9722 ..
9723 } => {
9724 normalized.push((table_id, crate::txn::Staged::Delete(row_id)));
9725 normalized.push((table_id, crate::txn::Staged::Put(cells)));
9726 }
9727 op => normalized.push((table_id, op)),
9728 }
9729 }
9730 staging = normalized;
9731 let has_changes = !staging.is_empty()
9732 || !external_states.is_empty()
9733 || !prepared_materialized_views.is_empty();
9734 let truncated_tables: HashSet<u64> = staging
9735 .iter()
9736 .filter_map(|(table_id, op)| matches!(op, Staged::Truncate).then_some(*table_id))
9737 .collect();
9738
9739 let write_keys = {
9740 let cat = self.catalog.read();
9741 let mut keys: Vec<WriteKey> = Vec::new();
9742 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
9743 commit_prepare_checkpoint(control, staged_index)?;
9744 match staged {
9745 Staged::Put(cells) => {
9746 if let Some(entry) = cat.tables.iter().find(|t| t.table_id == *table_id) {
9747 for col in &entry.schema.columns {
9748 if col.flags.contains(crate::schema::ColumnFlags::PRIMARY_KEY) {
9749 if let Some((_, val)) =
9750 cells.iter().find(|(id, _)| *id == col.id)
9751 {
9752 let mut h = DefaultHasher::new();
9753 val.encode_key().hash(&mut h);
9754 keys.push(WriteKey::Unique {
9755 table_id: *table_id,
9756 index_id: 0,
9757 key_hash: h.finish(),
9758 });
9759 }
9760 }
9761 }
9762 for uc in &entry.schema.constraints.uniques {
9769 if let Some(key_bytes) = crate::constraint::encode_composite_key(
9770 &uc.columns,
9771 &cells.iter().cloned().collect(),
9772 ) {
9773 let mut h = DefaultHasher::new();
9774 key_bytes.hash(&mut h);
9775 keys.push(WriteKey::Unique {
9776 table_id: *table_id,
9777 index_id: uc.id | 0x8000,
9778 key_hash: h.finish(),
9779 });
9780 }
9781 }
9782 }
9783 }
9784 Staged::Delete(rid) => keys.push(WriteKey::Row {
9785 table_id: *table_id,
9786 row_id: rid.0,
9787 }),
9788 Staged::Truncate => keys.push(WriteKey::Table {
9789 table_id: *table_id,
9790 }),
9791 Staged::Update { .. } => {
9792 return Err(MongrelError::Other(
9793 "transaction contains an unnormalized update during preparation".into(),
9794 ));
9795 }
9796 }
9797 }
9798 for (external_index, (name, _)) in external_states.iter().enumerate() {
9799 commit_prepare_checkpoint(control, external_index)?;
9800 let mut h = DefaultHasher::new();
9801 name.hash(&mut h);
9802 keys.push(WriteKey::Unique {
9803 table_id: EXTERNAL_TABLE_ID,
9804 index_id: 0,
9805 key_hash: h.finish(),
9806 });
9807 }
9808 keys
9809 };
9810
9811 let min_active = self.active_txns.min_read_epoch();
9813 if min_active < u64::MAX {
9814 self.conflicts.prune_below(Epoch(min_active));
9815 }
9816
9817 if self.conflicts.conflicts(&write_keys, read_epoch) {
9821 return Err(MongrelError::Conflict(
9822 "write-write conflict (pre-validate, first-committer-wins)".into(),
9823 ));
9824 }
9825 let pre_validate_version = self.conflicts.version();
9826
9827 let mut spilled: Vec<SpilledRun> = Vec::new();
9831 let mut spilled_tables: std::collections::HashSet<u64> = std::collections::HashSet::new();
9832 let mut spill_guard: Option<crate::retention::SpillGuard> = None;
9836 {
9837 let mut table_bytes: HashMap<u64, u64> = HashMap::new();
9838 let mut put_indexes: HashMap<u64, Vec<usize>> = HashMap::new();
9839 for (staged_index, (table_id, staged)) in staging.iter().enumerate() {
9840 commit_prepare_checkpoint(control, staged_index)?;
9841 if let Staged::Put(cells) = staged {
9842 let bytes = cells.iter().fold(32_u64, |bytes, (_, value)| {
9843 bytes.saturating_add(value.estimated_bytes())
9844 });
9845 let table_bytes = table_bytes.entry(*table_id).or_default();
9846 *table_bytes = table_bytes.saturating_add(bytes);
9847 put_indexes.entry(*table_id).or_default().push(staged_index);
9848 }
9849 }
9850 let tables = self.tables.read();
9851 for (table_index, (&table_id, &bytes)) in table_bytes.iter().enumerate() {
9852 commit_prepare_checkpoint(control, table_index)?;
9853 if bytes
9854 <= self
9855 .spill_threshold
9856 .load(std::sync::atomic::Ordering::Relaxed)
9857 {
9858 continue;
9859 }
9860 let Some(handle) = tables.get(&table_id) else {
9861 continue;
9862 };
9863 spill_guard.get_or_insert_with(|| self.active_spills.register(txn_id));
9864 let mut t = handle.lock();
9865 let tdir = t.table_dir().to_path_buf();
9866 let txn_dir = tdir.join("_txn").join(txn_id.to_string());
9867 std::fs::create_dir_all(&txn_dir)?;
9868 let run_id = t.alloc_run_id()? as u128;
9869 let pending_path = txn_dir.join(format!("r-{run_id}.sr"));
9870 let final_path = t.run_path(run_id as u64);
9871
9872 let mut rows: Vec<Row> = Vec::new();
9873 for (put_index, staged_index) in put_indexes[&table_id].iter().enumerate() {
9874 commit_prepare_checkpoint(control, put_index)?;
9875 let Staged::Put(cells) = &mut staging[*staged_index].1 else {
9876 return Err(MongrelError::Other(
9877 "transaction put index no longer references a put".into(),
9878 ));
9879 };
9880 t.validate_cells_not_null(cells)?;
9881 let row_id = t.alloc_row_id()?;
9882 let mut row = Row::new(row_id, Epoch(0));
9883 row.columns.extend(std::mem::take(cells));
9884 rows.push(row);
9885 }
9886 let schema = t.schema_ref().clone();
9887 let kek = t.kek_ref().cloned();
9888 let specs = t.indexable_column_specs();
9889 drop(t);
9890
9891 let mut writer = crate::sorted_run::RunWriter::new(&schema, run_id, Epoch(0), 0)
9892 .uniform_epoch(true);
9893 if let Some(ref kek) = kek {
9894 writer = writer.with_encryption(kek.as_ref(), specs);
9895 }
9896 commit_prepare_checkpoint(control, 0)?;
9897 let header = writer.write(&pending_path, &rows)?;
9898 commit_prepare_checkpoint(control, 0)?;
9899 let row_count = header.row_count;
9900 let min_rid = rows.first().map(|r| r.row_id.0).unwrap_or(0);
9901 let max_rid = rows.last().map(|r| r.row_id.0).unwrap_or(0);
9902
9903 spilled.push(SpilledRun {
9904 table_id,
9905 run_id,
9906 pending_path,
9907 final_path,
9908 rows,
9909 row_count,
9910 min_rid,
9911 max_rid,
9912 content_hash: header.content_hash,
9913 });
9914 spilled_tables.insert(table_id);
9915 }
9916 }
9917
9918 if spill_guard.is_some() {
9920 if let Some(hook) = self.spill_hook.lock().as_ref() {
9921 hook();
9922 }
9923 }
9924
9925 let mut prebuilt: Vec<Option<Row>> = std::iter::repeat_with(|| None)
9936 .take(staging.len())
9937 .collect();
9938 let mut delete_images: Vec<Option<Row>> = std::iter::repeat_with(|| None)
9939 .take(staging.len())
9940 .collect();
9941 {
9942 let mut indexes_by_table: HashMap<u64, Vec<usize>> = HashMap::new();
9943 for (index, (table_id, staged)) in staging.iter().enumerate() {
9944 commit_prepare_checkpoint(control, index)?;
9945 if matches!(staged, Staged::Delete(_))
9946 || matches!(staged, Staged::Put(_) if !spilled_tables.contains(table_id))
9947 {
9948 indexes_by_table.entry(*table_id).or_default().push(index);
9949 }
9950 }
9951 let tables = self.tables.read();
9952 for (table_index, (table_id, indexes)) in indexes_by_table.into_iter().enumerate() {
9953 commit_prepare_checkpoint(control, table_index)?;
9954 let handle = tables.get(&table_id).ok_or_else(|| {
9955 MongrelError::NotFound(format!("table {table_id} not mounted"))
9956 })?;
9957 #[cfg(test)]
9958 PREBUILD_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
9959 let mut t = handle.lock();
9960 for (prepare_index, index) in indexes.into_iter().enumerate() {
9961 commit_prepare_checkpoint(control, prepare_index)?;
9962 match &staging[index].1 {
9963 Staged::Put(cells) if !spilled_tables.contains(&table_id) => {
9964 t.validate_cells_not_null(cells)?;
9965 let mut row = Row::new(t.alloc_row_id()?, Epoch(0));
9966 for (column, value) in cells {
9967 row.columns.insert(*column, value.clone());
9968 }
9969 prebuilt[index] = Some(row);
9970 }
9971 Staged::Delete(row_id) => {
9972 delete_images[index] = t.get(*row_id, Snapshot::at(read_epoch));
9973 }
9974 Staged::Put(_) | Staged::Truncate => {}
9975 Staged::Update { .. } => {
9976 return Err(MongrelError::Other(
9977 "transaction contains an unnormalized update during row preparation"
9978 .into(),
9979 ));
9980 }
9981 }
9982 }
9983 }
9984 }
9985
9986 let prepared_table_handles = {
9990 let table_ids: HashSet<u64> = staging.iter().map(|(table_id, _)| *table_id).collect();
9991 let put_table_ids: HashSet<u64> = staging
9992 .iter()
9993 .filter_map(|(table_id, staged)| {
9994 matches!(staged, Staged::Put(_)).then_some(*table_id)
9995 })
9996 .collect();
9997 let tables = self.tables.read();
9998 let mut handles = HashMap::with_capacity(table_ids.len());
9999 for (table_index, table_id) in table_ids.into_iter().enumerate() {
10000 commit_prepare_checkpoint(control, table_index)?;
10001 let handle = tables.get(&table_id).ok_or_else(|| {
10002 MongrelError::NotFound(format!("table {table_id} not mounted"))
10003 })?;
10004 if put_table_ids.contains(&table_id) {
10005 match control {
10006 Some(control) => {
10007 handle.lock().prepare_durable_publish_controlled(control)?
10008 }
10009 None => handle.lock().prepare_durable_publish()?,
10010 }
10011 }
10012 handles.insert(table_id, handle.clone());
10013 }
10014 handles
10015 };
10016
10017 let mut prepared_run_links = PreparedRunLinks::prepare(&spilled)?;
10021
10022 let mut spilled_row_ids: HashMap<u64, VecDeque<RowId>> = spilled
10023 .iter()
10024 .map(|run| {
10025 (
10026 run.table_id,
10027 run.rows.iter().map(|row| row.row_id).collect(),
10028 )
10029 })
10030 .collect();
10031 let committed_row_ids = staging
10032 .iter()
10033 .enumerate()
10034 .filter_map(|(index, (table_id, staged))| {
10035 if !matches!(staged, Staged::Put(_)) {
10036 return None;
10037 }
10038 prebuilt[index].as_ref().map(|row| row.row_id).or_else(|| {
10039 spilled_row_ids
10040 .get_mut(table_id)
10041 .and_then(VecDeque::pop_front)
10042 })
10043 })
10044 .collect();
10045
10046 let mut prepared_external = Vec::with_capacity(external_states.len());
10047 for (external_index, (name, state)) in external_states.iter().enumerate() {
10048 commit_prepare_checkpoint(control, external_index)?;
10049 let pending = prepare_external_state_file(&self.root, name, state, txn_id)?;
10050 prepared_external.push((name.clone(), state.clone(), pending));
10051 }
10052
10053 let added_runs: Vec<crate::wal::AddedRun> = spilled
10055 .iter()
10056 .map(|s| crate::wal::AddedRun {
10057 table_id: s.table_id,
10058 run_id: s.run_id,
10059 row_count: s.row_count,
10060 level: 0,
10061 min_row_id: s.min_rid,
10062 max_row_id: s.max_rid,
10063 content_hash: s.content_hash,
10064 })
10065 .collect();
10066 if let Some(hook) = self.catalog_commit_hook.lock().as_ref() {
10067 hook();
10068 }
10069 let security_guard = self.security_coordinator.gate.read();
10073 if self.security_coordinator.version.load(Ordering::Acquire) != observed_security_version {
10074 return Err(MongrelError::Conflict(
10075 "security policy changed during write".into(),
10076 ));
10077 }
10078 if spill_guard.is_some() {
10079 if let Some(hook) = self.security_commit_hook.lock().as_ref() {
10080 hook();
10081 }
10082 }
10083 let commit_guard = self.commit_lock.lock();
10084 let catalog_generation_result = (|| {
10085 {
10086 let catalog = self.catalog.read();
10087 for table_id in prepared_table_handles.keys() {
10088 let is_current = catalog.tables.iter().any(|entry| {
10089 entry.table_id == *table_id
10090 && matches!(entry.state, TableState::Live | TableState::Building { .. })
10091 });
10092 if !is_current {
10093 return Err(MongrelError::Conflict(format!(
10094 "table {table_id} changed during transaction preparation"
10095 )));
10096 }
10097 }
10098 for (name, created_epoch) in &expected_external_generations {
10099 let current = catalog
10100 .external_tables
10101 .iter()
10102 .find(|entry| entry.name == *name)
10103 .map(|entry| entry.created_epoch);
10104 if current != Some(*created_epoch) {
10105 return Err(MongrelError::Conflict(format!(
10106 "external table {name:?} changed during transaction preparation"
10107 )));
10108 }
10109 }
10110 for (table_id, definition) in &prepared_materialized_views {
10111 let current = catalog.live(&definition.name).map(|entry| entry.table_id);
10112 if current != Some(*table_id) {
10113 return Err(MongrelError::Conflict(format!(
10114 "materialized view {:?} changed during transaction preparation",
10115 definition.name
10116 )));
10117 }
10118 }
10119 if trigger_catalog_binding(&catalog) != trigger_binding {
10120 return Err(MongrelError::Conflict(
10121 "trigger or referenced table generation changed during transaction preparation"
10122 .into(),
10123 ));
10124 }
10125 }
10126 let tables = self.tables.read();
10127 for (table_id, prepared) in &prepared_table_handles {
10128 if !tables
10129 .get(table_id)
10130 .is_some_and(|current| current.ptr_eq(prepared))
10131 {
10132 return Err(MongrelError::Conflict(format!(
10133 "table {table_id} mount changed during transaction preparation"
10134 )));
10135 }
10136 }
10137 Ok(())
10138 })();
10139 if let Err(error) = catalog_generation_result {
10140 drop(commit_guard);
10141 for (_, _, pending) in &prepared_external {
10142 let _ = std::fs::remove_file(pending);
10143 }
10144 return Err(error);
10145 }
10146 let new_epoch = self.epoch.assigned().next();
10150 let mut spilled_wal_bytes = 0;
10151 let mut spilled_wal_records = Vec::<(u64, Op)>::new();
10152 let spill_prepare = (|| {
10153 for run in &mut spilled {
10154 for row in &mut run.rows {
10155 row.committed_epoch = new_epoch;
10156 }
10157 for rows in encode_spilled_row_chunks(
10158 &run.rows,
10159 &mut spilled_wal_bytes,
10160 SPILLED_WAL_TOTAL_MAX_BYTES,
10161 control,
10162 )? {
10163 spilled_wal_records.push((
10164 run.table_id,
10165 Op::SpilledRows {
10166 table_id: run.table_id,
10167 rows,
10168 },
10169 ));
10170 }
10171 }
10172 Result::<()>::Ok(())
10173 })();
10174 if let Err(error) = spill_prepare {
10175 for (_, _, pending) in &prepared_external {
10176 let _ = std::fs::remove_file(pending);
10177 }
10178 return Err(error);
10179 }
10180 let (new_epoch, mut _epoch_guard, applies, committed_materialized_views, commit_seq) = {
10181 let mut wal = self.shared_wal.lock();
10182
10183 if self.conflicts.version() != pre_validate_version
10188 && self.conflicts.conflicts(&write_keys, read_epoch)
10189 {
10190 drop(wal);
10193 for (_, _, pending) in &prepared_external {
10194 let _ = std::fs::remove_file(pending);
10195 }
10196 return Err(MongrelError::Conflict(
10197 "write-write conflict (sequencer delta re-check)".into(),
10198 ));
10199 }
10200
10201 if let Some(control) = control {
10202 if let Err(error) = control.checkpoint() {
10203 drop(wal);
10204 for (_, _, pending) in &prepared_external {
10205 let _ = std::fs::remove_file(pending);
10206 }
10207 return Err(error);
10208 }
10209 }
10210 let mut applies = Vec::<TableApplyBatch>::new();
10211 let mut apply_indexes = HashMap::<u64, usize>::new();
10212 let mut committed_materialized_views = Vec::new();
10213 let mut wal_records = spilled_wal_records;
10214
10215 let mut index = 0;
10216 while index < staging.len() {
10217 let table_id = staging[index].0;
10218 let handle = prepared_table_handles
10219 .get(&table_id)
10220 .cloned()
10221 .ok_or_else(|| {
10222 MongrelError::NotFound(format!("table {table_id} not prepared"))
10223 })?;
10224 let batch_index = *apply_indexes.entry(table_id).or_insert_with(|| {
10225 let index = applies.len();
10226 applies.push(TableApplyBatch {
10227 table_id,
10228 handle,
10229 ops: Vec::new(),
10230 });
10231 index
10232 });
10233
10234 if spilled_tables.contains(&table_id) && matches!(&staging[index].1, Staged::Put(_))
10237 {
10238 index += 1;
10239 continue;
10240 }
10241
10242 match &staging[index].1 {
10243 Staged::Put(_) => {
10244 let mut rows = Vec::new();
10245 while index < staging.len()
10246 && staging[index].0 == table_id
10247 && matches!(&staging[index].1, Staged::Put(_))
10248 {
10249 let mut row = prebuilt[index].take().ok_or_else(|| {
10250 MongrelError::Other(
10251 "transaction prepare lost a prebuilt put row".into(),
10252 )
10253 })?;
10254 row.committed_epoch = new_epoch;
10255 rows.push(row);
10256 index += 1;
10257 }
10258 let payload = bincode::serialize(&rows)
10259 .map_err(|e| MongrelError::Other(format!("row serialize: {e}")))?;
10260 wal_records.push((
10261 table_id,
10262 Op::Put {
10263 table_id,
10264 rows: payload,
10265 },
10266 ));
10267 applies[batch_index].ops.push(StagedOp::Put(rows));
10268 }
10269 Staged::Delete(_) => {
10270 let mut row_ids = Vec::new();
10271 while index < staging.len()
10272 && staging[index].0 == table_id
10273 && matches!(&staging[index].1, Staged::Delete(_))
10274 {
10275 let Staged::Delete(row_id) = &staging[index].1 else {
10276 return Err(MongrelError::Other(
10277 "transaction delete batch changed during WAL preparation"
10278 .into(),
10279 ));
10280 };
10281 if let Some(before) = &delete_images[index] {
10282 wal_records.push((
10283 table_id,
10284 Op::BeforeImage {
10285 table_id,
10286 row_id: *row_id,
10287 row: bincode::serialize(before).map_err(|error| {
10288 MongrelError::Other(format!(
10289 "before-image serialize: {error}"
10290 ))
10291 })?,
10292 },
10293 ));
10294 }
10295 row_ids.push(*row_id);
10296 index += 1;
10297 }
10298 wal_records.push((
10299 table_id,
10300 Op::Delete {
10301 table_id,
10302 row_ids: row_ids.clone(),
10303 },
10304 ));
10305 applies[batch_index].ops.push(StagedOp::Delete(row_ids));
10306 }
10307 Staged::Truncate => {
10308 wal_records.push((table_id, Op::TruncateTable { table_id }));
10309 applies[batch_index].ops.push(StagedOp::Truncate);
10310 index += 1;
10311 }
10312 Staged::Update { .. } => {
10313 return Err(MongrelError::Other(
10314 "transaction contains an unnormalized update at the sequencer".into(),
10315 ));
10316 }
10317 }
10318 }
10319
10320 for (name, state, _) in &prepared_external {
10321 wal_records.push((
10322 EXTERNAL_TABLE_ID,
10323 Op::ExternalTableState {
10324 name: name.clone(),
10325 state: state.clone(),
10326 },
10327 ));
10328 }
10329
10330 for (table_id, definition) in &prepared_materialized_views {
10331 let mut definition = definition.clone();
10332 definition.last_refresh_epoch = new_epoch.0;
10333 wal_records.push((
10334 *table_id,
10335 Op::Ddl(crate::wal::DdlOp::SetMaterializedView {
10336 name: definition.name.clone(),
10337 definition_json: crate::wal::DdlOp::encode_materialized_view(&definition)?,
10338 }),
10339 ));
10340 committed_materialized_views.push(definition);
10341 }
10342 if !committed_materialized_views.is_empty() {
10343 let mut next_catalog = self.catalog.read().clone();
10344 for definition in &committed_materialized_views {
10345 if let Some(existing) = next_catalog
10346 .materialized_views
10347 .iter_mut()
10348 .find(|existing| existing.name == definition.name)
10349 {
10350 *existing = definition.clone();
10351 } else {
10352 next_catalog.materialized_views.push(definition.clone());
10353 }
10354 }
10355 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
10356 wal_records.push((
10357 WAL_TABLE_ID,
10358 Op::Ddl(crate::wal::DdlOp::CatalogSnapshot {
10359 catalog_json: crate::wal::DdlOp::encode_catalog(&next_catalog)?,
10360 }),
10361 ));
10362 }
10363
10364 if let Some(control) = control {
10365 if let Err(error) = control.checkpoint() {
10366 drop(wal);
10367 for (_, _, pending) in &prepared_external {
10368 let _ = std::fs::remove_file(pending);
10369 }
10370 return Err(error);
10371 }
10372 }
10373 if let Some(before_commit) = before_commit.as_mut() {
10374 if let Err(error) = before_commit() {
10375 drop(wal);
10376 for (_, _, pending) in &prepared_external {
10377 let _ = std::fs::remove_file(pending);
10378 }
10379 return Err(error);
10380 }
10381 }
10382
10383 let assigned_epoch = self.epoch.bump_assigned();
10384 let _epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
10385 if assigned_epoch != new_epoch {
10386 for (_, _, pending) in &prepared_external {
10387 let _ = std::fs::remove_file(pending);
10388 }
10389 return Err(MongrelError::Conflict(
10390 "commit epoch changed while sequencer lock was held".into(),
10391 ));
10392 }
10393
10394 prepared_run_links.disarm();
10398
10399 let append: Result<u64> = (|| {
10400 for (table_id, op) in wal_records {
10401 wal.append(txn_id, table_id, op)?;
10402 }
10403 wal.append_commit(txn_id, new_epoch, &added_runs)
10404 })();
10405 let commit_seq =
10406 append.map_err(|error| self.commit_outcome_unknown(new_epoch, error))?;
10407
10408 self.conflicts.record(&write_keys, new_epoch);
10413 (
10414 new_epoch,
10415 _epoch_guard,
10416 applies,
10417 committed_materialized_views,
10418 commit_seq,
10419 )
10420 };
10421 drop(commit_guard);
10422
10423 self.await_durable_commit(commit_seq, new_epoch)?;
10425 drop(security_guard);
10426
10427 let publish_result: Result<()> = {
10429 let mut first_error = None;
10430 let mut spilled_by_table: HashMap<u64, Vec<&SpilledRun>> = HashMap::new();
10431 for run in &spilled {
10432 spilled_by_table.entry(run.table_id).or_default().push(run);
10433 }
10434 let mut modified_tables = Vec::with_capacity(applies.len());
10435 for batch in applies {
10438 #[cfg(test)]
10439 PUBLISH_TABLE_LOCKS.with(|count| count.set(count.get() + 1));
10440 let mut t = batch.handle.lock();
10441 for op in batch.ops {
10442 match op {
10443 StagedOp::Put(rows) => t.apply_put_rows_prepared(rows),
10444 StagedOp::Delete(row_ids) => {
10445 for row_id in row_ids {
10446 t.apply_delete(row_id, new_epoch);
10447 }
10448 }
10449 StagedOp::Truncate => t.apply_truncate(new_epoch),
10450 }
10451 }
10452 if let Some(runs) = spilled_by_table.remove(&batch.table_id) {
10453 for run in runs {
10454 t.link_run(crate::manifest::RunRef {
10455 run_id: run.run_id,
10456 level: 0,
10457 epoch_created: new_epoch.0,
10458 row_count: run.row_count,
10459 });
10460 t.apply_run_metadata_prepared(&run.rows)?;
10461 if truncated_tables.contains(&batch.table_id) {
10462 t.set_flushed_epoch(new_epoch);
10467 }
10468 }
10469 }
10470 t.invalidate_pending_cache();
10471 drop(t);
10472 modified_tables.push(batch.handle);
10473 }
10474
10475 for handle in modified_tables {
10479 #[cfg(test)]
10480 COMMIT_MANIFEST_WRITES.with(|count| count.set(count.get() + 1));
10481 if let Err(error) = handle.lock().persist_manifest(new_epoch) {
10482 first_error.get_or_insert(error);
10483 }
10484 }
10485 for (name, _, pending) in &prepared_external {
10486 if let Err(error) = publish_external_state_file(&self.root, name, pending) {
10487 first_error.get_or_insert(error);
10488 }
10489 }
10490 if !committed_materialized_views.is_empty() {
10491 let mut next_catalog = self.catalog.read().clone();
10492 for definition in committed_materialized_views {
10493 if let Some(existing) = next_catalog
10494 .materialized_views
10495 .iter_mut()
10496 .find(|existing| existing.name == definition.name)
10497 {
10498 *existing = definition;
10499 } else {
10500 next_catalog.materialized_views.push(definition);
10501 }
10502 }
10503 next_catalog.db_epoch = next_catalog.db_epoch.max(new_epoch.0);
10504 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
10505 first_error.get_or_insert(error);
10506 }
10507 }
10508 match first_error {
10509 Some(error) => Err(error),
10510 None => Ok(()),
10511 }
10512 };
10513
10514 if has_changes {
10515 let _ = self.change_wake.send(());
10516 }
10517 self.finish_durable_publish(new_epoch, &mut _epoch_guard, publish_result)?;
10518 Ok((new_epoch, committed_row_ids))
10519 }
10520
10521 pub fn snapshot(&self) -> (Snapshot, SnapshotGuard<'_>) {
10524 let e = self.epoch.visible();
10525 let g = self.snapshots.register(e);
10526 (Snapshot::at(e), g)
10527 }
10528
10529 pub fn snapshot_owned(&self) -> (Snapshot, OwnedSnapshotGuard) {
10532 let e = self.epoch.visible();
10533 let g = self.snapshots.register_owned(e);
10534 (Snapshot::at(e), g)
10535 }
10536
10537 pub fn set_history_retention_epochs(&self, epochs: u64) -> Result<()> {
10542 let _guard = self.ddl_lock.lock();
10543 let current = self.epoch.visible();
10544 let (old_epochs, old_start) = self.snapshots.history_config();
10545 let earliest_already_guaranteed = if old_epochs == 0 {
10546 current
10547 } else {
10548 Epoch(old_start.0.max(current.0.saturating_sub(old_epochs)))
10549 };
10550 let start = if epochs == 0 {
10551 current
10552 } else {
10553 earliest_already_guaranteed
10554 };
10555 let published = std::cell::Cell::new(false);
10556 let result = write_history_retention(&self.root, epochs, start, || {
10557 self.snapshots.configure_history(epochs, start);
10558 published.set(true);
10559 });
10560 match result {
10561 Err(error) if published.get() => Err(MongrelError::CommitOutcomeUnknown {
10562 epoch: current.0,
10563 message: format!("history-retention publication was not durable: {error}"),
10564 }),
10565 result => result,
10566 }
10567 }
10568
10569 pub fn history_retention_epochs(&self) -> u64 {
10570 self.snapshots.history_config().0
10571 }
10572
10573 pub fn earliest_retained_epoch(&self) -> Epoch {
10574 let current = self.epoch.visible();
10575 self.snapshots.history_floor(current).unwrap_or(current)
10576 }
10577
10578 pub fn snapshot_at_owned(&self, epoch: Epoch) -> Result<(Snapshot, OwnedSnapshotGuard)> {
10581 let current = self.epoch.visible();
10582 if epoch > current {
10583 return Err(MongrelError::InvalidArgument(format!(
10584 "epoch {} is in the future; current epoch is {}",
10585 epoch.0, current.0
10586 )));
10587 }
10588 let earliest = self.earliest_retained_epoch();
10589 if epoch < earliest {
10590 return Err(MongrelError::InvalidArgument(format!(
10591 "epoch {} is no longer retained; earliest available epoch is {}",
10592 epoch.0, earliest.0
10593 )));
10594 }
10595 let guard = self.snapshots.register_owned(epoch);
10596 Ok((Snapshot::at(epoch), guard))
10597 }
10598
10599 pub fn table_names(&self) -> Vec<String> {
10601 self.catalog
10602 .read()
10603 .tables
10604 .iter()
10605 .filter(|t| matches!(t.state, TableState::Live))
10606 .map(|t| t.name.clone())
10607 .collect()
10608 }
10609
10610 pub fn close(&self) -> Result<()> {
10616 for name in self.table_names() {
10617 if let Ok(handle) = self.table(&name) {
10618 if let Err(e) = handle.lock().close() {
10619 eprintln!("[close] flush failed for {name}: {e}");
10620 }
10621 }
10622 }
10623 Ok(())
10624 }
10625
10626 pub fn compact(&self) -> Result<(usize, usize)> {
10633 self.require(&crate::auth::Permission::Ddl)?;
10634 let mut compacted = 0;
10635 let mut skipped = 0;
10636 for name in self.table_names() {
10637 let Ok(handle) = self.table(&name) else {
10638 continue;
10639 };
10640 {
10641 let mut t = handle.lock();
10642 let before = t.run_count();
10643 if before < 2 && !t.should_compact() {
10644 skipped += 1;
10645 continue;
10646 }
10647 match t.compact() {
10648 Ok(()) => {
10649 let after = t.run_count();
10650 compacted += 1;
10651 eprintln!("[compact] {name}: {before} -> {after} runs");
10652 }
10653 Err(e) => {
10654 eprintln!("[compact] {name}: compaction failed: {e}");
10655 skipped += 1;
10656 }
10657 }
10658 }
10659 }
10660 Ok((compacted, skipped))
10661 }
10662
10663 pub fn compact_table(&self, name: &str) -> Result<bool> {
10666 self.require(&crate::auth::Permission::Ddl)?;
10667 let handle = self.table(name)?;
10668 let mut t = handle.lock();
10669 let before = t.run_count();
10670 if before < 2 {
10671 return Ok(false);
10672 }
10673 t.compact()?;
10674 Ok(t.run_count() < before)
10675 }
10676
10677 pub fn table(&self, name: &str) -> Result<TableHandle> {
10679 self.ensure_owner_process()?;
10680 let cat = self.catalog.read();
10681 let entry = cat
10682 .live(name)
10683 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
10684 let id = entry.table_id;
10685 drop(cat);
10686 self.tables
10687 .read()
10688 .get(&id)
10689 .cloned()
10690 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not mounted")))
10691 }
10692
10693 pub fn has_ttl_tables(&self) -> bool {
10696 self.tables
10697 .read()
10698 .values()
10699 .any(|table| table.lock().ttl().is_some())
10700 }
10701
10702 pub(crate) fn table_by_id(&self, id: u64) -> Result<TableHandle> {
10705 self.tables
10706 .read()
10707 .get(&id)
10708 .cloned()
10709 .ok_or_else(|| MongrelError::NotFound(format!("table id {id} not mounted")))
10710 }
10711
10712 pub fn create_table(&self, name: &str, schema: Schema) -> Result<u64> {
10718 if name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
10719 return Err(MongrelError::InvalidArgument(format!(
10720 "table names beginning with {CTAS_BUILD_TABLE_PREFIX:?} are reserved"
10721 )));
10722 }
10723 self.create_table_with_state(name, schema, TableState::Live)
10724 }
10725
10726 #[doc(hidden)]
10728 pub fn create_building_table(
10729 &self,
10730 build_name: &str,
10731 intended_name: &str,
10732 query_id: &str,
10733 schema: Schema,
10734 ) -> Result<u64> {
10735 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10736 || intended_name.is_empty()
10737 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10738 || query_id.is_empty()
10739 {
10740 return Err(MongrelError::InvalidArgument(
10741 "invalid CTAS building-table identity".into(),
10742 ));
10743 }
10744 self.create_table_with_state(
10745 build_name,
10746 schema,
10747 TableState::Building {
10748 intended_name: intended_name.to_string(),
10749 query_id: query_id.to_string(),
10750 created_at_unix_nanos: current_unix_nanos(),
10751 replaces_table_id: None,
10752 },
10753 )
10754 }
10755
10756 #[doc(hidden)]
10759 pub fn create_rebuilding_table(
10760 &self,
10761 build_name: &str,
10762 intended_name: &str,
10763 query_id: &str,
10764 schema: Schema,
10765 ) -> Result<u64> {
10766 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10767 || intended_name.is_empty()
10768 || intended_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
10769 || query_id.is_empty()
10770 {
10771 return Err(MongrelError::InvalidArgument(
10772 "invalid rebuilding-table identity".into(),
10773 ));
10774 }
10775 let replaces_table_id = self
10776 .catalog
10777 .read()
10778 .live(intended_name)
10779 .ok_or_else(|| MongrelError::NotFound(format!("table {intended_name:?} not found")))?
10780 .table_id;
10781 self.create_table_with_state(
10782 build_name,
10783 schema,
10784 TableState::Building {
10785 intended_name: intended_name.to_string(),
10786 query_id: query_id.to_string(),
10787 created_at_unix_nanos: current_unix_nanos(),
10788 replaces_table_id: Some(replaces_table_id),
10789 },
10790 )
10791 }
10792
10793 fn create_table_with_state(
10794 &self,
10795 name: &str,
10796 schema: Schema,
10797 state: TableState,
10798 ) -> Result<u64> {
10799 use crate::wal::DdlOp;
10800 use std::sync::atomic::Ordering;
10801
10802 self.require(&crate::auth::Permission::Ddl)?;
10803 if self.poisoned.load(Ordering::Relaxed) {
10804 return Err(MongrelError::Other(
10805 "database poisoned by fsync error".into(),
10806 ));
10807 }
10808
10809 let _g = self.ddl_lock.lock();
10810 let _security_write = self.security_write()?;
10811 self.require(&crate::auth::Permission::Ddl)?;
10812 {
10813 let cat = self.catalog.read();
10814 match &state {
10815 TableState::Live => {
10816 if cat.live(name).is_some() || cat.building_for(name).is_some() {
10817 return Err(MongrelError::InvalidArgument(format!(
10818 "table {name:?} already exists or is being built"
10819 )));
10820 }
10821 }
10822 TableState::Building {
10823 intended_name,
10824 replaces_table_id,
10825 ..
10826 } => {
10827 let target_matches = match replaces_table_id {
10828 Some(table_id) => cat
10829 .live(intended_name)
10830 .is_some_and(|entry| entry.table_id == *table_id),
10831 None => cat.live(intended_name).is_none(),
10832 };
10833 if !target_matches || cat.building_for(intended_name).is_some() {
10834 return Err(MongrelError::InvalidArgument(format!(
10835 "table {intended_name:?} changed or is already being built"
10836 )));
10837 }
10838 if cat.building(name).is_some() {
10839 return Err(MongrelError::InvalidArgument(format!(
10840 "building table {name:?} already exists"
10841 )));
10842 }
10843 }
10844 TableState::Dropped { .. } => {
10845 return Err(MongrelError::InvalidArgument(
10846 "cannot create a dropped table".into(),
10847 ));
10848 }
10849 }
10850 }
10851
10852 let commit_lock = Arc::clone(&self.commit_lock);
10855 let _c = commit_lock.lock();
10856 let table_id = {
10857 let mut cat = self.catalog.write();
10858 let id = cat.next_table_id;
10859 cat.next_table_id = id
10860 .checked_add(1)
10861 .ok_or_else(|| MongrelError::InvalidArgument("table id space exhausted".into()))?;
10862 Result::<u64>::Ok(id)
10863 }?;
10864 let epoch = self.epoch.bump_assigned();
10865 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
10866 let txn_id = self.alloc_txn_id()?;
10867
10868 let mut schema = schema;
10872 schema.schema_id = table_id;
10873 schema.validate_auto_increment()?;
10880 schema.validate_defaults()?;
10881 schema.validate_ai()?;
10882 for index in &schema.indexes {
10883 index.validate_options()?;
10884 }
10885 for constraint in &schema.constraints.checks {
10886 constraint.expr.validate()?;
10887 }
10888
10889 let table_relative = Path::new(TABLES_DIR).join(table_id.to_string());
10892 let canonical_tdir = self.root.join(&table_relative);
10893 let table_root = Arc::new(
10894 self.durable_root
10895 .create_directory_all_pinned(&table_relative)?,
10896 );
10897 let tdir = table_root.io_path()?;
10898 let mut pending_table_dir = PendingTableDir::new(canonical_tdir);
10899 let ctx = SharedCtx {
10900 root_guard: Some(table_root),
10901 epoch: Arc::clone(&self.epoch),
10902 page_cache: Arc::clone(&self.page_cache),
10903 decoded_cache: Arc::clone(&self.decoded_cache),
10904 snapshots: Arc::clone(&self.snapshots),
10905 kek: self.kek.clone(),
10906 commit_lock: Arc::clone(&self.commit_lock),
10907 shared: Some(crate::engine::SharedWalCtx {
10908 wal: Arc::clone(&self.shared_wal),
10909 group: Arc::clone(&self.group),
10910 poisoned: Arc::clone(&self.poisoned),
10911 txn_ids: Arc::clone(&self.next_txn_id),
10912 change_wake: self.change_wake.clone(),
10913 }),
10914 table_name: Some(name.to_string()),
10915 auth: self.table_auth_checker(),
10916 read_only: self.read_only,
10917 };
10918 let table = Table::create_in(&tdir, schema.clone(), table_id, ctx)?;
10919
10920 let schema_json = DdlOp::encode_schema(&schema)?;
10923 let ddl = match &state {
10924 TableState::Live => DdlOp::CreateTable {
10925 table_id,
10926 name: name.to_string(),
10927 schema_json,
10928 },
10929 TableState::Building {
10930 intended_name,
10931 query_id,
10932 created_at_unix_nanos,
10933 replaces_table_id,
10934 } => match replaces_table_id {
10935 Some(replaces_table_id) => DdlOp::CreateRebuildingTable {
10936 table_id,
10937 build_name: name.to_string(),
10938 intended_name: intended_name.clone(),
10939 query_id: query_id.clone(),
10940 created_at_unix_nanos: *created_at_unix_nanos,
10941 replaces_table_id: *replaces_table_id,
10942 schema_json,
10943 },
10944 None => DdlOp::CreateBuildingTable {
10945 table_id,
10946 build_name: name.to_string(),
10947 intended_name: intended_name.clone(),
10948 query_id: query_id.clone(),
10949 created_at_unix_nanos: *created_at_unix_nanos,
10950 schema_json,
10951 },
10952 },
10953 TableState::Dropped { .. } => {
10954 return Err(MongrelError::InvalidArgument(
10955 "cannot create a table in dropped state".into(),
10956 ));
10957 }
10958 };
10959 let mut next_catalog = self.catalog.read().clone();
10960 next_catalog.tables.push(CatalogEntry {
10961 table_id,
10962 name: name.to_string(),
10963 schema: schema.clone(),
10964 state: state.clone(),
10965 created_epoch: epoch.0,
10966 });
10967 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
10968 let commit_seq = {
10969 let mut wal = self.shared_wal.lock();
10970 let append: Result<u64> = (|| {
10971 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
10972 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
10973 wal.append_commit(txn_id, epoch, &[])
10974 })();
10975 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
10976 };
10977 self.await_durable_commit(commit_seq, epoch)?;
10978 pending_table_dir.disarm();
10979
10980 self.tables
10983 .write()
10984 .insert(table_id, TableHandle::new(table));
10985 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
10986 self.finish_durable_publish(epoch, &mut _epoch_guard, checkpoint)?;
10987 Ok(table_id)
10988 }
10989
10990 pub fn drop_table(&self, name: &str) -> Result<()> {
10992 self.drop_table_with_epoch(name).map(|_| ())
10993 }
10994
10995 pub fn drop_table_with_epoch(&self, name: &str) -> Result<Epoch> {
10997 self.drop_table_with_state(name, false, None)
10998 }
10999
11000 pub fn drop_table_with_epoch_controlled<F>(
11001 &self,
11002 name: &str,
11003 mut before_commit: F,
11004 ) -> Result<Epoch>
11005 where
11006 F: FnMut() -> Result<()>,
11007 {
11008 self.drop_table_with_state(name, false, Some(&mut before_commit))
11009 }
11010
11011 #[doc(hidden)]
11013 pub fn discard_building_table(&self, name: &str) -> Result<()> {
11014 if !name.starts_with(CTAS_BUILD_TABLE_PREFIX) {
11015 return Err(MongrelError::InvalidArgument(
11016 "not a CTAS building table".into(),
11017 ));
11018 }
11019 self.drop_table_with_state(name, true, None).map(|_| ())
11020 }
11021
11022 fn drop_table_with_state(
11023 &self,
11024 name: &str,
11025 building: bool,
11026 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11027 ) -> Result<Epoch> {
11028 use crate::wal::DdlOp;
11029 use std::sync::atomic::Ordering;
11030
11031 self.require(&crate::auth::Permission::Ddl)?;
11032 if self.poisoned.load(Ordering::Relaxed) {
11033 return Err(MongrelError::Other(
11034 "database poisoned by fsync error".into(),
11035 ));
11036 }
11037
11038 let _g = self.ddl_lock.lock();
11039 let _security_write = self.security_write()?;
11040 self.require(&crate::auth::Permission::Ddl)?;
11041 let table_id = {
11042 let cat = self.catalog.read();
11043 if building {
11044 cat.building(name)
11045 } else {
11046 cat.live(name)
11047 }
11048 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?
11049 .table_id
11050 };
11051
11052 let commit_lock = Arc::clone(&self.commit_lock);
11053 let _c = commit_lock.lock();
11054 let epoch = self.epoch.bump_assigned();
11055 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
11056 let txn_id = self.alloc_txn_id()?;
11057 let mut next_catalog = self.catalog.read().clone();
11058 let entry = next_catalog
11059 .tables
11060 .iter_mut()
11061 .find(|t| t.table_id == table_id)
11062 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
11063 entry.state = TableState::Dropped { at_epoch: epoch.0 };
11064 next_catalog.triggers.retain(|trigger| {
11065 !matches!(
11066 &trigger.trigger.target,
11067 TriggerTarget::Table(target) if target == name
11068 )
11069 });
11070 next_catalog
11071 .materialized_views
11072 .retain(|definition| definition.name != name);
11073 next_catalog
11074 .security
11075 .rls_tables
11076 .retain(|table| table != name);
11077 next_catalog
11078 .security
11079 .policies
11080 .retain(|policy| policy.table != name);
11081 next_catalog
11082 .security
11083 .masks
11084 .retain(|mask| mask.table != name);
11085 for role in &mut next_catalog.roles {
11086 role.permissions
11087 .retain(|permission| permission_table(permission) != Some(name));
11088 }
11089 advance_security_version(&mut next_catalog)?;
11090 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11091 let commit_seq = {
11092 let mut wal = self.shared_wal.lock();
11093 if let Some(before_commit) = before_commit {
11094 before_commit()?;
11095 }
11096 let append: Result<u64> = (|| {
11097 wal.append(
11098 txn_id,
11099 table_id,
11100 crate::wal::Op::Ddl(DdlOp::DropTable { table_id }),
11101 )?;
11102 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11103 wal.append_commit(txn_id, epoch, &[])
11104 })();
11105 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11106 };
11107 self.await_durable_commit(commit_seq, epoch)?;
11108
11109 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
11110 self.tables.write().remove(&table_id);
11111 self.finish_durable_publish(epoch, &mut _epoch_guard, checkpoint)?;
11112 Ok(epoch)
11113 }
11114
11115 pub fn rename_table(&self, name: &str, new_name: &str) -> Result<()> {
11124 self.rename_table_with_epoch(name, new_name).map(|_| ())
11125 }
11126
11127 pub fn rename_table_with_epoch(&self, name: &str, new_name: &str) -> Result<Epoch> {
11129 self.rename_table_with_epoch_inner(name, new_name, None)
11130 }
11131
11132 pub fn rename_table_with_epoch_controlled<F>(
11133 &self,
11134 name: &str,
11135 new_name: &str,
11136 mut before_commit: F,
11137 ) -> Result<Epoch>
11138 where
11139 F: FnMut() -> Result<()>,
11140 {
11141 self.rename_table_with_epoch_inner(name, new_name, Some(&mut before_commit))
11142 }
11143
11144 fn rename_table_with_epoch_inner(
11145 &self,
11146 name: &str,
11147 new_name: &str,
11148 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11149 ) -> Result<Epoch> {
11150 if name.starts_with(CTAS_BUILD_TABLE_PREFIX)
11151 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
11152 {
11153 return Err(MongrelError::InvalidArgument(
11154 "the CTAS building-table namespace is reserved".into(),
11155 ));
11156 }
11157 self.rename_table_with_state(name, new_name, false, None, before_commit)
11158 }
11159
11160 #[doc(hidden)]
11162 pub fn publish_building_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
11163 self.publish_building_table_inner(build_name, new_name, None)
11164 }
11165
11166 #[doc(hidden)]
11167 pub fn publish_building_table_controlled<F>(
11168 &self,
11169 build_name: &str,
11170 new_name: &str,
11171 mut before_commit: F,
11172 ) -> Result<Epoch>
11173 where
11174 F: FnMut() -> Result<()>,
11175 {
11176 self.publish_building_table_inner(build_name, new_name, Some(&mut before_commit))
11177 }
11178
11179 fn publish_building_table_inner(
11180 &self,
11181 build_name: &str,
11182 new_name: &str,
11183 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11184 ) -> Result<Epoch> {
11185 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
11186 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
11187 {
11188 return Err(MongrelError::InvalidArgument(
11189 "invalid CTAS publish identity".into(),
11190 ));
11191 }
11192 self.rename_table_with_state(build_name, new_name, true, None, before_commit)
11193 }
11194
11195 #[doc(hidden)]
11197 pub fn publish_materialized_building_table(
11198 &self,
11199 build_name: &str,
11200 new_name: &str,
11201 definition: crate::catalog::MaterializedViewEntry,
11202 ) -> Result<Epoch> {
11203 self.publish_materialized_building_table_inner(build_name, new_name, definition, None)
11204 }
11205
11206 #[doc(hidden)]
11207 pub fn publish_materialized_building_table_controlled<F>(
11208 &self,
11209 build_name: &str,
11210 new_name: &str,
11211 definition: crate::catalog::MaterializedViewEntry,
11212 mut before_commit: F,
11213 ) -> Result<Epoch>
11214 where
11215 F: FnMut() -> Result<()>,
11216 {
11217 self.publish_materialized_building_table_inner(
11218 build_name,
11219 new_name,
11220 definition,
11221 Some(&mut before_commit),
11222 )
11223 }
11224
11225 fn publish_materialized_building_table_inner(
11226 &self,
11227 build_name: &str,
11228 new_name: &str,
11229 definition: crate::catalog::MaterializedViewEntry,
11230 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11231 ) -> Result<Epoch> {
11232 if definition.name != new_name || definition.query.trim().is_empty() {
11233 return Err(MongrelError::InvalidArgument(
11234 "invalid materialized-view publication".into(),
11235 ));
11236 }
11237 self.rename_table_with_state(build_name, new_name, true, Some(definition), before_commit)
11238 }
11239
11240 #[doc(hidden)]
11242 pub fn publish_rebuilding_table(&self, build_name: &str, new_name: &str) -> Result<Epoch> {
11243 self.publish_rebuilding_table_inner(build_name, new_name, None, None)
11244 }
11245
11246 #[doc(hidden)]
11247 pub fn publish_rebuilding_table_controlled<F>(
11248 &self,
11249 build_name: &str,
11250 new_name: &str,
11251 mut before_commit: F,
11252 ) -> Result<Epoch>
11253 where
11254 F: FnMut() -> Result<()>,
11255 {
11256 self.publish_rebuilding_table_inner(build_name, new_name, None, Some(&mut before_commit))
11257 }
11258
11259 #[doc(hidden)]
11261 pub fn publish_materialized_rebuilding_table_controlled<F>(
11262 &self,
11263 build_name: &str,
11264 new_name: &str,
11265 definition: crate::catalog::MaterializedViewEntry,
11266 mut before_commit: F,
11267 ) -> Result<Epoch>
11268 where
11269 F: FnMut() -> Result<()>,
11270 {
11271 self.publish_rebuilding_table_inner(
11272 build_name,
11273 new_name,
11274 Some(definition),
11275 Some(&mut before_commit),
11276 )
11277 }
11278
11279 fn publish_rebuilding_table_inner(
11280 &self,
11281 build_name: &str,
11282 new_name: &str,
11283 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
11284 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11285 ) -> Result<Epoch> {
11286 use crate::wal::DdlOp;
11287
11288 if !build_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
11289 || new_name.is_empty()
11290 || new_name.starts_with(CTAS_BUILD_TABLE_PREFIX)
11291 {
11292 return Err(MongrelError::InvalidArgument(
11293 "invalid rebuilding-table publish identity".into(),
11294 ));
11295 }
11296 if materialized_view.as_ref().is_some_and(|definition| {
11297 definition.name != new_name || definition.query.trim().is_empty()
11298 }) {
11299 return Err(MongrelError::InvalidArgument(
11300 "invalid materialized-view replacement".into(),
11301 ));
11302 }
11303 self.require(&crate::auth::Permission::Ddl)?;
11304 if self.poisoned.load(Ordering::Relaxed) {
11305 return Err(MongrelError::Other(
11306 "database poisoned by fsync error".into(),
11307 ));
11308 }
11309
11310 let _ddl = self.ddl_lock.lock();
11311 let _security_write = self.security_write()?;
11312 let (table_id, replaced_table_id) = {
11313 let catalog = self.catalog.read();
11314 let build = catalog.building(build_name).ok_or_else(|| {
11315 MongrelError::NotFound(format!("building table {build_name:?} not found"))
11316 })?;
11317 let replaced_table_id = match &build.state {
11318 TableState::Building {
11319 intended_name,
11320 replaces_table_id: Some(replaced_table_id),
11321 ..
11322 } if intended_name == new_name => *replaced_table_id,
11323 _ => {
11324 return Err(MongrelError::InvalidArgument(format!(
11325 "building table {build_name:?} is not a replacement for {new_name:?}"
11326 )))
11327 }
11328 };
11329 if catalog
11330 .live(new_name)
11331 .is_none_or(|entry| entry.table_id != replaced_table_id)
11332 {
11333 return Err(MongrelError::Conflict(format!(
11334 "table {new_name:?} changed while its replacement was built"
11335 )));
11336 }
11337 (build.table_id, replaced_table_id)
11338 };
11339
11340 let _commit = self.commit_lock.lock();
11341 let epoch = self.epoch.assigned().next();
11342 let txn_id = self.alloc_txn_id()?;
11343 let mut next_catalog = self.catalog.read().clone();
11344 apply_rebuilding_publish(
11345 &mut next_catalog,
11346 table_id,
11347 replaced_table_id,
11348 new_name,
11349 epoch.0,
11350 )?;
11351 if let Some(definition) = materialized_view.as_mut() {
11352 definition.last_refresh_epoch = epoch.0;
11353 }
11354 let materialized_view_json = materialized_view
11355 .as_ref()
11356 .map(DdlOp::encode_materialized_view)
11357 .transpose()?;
11358 if let Some(definition) = materialized_view {
11359 if let Some(existing) = next_catalog
11360 .materialized_views
11361 .iter_mut()
11362 .find(|existing| existing.name == definition.name)
11363 {
11364 *existing = definition;
11365 } else {
11366 next_catalog.materialized_views.push(definition);
11367 }
11368 }
11369 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11370 if let Some(before_commit) = before_commit {
11371 before_commit()?;
11372 }
11373 let assigned_epoch = self.epoch.bump_assigned();
11374 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), assigned_epoch);
11375 if assigned_epoch != epoch {
11376 return Err(MongrelError::Conflict(
11377 "commit epoch changed while sequencer lock was held".into(),
11378 ));
11379 }
11380 let commit_seq = {
11381 let mut wal = self.shared_wal.lock();
11382 let append: Result<u64> = (|| {
11383 wal.append(
11384 txn_id,
11385 table_id,
11386 crate::wal::Op::Ddl(DdlOp::ReplaceBuildingTable {
11387 table_id,
11388 replaced_table_id,
11389 new_name: new_name.to_string(),
11390 }),
11391 )?;
11392 if let Some(definition_json) = materialized_view_json {
11393 wal.append(
11394 txn_id,
11395 table_id,
11396 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
11397 name: new_name.to_string(),
11398 definition_json,
11399 }),
11400 )?;
11401 }
11402 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11403 wal.append_commit(txn_id, epoch, &[])
11404 })();
11405 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11406 };
11407 self.await_durable_commit(commit_seq, epoch)?;
11408
11409 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
11410 self.tables.write().remove(&replaced_table_id);
11411 if let Some(table) = self.tables.read().get(&table_id) {
11412 table.lock().set_catalog_name(new_name.to_string());
11413 }
11414 self.finish_durable_publish(epoch, &mut epoch_guard, checkpoint)?;
11415 Ok(epoch)
11416 }
11417
11418 fn rename_table_with_state(
11419 &self,
11420 name: &str,
11421 new_name: &str,
11422 building: bool,
11423 mut materialized_view: Option<crate::catalog::MaterializedViewEntry>,
11424 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11425 ) -> Result<Epoch> {
11426 use crate::wal::DdlOp;
11427 use std::sync::atomic::Ordering;
11428
11429 self.require(&crate::auth::Permission::Ddl)?;
11430 if self.poisoned.load(Ordering::Relaxed) {
11431 return Err(MongrelError::Other(
11432 "database poisoned by fsync error".into(),
11433 ));
11434 }
11435
11436 if name == new_name {
11439 return Ok(self.visible_epoch());
11440 }
11441 if new_name.is_empty() {
11442 return Err(MongrelError::InvalidArgument(
11443 "rename_table: new name must not be empty".into(),
11444 ));
11445 }
11446
11447 let _g = self.ddl_lock.lock();
11448 let _security_write = self.security_write()?;
11449 self.require(&crate::auth::Permission::Ddl)?;
11450 let table_id = {
11451 let cat = self.catalog.read();
11452 let src = if building {
11453 cat.building(name)
11454 } else {
11455 cat.live(name)
11456 }
11457 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
11458 if building
11459 && !matches!(
11460 &src.state,
11461 TableState::Building { intended_name, .. } if intended_name == new_name
11462 )
11463 {
11464 return Err(MongrelError::InvalidArgument(format!(
11465 "building table {name:?} is not reserved for {new_name:?}"
11466 )));
11467 }
11468 if cat.live(new_name).is_some() || (!building && cat.building_for(new_name).is_some()) {
11472 return Err(MongrelError::InvalidArgument(format!(
11473 "rename_table: a table named {new_name:?} already exists"
11474 )));
11475 }
11476 src.table_id
11477 };
11478
11479 let commit_lock = Arc::clone(&self.commit_lock);
11480 let _c = commit_lock.lock();
11481 let epoch = self.epoch.bump_assigned();
11482 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
11483 let txn_id = self.alloc_txn_id()?;
11484 if let Some(definition) = materialized_view.as_mut() {
11485 definition.last_refresh_epoch = epoch.0;
11486 }
11487 let materialized_view_json = materialized_view
11488 .as_ref()
11489 .map(DdlOp::encode_materialized_view)
11490 .transpose()?;
11491 let mut next_catalog = self.catalog.read().clone();
11492 let entry = next_catalog
11493 .tables
11494 .iter_mut()
11495 .find(|t| t.table_id == table_id)
11496 .ok_or_else(|| MongrelError::NotFound(format!("table {name:?} not found")))?;
11497 entry.name = new_name.to_string();
11498 if building {
11499 entry.state = TableState::Live;
11500 }
11501 for trigger in &mut next_catalog.triggers {
11502 if matches!(
11503 &trigger.trigger.target,
11504 TriggerTarget::Table(target) if target == name
11505 ) {
11506 trigger.trigger = trigger.trigger.retarget_table(new_name, epoch.0)?;
11507 }
11508 }
11509 if let Some(definition) = next_catalog
11510 .materialized_views
11511 .iter_mut()
11512 .find(|definition| definition.name == name)
11513 {
11514 definition.name = new_name.to_string();
11515 }
11516 if let Some(definition) = materialized_view.take() {
11517 next_catalog.materialized_views.push(definition);
11518 }
11519 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11520 for table in &mut next_catalog.security.rls_tables {
11521 if table == name {
11522 *table = new_name.to_string();
11523 }
11524 }
11525 for policy in &mut next_catalog.security.policies {
11526 if policy.table == name {
11527 policy.table = new_name.to_string();
11528 }
11529 }
11530 for mask in &mut next_catalog.security.masks {
11531 if mask.table == name {
11532 mask.table = new_name.to_string();
11533 }
11534 }
11535 for role in &mut next_catalog.roles {
11536 for permission in &mut role.permissions {
11537 rename_permission_table(permission, name, new_name);
11538 }
11539 }
11540 advance_security_version(&mut next_catalog)?;
11541 let ddl = if building {
11542 DdlOp::PublishBuildingTable {
11543 table_id,
11544 new_name: new_name.to_string(),
11545 }
11546 } else {
11547 DdlOp::RenameTable {
11548 table_id,
11549 new_name: new_name.to_string(),
11550 }
11551 };
11552 let commit_seq = {
11553 let mut wal = self.shared_wal.lock();
11554 if let Some(before_commit) = before_commit {
11555 before_commit()?;
11556 }
11557 let append: Result<u64> = (|| {
11558 wal.append(txn_id, table_id, crate::wal::Op::Ddl(ddl))?;
11559 if let Some(definition_json) = materialized_view_json {
11560 wal.append(
11561 txn_id,
11562 table_id,
11563 crate::wal::Op::Ddl(DdlOp::SetMaterializedView {
11564 name: new_name.to_string(),
11565 definition_json,
11566 }),
11567 )?;
11568 }
11569 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11570 wal.append_commit(txn_id, epoch, &[])
11571 })();
11572 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11573 };
11574 self.await_durable_commit(commit_seq, epoch)?;
11575
11576 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
11577 if let Some(table) = self.tables.read().get(&table_id) {
11580 table.lock().set_catalog_name(new_name.to_string());
11581 }
11582 self.finish_durable_publish(epoch, &mut _epoch_guard, checkpoint)?;
11583 Ok(epoch)
11584 }
11585
11586 pub fn alter_column(
11587 &self,
11588 table_name: &str,
11589 column_name: &str,
11590 change: AlterColumn,
11591 ) -> Result<ColumnDef> {
11592 self.alter_column_with_epoch(table_name, column_name, change)
11593 .map(|(column, _)| column)
11594 }
11595
11596 pub fn alter_column_with_epoch(
11597 &self,
11598 table_name: &str,
11599 column_name: &str,
11600 change: AlterColumn,
11601 ) -> Result<(ColumnDef, Option<Epoch>)> {
11602 self.alter_column_with_epoch_inner(table_name, column_name, change, None, None, None)
11603 }
11604
11605 pub fn alter_column_with_epoch_controlled<B, A>(
11610 &self,
11611 table_name: &str,
11612 column_name: &str,
11613 change: AlterColumn,
11614 control: &crate::ExecutionControl,
11615 mut before_commit: B,
11616 mut after_commit: A,
11617 ) -> Result<(ColumnDef, Option<Epoch>)>
11618 where
11619 B: FnMut() -> Result<()>,
11620 A: FnMut(Option<Epoch>) -> Result<()>,
11621 {
11622 self.alter_column_with_epoch_inner(
11623 table_name,
11624 column_name,
11625 change,
11626 Some(control),
11627 Some(&mut before_commit),
11628 Some(&mut after_commit),
11629 )
11630 }
11631
11632 #[allow(clippy::too_many_arguments)]
11633 fn alter_column_with_epoch_inner(
11634 &self,
11635 table_name: &str,
11636 column_name: &str,
11637 change: AlterColumn,
11638 control: Option<&crate::ExecutionControl>,
11639 mut before_commit: Option<&mut dyn FnMut() -> Result<()>>,
11640 mut after_commit: Option<&mut dyn FnMut(Option<Epoch>) -> Result<()>>,
11641 ) -> Result<(ColumnDef, Option<Epoch>)> {
11642 use crate::wal::DdlOp;
11643 use std::sync::atomic::Ordering;
11644
11645 self.require(&crate::auth::Permission::Ddl)?;
11646 commit_prepare_checkpoint(control, 0)?;
11647 if self.poisoned.load(Ordering::Relaxed) {
11648 return Err(MongrelError::Other(
11649 "database poisoned by fsync error".into(),
11650 ));
11651 }
11652
11653 let _g = self.ddl_lock.lock();
11654 let table_id = {
11655 let cat = self.catalog.read();
11656 cat.live(table_name)
11657 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
11658 .table_id
11659 };
11660 let handle =
11661 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
11662 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
11663 })?;
11664
11665 let backfill = {
11671 let table = handle.lock();
11672 let old = table
11673 .schema()
11674 .column(column_name)
11675 .cloned()
11676 .ok_or_else(|| MongrelError::Schema(format!("unknown column {column_name}")))?;
11677 let next_flags = change.flags.unwrap_or(old.flags);
11678 if old.flags.contains(crate::schema::ColumnFlags::NULLABLE)
11679 && !next_flags.contains(crate::schema::ColumnFlags::NULLABLE)
11680 && old.default_value.is_some()
11681 {
11682 let snapshot = Snapshot::at(self.epoch.visible());
11683 let mut updates = Vec::new();
11684 let rows = match control {
11685 Some(control) => table.visible_rows_controlled(snapshot, control)?,
11686 None => table.visible_rows(snapshot)?,
11687 };
11688 for (row_index, row) in rows.into_iter().enumerate() {
11689 commit_prepare_checkpoint(control, row_index)?;
11690 if row
11691 .columns
11692 .get(&old.id)
11693 .is_some_and(|value| !matches!(value, Value::Null))
11694 {
11695 continue;
11696 }
11697 let mut cells: Vec<(u16, Value)> = row.columns.into_iter().collect();
11698 table.apply_defaults(&mut cells)?;
11699 updates.push((
11700 table_id,
11701 crate::txn::Staged::Update {
11702 row_id: row.row_id,
11703 new_row: cells,
11704 changed_columns: vec![old.id],
11705 },
11706 ));
11707 }
11708 updates
11709 } else {
11710 Vec::new()
11711 }
11712 };
11713 let durable_epoch = std::cell::Cell::new(None);
11714 let backfill_epoch = if backfill.is_empty() {
11715 None
11716 } else {
11717 let (principal, catalog_bound) = self.transaction_principal_snapshot();
11718 let txn_id = self.alloc_txn_id()?;
11719 let mut entered_fence = false;
11720 let commit_result = match (control, before_commit.as_deref_mut()) {
11721 (Some(control), Some(before_commit)) => self
11722 .commit_transaction_with_external_states_controlled(
11723 txn_id,
11724 self.epoch.visible(),
11725 backfill,
11726 Vec::new(),
11727 Vec::new(),
11728 principal.clone(),
11729 catalog_bound,
11730 None,
11731 control,
11732 &mut || {
11733 before_commit()?;
11734 entered_fence = true;
11735 Ok(())
11736 },
11737 )
11738 .map(|(epoch, _)| epoch),
11739 _ => self
11740 .commit_transaction_with_external_states(
11741 txn_id,
11742 self.epoch.visible(),
11743 backfill,
11744 Vec::new(),
11745 Vec::new(),
11746 principal,
11747 catalog_bound,
11748 None,
11749 )
11750 .map(|(epoch, _)| epoch),
11751 };
11752 let commit_result = if entered_fence {
11753 finish_controlled_commit_attempt(commit_result, &mut after_commit)
11754 } else {
11755 commit_result
11756 };
11757 match &commit_result {
11758 Ok(epoch) => durable_epoch.set(Some(*epoch)),
11759 Err(MongrelError::DurableCommit { epoch, .. }) => {
11760 durable_epoch.set(Some(Epoch(*epoch)));
11761 }
11762 Err(_) => {}
11763 }
11764 Some(commit_result?)
11765 };
11766 let result: Result<(ColumnDef, Option<Epoch>)> = (|| {
11767 let _security_write = self.security_write()?;
11768 self.require(&crate::auth::Permission::Ddl)?;
11769 if self
11770 .catalog
11771 .read()
11772 .live(table_name)
11773 .is_none_or(|entry| entry.table_id != table_id)
11774 {
11775 return Err(MongrelError::Conflict(format!(
11776 "table {table_name:?} changed during ALTER"
11777 )));
11778 }
11779 let mut table = handle.lock();
11780 let (column, prepared_schema) = table.prepare_alter_column(column_name, &change)?;
11781 let renamed_column = (column.name != column_name).then(|| column.name.clone());
11782 let Some(prepared_schema) = prepared_schema else {
11783 return Ok((column, backfill_epoch));
11784 };
11785
11786 let commit_lock = Arc::clone(&self.commit_lock);
11787 let _c = commit_lock.lock();
11788 let epoch = self.epoch.bump_assigned();
11789 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
11790 let txn_id = self.alloc_txn_id()?;
11791 let column_json = DdlOp::encode_column(&column)?;
11792 let mut next_catalog = self.catalog.read().clone();
11793 let catalog_entry_index = next_catalog
11794 .tables
11795 .iter()
11796 .position(|entry| entry.table_id == table_id)
11797 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?;
11798 if let Some(new_column_name) = &renamed_column {
11799 for (trigger_index, trigger) in next_catalog.triggers.iter_mut().enumerate() {
11800 commit_prepare_checkpoint(control, trigger_index)?;
11801 if matches!(
11802 &trigger.trigger.target,
11803 TriggerTarget::Table(target) if target == table_name
11804 ) {
11805 trigger.trigger = trigger.trigger.renamed_update_column(
11806 column_name,
11807 new_column_name.clone(),
11808 epoch.0,
11809 )?;
11810 }
11811 }
11812 for (role_index, role) in next_catalog.roles.iter_mut().enumerate() {
11813 commit_prepare_checkpoint(control, role_index)?;
11814 for (permission_index, permission) in role.permissions.iter_mut().enumerate() {
11815 commit_prepare_checkpoint(control, permission_index)?;
11816 rename_permission_column(
11817 permission,
11818 table_name,
11819 column_name,
11820 new_column_name,
11821 );
11822 }
11823 }
11824 advance_security_version(&mut next_catalog)?;
11825 }
11826 next_catalog.tables[catalog_entry_index].schema = prepared_schema.clone();
11827 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11828 commit_prepare_checkpoint(control, 0)?;
11829 let mut entered_fence = false;
11830 if let Some(before_commit) = before_commit.as_deref_mut() {
11831 before_commit()?;
11832 entered_fence = true;
11833 }
11834 let commit_result: Result<Epoch> = (|| {
11835 let commit_seq = {
11836 let mut wal = self.shared_wal.lock();
11837 let append: Result<u64> = (|| {
11838 wal.append(
11839 txn_id,
11840 table_id,
11841 crate::wal::Op::Ddl(DdlOp::AlterTable {
11842 table_id,
11843 column_json,
11844 }),
11845 )?;
11846 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
11847 wal.append_commit(txn_id, epoch, &[])
11848 })();
11849 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
11850 };
11851 self.await_durable_commit(commit_seq, epoch)?;
11852 durable_epoch.set(Some(epoch));
11853
11854 table.apply_altered_schema_prepared(prepared_schema);
11855 let schema = table.schema().clone();
11856 let table_checkpoint = table.checkpoint_altered_schema();
11857 drop(table);
11858 next_catalog.tables[catalog_entry_index].schema = schema;
11859 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11860 let catalog_result =
11861 catalog::write_atomic(&self.root, &next_catalog, self.meta_dek.as_ref());
11862 let security_version = next_catalog.security_version;
11863 *self.catalog.write() = next_catalog;
11864 if renamed_column.is_some() {
11865 self.security_coordinator
11866 .version
11867 .store(security_version, Ordering::Release);
11868 }
11869 self.epoch.publish_in_order(epoch);
11870 _epoch_guard.disarm();
11871 if let Err(error) = table_checkpoint.and(catalog_result) {
11872 self.poisoned.store(true, Ordering::Relaxed);
11873 return Err(MongrelError::DurableCommit {
11874 epoch: epoch.0,
11875 message: error.to_string(),
11876 });
11877 }
11878 Ok(epoch)
11879 })();
11880 let commit_result = if entered_fence {
11881 finish_controlled_commit_attempt(commit_result, &mut after_commit)
11882 } else {
11883 commit_result
11884 };
11885 let epoch = commit_result?;
11886 Ok((column, Some(epoch)))
11887 })();
11888 result.map_err(|error| match (durable_epoch.get(), error) {
11889 (_, error @ MongrelError::DurableCommit { .. }) => error,
11890 (Some(epoch), error) => MongrelError::DurableCommit {
11891 epoch: epoch.0,
11892 message: error.to_string(),
11893 },
11894 (None, error) => error,
11895 })
11896 }
11897
11898 pub fn set_table_ttl(
11901 &self,
11902 table_name: &str,
11903 column_name: &str,
11904 duration_nanos: u64,
11905 ) -> Result<crate::manifest::TtlPolicy> {
11906 let policy = self.replace_table_ttl(table_name, Some((column_name, duration_nanos)))?;
11907 policy.ok_or_else(|| MongrelError::Other("set TTL produced no policy".into()))
11908 }
11909
11910 #[doc(hidden)]
11912 pub fn set_building_table_ttl(
11913 &self,
11914 table_name: &str,
11915 column_name: &str,
11916 duration_nanos: u64,
11917 ) -> Result<crate::manifest::TtlPolicy> {
11918 let policy = self.replace_table_ttl_with_state(
11919 table_name,
11920 Some((column_name, duration_nanos)),
11921 true,
11922 )?;
11923 policy
11924 .ok_or_else(|| MongrelError::Other("set building-table TTL produced no policy".into()))
11925 }
11926
11927 pub fn clear_table_ttl(&self, table_name: &str) -> Result<()> {
11928 self.replace_table_ttl(table_name, None)?;
11929 Ok(())
11930 }
11931
11932 fn replace_table_ttl(
11933 &self,
11934 table_name: &str,
11935 requested: Option<(&str, u64)>,
11936 ) -> Result<Option<crate::manifest::TtlPolicy>> {
11937 self.replace_table_ttl_with_state(table_name, requested, false)
11938 }
11939
11940 fn replace_table_ttl_with_state(
11941 &self,
11942 table_name: &str,
11943 requested: Option<(&str, u64)>,
11944 building: bool,
11945 ) -> Result<Option<crate::manifest::TtlPolicy>> {
11946 use crate::wal::DdlOp;
11947 use std::sync::atomic::Ordering;
11948
11949 self.require(&crate::auth::Permission::Ddl)?;
11950 if self.poisoned.load(Ordering::Relaxed) {
11951 return Err(MongrelError::Other(
11952 "database poisoned by fsync error".into(),
11953 ));
11954 }
11955
11956 let _g = self.ddl_lock.lock();
11957 let _security_write = self.security_write()?;
11958 self.require(&crate::auth::Permission::Ddl)?;
11959 let table_id = {
11960 let cat = self.catalog.read();
11961 if building {
11962 cat.building(table_name)
11963 } else {
11964 cat.live(table_name)
11965 }
11966 .ok_or_else(|| MongrelError::NotFound(format!("table {table_name:?} not found")))?
11967 .table_id
11968 };
11969 let handle =
11970 self.tables.read().get(&table_id).cloned().ok_or_else(|| {
11971 MongrelError::NotFound(format!("table {table_name:?} not mounted"))
11972 })?;
11973 let mut table = handle.lock();
11974 let policy = match requested {
11975 Some((column, duration)) => Some(table.prepare_ttl_policy(column, duration)?),
11976 None => None,
11977 };
11978 if table.ttl() == policy {
11979 return Ok(policy);
11980 }
11981
11982 let commit_lock = Arc::clone(&self.commit_lock);
11983 let _c = commit_lock.lock();
11984 let epoch = self.epoch.bump_assigned();
11985 let mut _epoch_guard = EpochGuard::new(self.epoch.as_ref(), epoch);
11986 let txn_id = self.alloc_txn_id()?;
11987 let policy_json = DdlOp::encode_ttl(policy)?;
11988 let mut next_catalog = self.catalog.read().clone();
11989 next_catalog.db_epoch = next_catalog.db_epoch.max(epoch.0);
11990 let commit_seq = {
11991 let mut wal = self.shared_wal.lock();
11992 let append: Result<u64> = (|| {
11993 wal.append(
11994 txn_id,
11995 table_id,
11996 crate::wal::Op::Ddl(DdlOp::SetTtl {
11997 table_id,
11998 policy_json,
11999 }),
12000 )?;
12001 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
12002 wal.append_commit(txn_id, epoch, &[])
12003 })();
12004 append.map_err(|error| self.commit_outcome_unknown(epoch, error))?
12005 };
12006 self.await_durable_commit(commit_seq, epoch)?;
12007
12008 let mut publish_error = table.apply_ttl_policy_at(policy, epoch).err();
12009 drop(table);
12010 if let Err(error) = self.checkpoint_catalog_after_durable(next_catalog) {
12011 publish_error.get_or_insert(error);
12012 }
12013 self.finish_durable_publish(epoch, &mut _epoch_guard, publish_error.map_or(Ok(()), Err))?;
12014 Ok(policy)
12015 }
12016
12017 pub fn gc(&self) -> Result<usize> {
12023 let control = crate::ExecutionControl::new(None);
12024 self.gc_controlled(&control, || true)
12025 }
12026
12027 #[doc(hidden)]
12030 pub fn gc_controlled<F>(
12031 &self,
12032 control: &crate::ExecutionControl,
12033 before_publish: F,
12034 ) -> Result<usize>
12035 where
12036 F: FnOnce() -> bool,
12037 {
12038 self.gc_controlled_with_receipt(control, before_publish)
12039 .map(|(reclaimed, _)| reclaimed)
12040 }
12041
12042 #[doc(hidden)]
12045 pub fn gc_controlled_with_receipt<F>(
12046 &self,
12047 control: &crate::ExecutionControl,
12048 before_publish: F,
12049 ) -> Result<(usize, Option<MaintenanceReceipt>)>
12050 where
12051 F: FnOnce() -> bool,
12052 {
12053 enum Candidate {
12054 Directory(PathBuf),
12055 File(PathBuf),
12056 }
12057
12058 self.require(&crate::auth::Permission::Ddl)?;
12059 let _ddl = self.ddl_lock.lock();
12060 self.require(&crate::auth::Permission::Ddl)?;
12061 control.checkpoint()?;
12062 let maintenance_epoch = self.epoch.visible();
12063 let min_active = self.snapshots.min_active(maintenance_epoch).0;
12064 let mut candidates = Vec::new();
12065
12066 let cat = self.catalog.read();
12068 for (entry_index, entry) in cat.tables.iter().enumerate() {
12069 if entry_index % 256 == 0 {
12070 control.checkpoint()?;
12071 }
12072 if let TableState::Dropped { at_epoch } = entry.state {
12073 if at_epoch <= min_active {
12074 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
12075 if tdir.exists() {
12076 candidates.push(Candidate::Directory(tdir));
12077 }
12078 }
12079 }
12080 }
12081 drop(cat);
12082
12083 let cat = self.catalog.read();
12088 for (entry_index, entry) in cat.tables.iter().enumerate() {
12089 if entry_index % 256 == 0 {
12090 control.checkpoint()?;
12091 }
12092 if !matches!(entry.state, TableState::Live) {
12093 continue;
12094 }
12095 let txn_dir = self
12096 .root
12097 .join(TABLES_DIR)
12098 .join(entry.table_id.to_string())
12099 .join("_txn");
12100 if !txn_dir.exists() {
12101 continue;
12102 }
12103 for (sub_index, sub) in std::fs::read_dir(&txn_dir)?.enumerate() {
12104 if sub_index % 256 == 0 {
12105 control.checkpoint()?;
12106 }
12107 let sub = sub?;
12108 let name = sub.file_name();
12109 let Some(name) = name.to_str() else { continue };
12110 let is_active = name
12112 .parse::<u64>()
12113 .map(|id| self.active_spills.is_active(id))
12114 .unwrap_or(false);
12115 if is_active {
12116 continue;
12117 }
12118 candidates.push(Candidate::Directory(sub.path()));
12119 }
12120 }
12121 drop(cat);
12122
12123 let external_names = {
12124 let cat = self.catalog.read();
12125 cat.external_tables
12126 .iter()
12127 .map(|entry| entry.name.clone())
12128 .collect::<std::collections::HashSet<_>>()
12129 };
12130 let vtab_dir = self.root.join(VTAB_DIR);
12131 if vtab_dir.exists() {
12132 for (entry_index, entry) in std::fs::read_dir(&vtab_dir)?.enumerate() {
12133 if entry_index % 256 == 0 {
12134 control.checkpoint()?;
12135 }
12136 let entry = entry?;
12137 let name = entry.file_name();
12138 let Some(name) = name.to_str() else { continue };
12139 if external_names.contains(name) {
12140 continue;
12141 }
12142 let path = entry.path();
12143 if path.is_dir() {
12144 candidates.push(Candidate::Directory(path));
12145 } else {
12146 candidates.push(Candidate::File(path));
12147 }
12148 }
12149 }
12150
12151 let tables = self
12155 .tables
12156 .read()
12157 .iter()
12158 .map(|(table_id, handle)| (*table_id, handle.clone()))
12159 .collect::<Vec<_>>();
12160 let mut retiring = Vec::new();
12161 for (table_index, (table_id, handle)) in tables.iter().enumerate() {
12162 if table_index % 256 == 0 {
12163 control.checkpoint()?;
12164 }
12165 let backup_pinned: HashSet<u128> = self
12166 .backup_pins
12167 .lock()
12168 .keys()
12169 .filter_map(|(pinned_table, run_id)| {
12170 (*pinned_table == *table_id).then_some(*run_id)
12171 })
12172 .collect();
12173 if handle
12174 .lock()
12175 .has_reapable_retiring(Epoch(min_active), &backup_pinned)
12176 {
12177 retiring.push((handle.clone(), backup_pinned));
12178 }
12179 }
12180
12181 let all_durable = self.active_spills.is_idle()
12189 && tables.iter().all(|(_, handle)| {
12190 let g = handle.lock();
12191 g.memtable_len() == 0 && g.mutable_run_len() == 0
12192 });
12193 let retain = self
12194 .replication_wal_retention_segments
12195 .load(std::sync::atomic::Ordering::Relaxed);
12196 let reap_wal = all_durable
12197 && self
12198 .shared_wal
12199 .lock()
12200 .has_gc_segments_retain_recent(retain)?;
12201
12202 if candidates.is_empty() && retiring.is_empty() && !reap_wal {
12203 return Ok((0, None));
12204 }
12205 control.checkpoint()?;
12206 if !before_publish() {
12207 return Err(MongrelError::Cancelled);
12208 }
12209
12210 let mut reclaimed = 0;
12211 for candidate in candidates {
12212 match candidate {
12213 Candidate::Directory(path) => std::fs::remove_dir_all(path)?,
12214 Candidate::File(path) => std::fs::remove_file(path)?,
12215 }
12216 reclaimed += 1;
12217 }
12218 for (handle, backup_pinned) in retiring {
12219 reclaimed += handle
12220 .lock()
12221 .reap_retiring(Epoch(min_active), &backup_pinned)?;
12222 }
12223 if reap_wal {
12224 reclaimed += self
12225 .shared_wal
12226 .lock()
12227 .gc_segments_retain_recent(u64::MAX, retain)?;
12228 }
12229
12230 Ok((
12231 reclaimed,
12232 Some(MaintenanceReceipt {
12233 epoch: maintenance_epoch,
12234 }),
12235 ))
12236 }
12237
12238 pub fn checkpoint(&self) -> Result<()> {
12256 self.checkpoint_controlled(|| Ok(()))
12257 }
12258
12259 #[doc(hidden)]
12262 pub fn checkpoint_controlled<F>(&self, before_wal_reset: F) -> Result<()>
12263 where
12264 F: FnOnce() -> Result<()>,
12265 {
12266 self.require(&crate::auth::Permission::Ddl)?;
12267 let _replication = self.replication_barrier.write();
12271 let _ddl = self.ddl_lock.lock();
12272 let _security = self.security_coordinator.gate.read();
12273 self.require(&crate::auth::Permission::Ddl)?;
12274
12275 let mut handles = self
12276 .tables
12277 .read()
12278 .iter()
12279 .map(|(table_id, handle)| (*table_id, handle.clone()))
12280 .collect::<Vec<_>>();
12281 handles.sort_by_key(|(table_id, _)| *table_id);
12282 let mut tables = handles
12283 .iter()
12284 .map(|(table_id, handle)| (*table_id, handle.lock()))
12285 .collect::<Vec<_>>();
12286
12287 for (_, table) in &mut tables {
12289 if table.has_pending_writes() || table.memtable_len() > 0 || table.mutable_run_len() > 0
12290 {
12291 table.force_flush()?;
12292 }
12293 }
12294
12295 for (_, table) in &mut tables {
12298 if table.run_count() >= 2 || table.should_compact() {
12299 table.compact()?;
12300 }
12301 }
12302
12303 before_wal_reset()?;
12304
12305 let maintenance_epoch = self.epoch.visible();
12307 let min_active = self.snapshots.min_active(maintenance_epoch);
12308 for (table_id, table) in &mut tables {
12309 let backup_pinned: HashSet<u128> = self
12310 .backup_pins
12311 .lock()
12312 .keys()
12313 .filter_map(|(pinned_table, run_id)| {
12314 (*pinned_table == *table_id).then_some(*run_id)
12315 })
12316 .collect();
12317 table.reap_retiring(min_active, &backup_pinned)?;
12318 }
12319
12320 self.shared_wal.lock().reset_after_checkpoint()?;
12323
12324 let catalog_snapshot = self.catalog.read().clone();
12326 for entry in &catalog_snapshot.tables {
12327 if matches!(entry.state, TableState::Dropped { at_epoch } if at_epoch <= min_active.0) {
12328 crate::durable_file::remove_directory_all(
12329 &self.root.join(TABLES_DIR).join(entry.table_id.to_string()),
12330 )?;
12331 }
12332 if !matches!(entry.state, TableState::Live) {
12333 continue;
12334 }
12335 let transaction_dir = self
12336 .root
12337 .join(TABLES_DIR)
12338 .join(entry.table_id.to_string())
12339 .join("_txn");
12340 if transaction_dir.is_dir() {
12341 for child in std::fs::read_dir(&transaction_dir)? {
12342 let child = child?;
12343 let active = child
12344 .file_name()
12345 .to_str()
12346 .and_then(|name| name.parse::<u64>().ok())
12347 .is_some_and(|txn_id| self.active_spills.is_active(txn_id));
12348 if !active {
12349 crate::durable_file::remove_directory_all(&child.path())?;
12350 }
12351 }
12352 }
12353 }
12354 let external_names = catalog_snapshot
12355 .external_tables
12356 .iter()
12357 .map(|entry| entry.name.as_str())
12358 .collect::<HashSet<_>>();
12359 let external_root = self.root.join(VTAB_DIR);
12360 if external_root.is_dir() {
12361 for entry in std::fs::read_dir(&external_root)? {
12362 let entry = entry?;
12363 let name = entry.file_name();
12364 if name
12365 .to_str()
12366 .is_some_and(|name| external_names.contains(name))
12367 {
12368 continue;
12369 }
12370 if entry.file_type()?.is_dir() {
12371 crate::durable_file::remove_directory_all(&entry.path())?;
12372 } else {
12373 std::fs::remove_file(entry.path())?;
12374 crate::durable_file::sync_directory(&external_root)?;
12375 }
12376 }
12377 }
12378
12379 catalog::write_atomic(&self.root, &catalog_snapshot, self.meta_dek.as_ref())?;
12382 let visible = self.epoch.visible();
12383 for (_, table) in &tables {
12384 table.persist_manifest(visible)?;
12385 }
12386
12387 Ok(())
12388 }
12389 fn alloc_txn_id(&self) -> Result<u64> {
12390 self.ensure_owner_process()?;
12391 crate::txn::allocate_txn_id(&self.next_txn_id)
12392 }
12393
12394 pub fn set_spill_threshold(&self, bytes: u64) {
12398 self.spill_threshold
12399 .store(bytes, std::sync::atomic::Ordering::Relaxed);
12400 }
12401
12402 #[doc(hidden)]
12406 pub fn __set_spill_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12407 *self.spill_hook.lock() = Some(Box::new(f));
12408 }
12409
12410 #[doc(hidden)]
12413 pub fn __set_security_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12414 *self.security_commit_hook.lock() = Some(Box::new(f));
12415 }
12416
12417 #[doc(hidden)]
12420 pub fn __set_catalog_commit_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12421 *self.catalog_commit_hook.lock() = Some(Box::new(f));
12422 }
12423
12424 #[doc(hidden)]
12427 pub fn __set_backup_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12428 *self.backup_hook.lock() = Some(Box::new(f));
12429 }
12430
12431 #[doc(hidden)]
12433 pub fn __set_replication_hook(&self, f: impl Fn() + Send + Sync + 'static) {
12434 *self.replication_hook.lock() = Some(Box::new(f));
12435 }
12436
12437 #[doc(hidden)]
12441 pub fn __wal_group_sync_count(&self) -> u64 {
12442 self.shared_wal.lock().group_sync_count()
12443 }
12444
12445 #[doc(hidden)]
12448 pub fn __poison(&self) {
12449 self.poisoned
12450 .store(true, std::sync::atomic::Ordering::Relaxed);
12451 }
12452
12453 pub fn check(&self) -> Vec<CheckIssue> {
12466 match self.check_inner(None) {
12467 Ok(issues) => issues,
12468 Err(error) => vec![CheckIssue {
12469 table_id: WAL_TABLE_ID,
12470 table_name: "shared WAL".into(),
12471 severity: "error".into(),
12472 description: error.to_string(),
12473 }],
12474 }
12475 }
12476
12477 #[doc(hidden)]
12479 pub fn check_controlled(&self, control: &crate::ExecutionControl) -> Result<Vec<CheckIssue>> {
12480 self.check_inner(Some(control))
12481 }
12482
12483 fn check_inner(&self, control: Option<&crate::ExecutionControl>) -> Result<Vec<CheckIssue>> {
12484 let mut issues = Vec::new();
12485 let cat = self.catalog.read();
12486 let manifest_meta_dek = crate::encryption::meta_dek_for(self.kek.as_deref());
12487 for (table_index, entry) in cat.tables.iter().enumerate() {
12488 if table_index % 256 == 0 {
12489 if let Some(control) = control {
12490 control.checkpoint()?;
12491 }
12492 }
12493 if !matches!(entry.state, TableState::Live) {
12494 continue;
12495 }
12496 let tdir = self.root.join(TABLES_DIR).join(entry.table_id.to_string());
12497 let mut err = |sev: &str, desc: String| {
12498 issues.push(CheckIssue {
12499 table_id: entry.table_id,
12500 table_name: entry.name.clone(),
12501 severity: sev.into(),
12502 description: desc,
12503 });
12504 };
12505 let m = match crate::manifest::read(&tdir, manifest_meta_dek.as_ref()) {
12506 Ok(m) => m,
12507 Err(e) => {
12508 err("error", format!("manifest read failed: {e}"));
12509 continue;
12510 }
12511 };
12512 if m.flushed_epoch > m.current_epoch {
12513 err(
12514 "error",
12515 format!(
12516 "flushed_epoch {} exceeds current_epoch {} (impossible)",
12517 m.flushed_epoch, m.current_epoch
12518 ),
12519 );
12520 }
12521
12522 let runs_dir = tdir.join(crate::engine::RUNS_DIR);
12523 let mut referenced: std::collections::HashSet<u128> = std::collections::HashSet::new();
12524 for (run_index, rr) in m.runs.iter().enumerate() {
12525 if run_index % 256 == 0 {
12526 if let Some(control) = control {
12527 control.checkpoint()?;
12528 }
12529 }
12530 referenced.insert(rr.run_id);
12531 let run_path = runs_dir.join(format!("r-{}.sr", rr.run_id));
12532 if !run_path.exists() {
12533 err("error", format!("missing run file: r-{}.sr", rr.run_id));
12534 continue;
12535 }
12536 match crate::sorted_run::RunReader::open(
12537 &run_path,
12538 entry.schema.clone(),
12539 self.kek.clone(),
12540 ) {
12541 Ok(reader) => {
12542 if reader.row_count() as u64 != rr.row_count {
12543 err(
12544 "error",
12545 format!(
12546 "run r-{} row count mismatch: manifest {} vs run {}",
12547 rr.run_id,
12548 rr.row_count,
12549 reader.row_count()
12550 ),
12551 );
12552 }
12553 }
12554 Err(e) => {
12555 err(
12556 "error",
12557 format!("run r-{} integrity check failed: {e}", rr.run_id),
12558 );
12559 }
12560 }
12561 }
12562
12563 for r in &m.retiring {
12567 referenced.insert(r.run_id);
12568 }
12569
12570 if let Ok(rd) = std::fs::read_dir(&runs_dir) {
12572 for (entry_index, ent) in rd.flatten().enumerate() {
12573 if entry_index % 256 == 0 {
12574 if let Some(control) = control {
12575 control.checkpoint()?;
12576 }
12577 }
12578 let p = ent.path();
12579 if p.extension().and_then(|s| s.to_str()) != Some("sr") {
12580 continue;
12581 }
12582 let run_id = p
12583 .file_stem()
12584 .and_then(|s| s.to_str())
12585 .and_then(|s| s.strip_prefix("r-"))
12586 .and_then(|s| s.parse::<u128>().ok());
12587 if let Some(id) = run_id {
12588 if !referenced.contains(&id) {
12589 err(
12590 "warning",
12591 format!("orphan run file r-{id}.sr not referenced by the manifest"),
12592 );
12593 }
12594 }
12595 }
12596 }
12597 }
12598
12599 let external_names = cat
12600 .external_tables
12601 .iter()
12602 .map(|entry| entry.name.clone())
12603 .collect::<std::collections::HashSet<_>>();
12604 let vtab_dir = self.root.join(VTAB_DIR);
12605 if let Ok(entries) = std::fs::read_dir(&vtab_dir) {
12606 for (entry_index, entry) in entries.flatten().enumerate() {
12607 if entry_index % 256 == 0 {
12608 if let Some(control) = control {
12609 control.checkpoint()?;
12610 }
12611 }
12612 let name = entry.file_name();
12613 let Some(name) = name.to_str() else { continue };
12614 if !external_names.contains(name) {
12615 issues.push(CheckIssue {
12616 table_id: EXTERNAL_TABLE_ID,
12617 table_name: name.to_string(),
12618 severity: "warning".into(),
12619 description: format!(
12620 "orphan external table state entry {:?} not referenced by the catalog",
12621 entry.path()
12622 ),
12623 });
12624 }
12625 }
12626 }
12627
12628 if let Some(control) = control {
12635 control.checkpoint()?;
12636 }
12637 for (seg, msg) in self.shared_wal.lock().verify_segments() {
12638 issues.push(CheckIssue {
12639 table_id: WAL_TABLE_ID,
12640 table_name: "<wal>".into(),
12641 severity: "error".into(),
12642 description: format!("WAL segment seg-{seg:06}.wal failed integrity check: {msg}"),
12643 });
12644 }
12645 Ok(issues)
12646 }
12647
12648 pub fn doctor(&self) -> Result<Vec<u64>> {
12652 let control = crate::ExecutionControl::new(None);
12653 self.doctor_controlled(&control, || true)
12654 }
12655
12656 #[doc(hidden)]
12660 pub fn doctor_controlled<F>(
12661 &self,
12662 control: &crate::ExecutionControl,
12663 before_publish: F,
12664 ) -> Result<Vec<u64>>
12665 where
12666 F: FnOnce() -> bool,
12667 {
12668 self.doctor_controlled_with_receipt(control, before_publish)
12669 .map(|(quarantined, _)| quarantined)
12670 }
12671
12672 #[doc(hidden)]
12675 pub fn doctor_controlled_with_receipt<F>(
12676 &self,
12677 control: &crate::ExecutionControl,
12678 before_publish: F,
12679 ) -> Result<(Vec<u64>, Option<MaintenanceReceipt>)>
12680 where
12681 F: FnOnce() -> bool,
12682 {
12683 let _ddl = self.ddl_lock.lock();
12686 let _security_write = self.security_write()?;
12687 let issues = self.check_inner(Some(control))?;
12688 let bad_tables: std::collections::HashSet<u64> = issues
12693 .iter()
12694 .filter(|i| {
12695 i.severity == "error"
12696 && i.table_id != WAL_TABLE_ID
12697 && i.table_id != EXTERNAL_TABLE_ID
12698 })
12699 .map(|i| i.table_id)
12700 .collect();
12701 if bad_tables.is_empty() {
12702 return Ok((Vec::new(), None));
12703 }
12704 let _commit = self.commit_lock.lock();
12705 control.checkpoint()?;
12706 if !before_publish() {
12707 return Err(MongrelError::Cancelled);
12708 }
12709 let maintenance_epoch = self.epoch.bump_assigned();
12710 let mut epoch_guard = EpochGuard::new(self.epoch.as_ref(), maintenance_epoch);
12711
12712 let qdir = self.root.join("_quarantine");
12713 crate::durable_file::create_directory(&qdir)?;
12714 let mut bad_tables = bad_tables.into_iter().collect::<Vec<_>>();
12715 bad_tables.sort_unstable();
12716
12717 let mut handles = self
12721 .tables
12722 .read()
12723 .iter()
12724 .filter(|(table_id, _)| bad_tables.binary_search(table_id).is_ok())
12725 .map(|(table_id, handle)| (*table_id, handle.clone()))
12726 .collect::<Vec<_>>();
12727 handles.sort_by_key(|(table_id, _)| *table_id);
12728 let mut table_guards = handles
12729 .iter()
12730 .map(|(table_id, handle)| (*table_id, handle.lock()))
12731 .collect::<Vec<_>>();
12732
12733 let mut next_catalog = self.catalog.read().clone();
12734 for table_id in &bad_tables {
12735 if let Some(entry) = next_catalog
12736 .tables
12737 .iter_mut()
12738 .find(|entry| entry.table_id == *table_id)
12739 {
12740 entry.state = TableState::Dropped {
12741 at_epoch: maintenance_epoch.0,
12742 };
12743 }
12744 }
12745 next_catalog.db_epoch = next_catalog.db_epoch.max(maintenance_epoch.0);
12746
12747 let txn_id = self.alloc_txn_id()?;
12748 let commit_seq = {
12749 let mut wal = self.shared_wal.lock();
12750 let append: Result<u64> = (|| {
12751 for table_id in &bad_tables {
12752 wal.append(
12753 txn_id,
12754 *table_id,
12755 crate::wal::Op::Ddl(crate::wal::DdlOp::DropTable {
12756 table_id: *table_id,
12757 }),
12758 )?;
12759 }
12760 append_catalog_snapshot(&mut wal, txn_id, &next_catalog)?;
12761 wal.append_commit(txn_id, maintenance_epoch, &[])
12762 })();
12763 append.map_err(|error| self.commit_outcome_unknown(maintenance_epoch, error))?
12764 };
12765 self.await_durable_commit(commit_seq, maintenance_epoch)?;
12766 for (_, table) in &mut table_guards {
12767 table.mark_unavailable_after_quarantine();
12768 }
12769 {
12770 let mut live_tables = self.tables.write();
12771 for table_id in &bad_tables {
12772 live_tables.remove(table_id);
12773 }
12774 }
12775 let checkpoint = self.checkpoint_catalog_after_durable(next_catalog);
12776 self.finish_durable_publish(maintenance_epoch, &mut epoch_guard, checkpoint)?;
12777
12778 for table_id in &bad_tables {
12782 let source = self.root.join(TABLES_DIR).join(table_id.to_string());
12783 if source.exists() {
12784 let destination = qdir.join(table_id.to_string());
12785 if let Err(error) = crate::durable_file::rename(&source, &destination) {
12786 return Err(MongrelError::DurableCommit {
12787 epoch: maintenance_epoch.0,
12788 message: format!(
12789 "DOCTOR dropped table {table_id} but quarantine move failed: {error}"
12790 ),
12791 });
12792 }
12793 }
12794 }
12795 Ok((
12796 bad_tables,
12797 Some(MaintenanceReceipt {
12798 epoch: maintenance_epoch,
12799 }),
12800 ))
12801 }
12802
12803 #[allow(dead_code)]
12805 pub(crate) fn kek(&self) -> Option<&Arc<crate::encryption::Kek>> {
12806 self.kek.as_ref()
12807 }
12808
12809 #[allow(dead_code)]
12811 pub(crate) fn epoch_authority(&self) -> &Arc<EpochAuthority> {
12812 &self.epoch
12813 }
12814
12815 #[allow(dead_code)]
12817 pub(crate) fn snapshots(&self) -> &Arc<SnapshotRegistry> {
12818 &self.snapshots
12819 }
12820}
12821
12822fn external_state_dir(root: &Path, name: &str) -> PathBuf {
12823 root.join(VTAB_DIR).join(name)
12824}
12825
12826fn append_catalog_snapshot(
12827 wal: &mut crate::wal::SharedWal,
12828 txn_id: u64,
12829 catalog: &Catalog,
12830) -> Result<()> {
12831 let catalog_json = crate::wal::DdlOp::encode_catalog(catalog)?;
12832 wal.append(
12833 txn_id,
12834 WAL_TABLE_ID,
12835 crate::wal::Op::Ddl(crate::wal::DdlOp::CatalogSnapshot { catalog_json }),
12836 )?;
12837 Ok(())
12838}
12839
12840fn filter_ignored_staging(
12841 staging: Vec<(u64, crate::txn::Staged)>,
12842 ignored_indices: &std::collections::BTreeSet<usize>,
12843) -> Vec<(u64, crate::txn::Staged)> {
12844 if ignored_indices.is_empty() {
12845 return staging;
12846 }
12847 staging
12848 .into_iter()
12849 .enumerate()
12850 .filter_map(|(idx, staged)| (!ignored_indices.contains(&idx)).then_some(staged))
12851 .collect()
12852}
12853
12854fn external_state_file(root: &Path, name: &str) -> PathBuf {
12855 external_state_dir(root, name).join("state.json")
12856}
12857
12858fn read_external_state_file(root: &Path, name: &str) -> Result<Vec<u8>> {
12859 let path = external_state_file(root, name);
12860 match std::fs::read(path) {
12861 Ok(bytes) => Ok(bytes),
12862 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
12863 Err(e) => Err(e.into()),
12864 }
12865}
12866
12867fn current_external_state_bytes(
12868 root: &Path,
12869 external_states: &[(String, Vec<u8>)],
12870 name: &str,
12871) -> Result<Vec<u8>> {
12872 for (table, state) in external_states.iter().rev() {
12873 if table == name {
12874 return Ok(state.clone());
12875 }
12876 }
12877 read_external_state_file(root, name)
12878}
12879
12880fn dedup_external_states(external_states: Vec<(String, Vec<u8>)>) -> Vec<(String, Vec<u8>)> {
12881 let mut out = external_states;
12882 dedup_external_states_in_place(&mut out);
12883 out
12884}
12885
12886fn dedup_external_states_in_place(external_states: &mut Vec<(String, Vec<u8>)>) {
12887 let mut seen = std::collections::HashSet::new();
12888 let mut out = Vec::with_capacity(external_states.len());
12889 for (name, state) in std::mem::take(external_states).into_iter().rev() {
12890 if seen.insert(name.clone()) {
12891 out.push((name, state));
12892 }
12893 }
12894 out.reverse();
12895 *external_states = out;
12896}
12897
12898fn prepare_external_state_file(
12899 root: &Path,
12900 name: &str,
12901 state: &[u8],
12902 txn_id: u64,
12903) -> Result<PathBuf> {
12904 crate::durable_file::create_directory(&root.join(VTAB_DIR))?;
12905 let dir = external_state_dir(root, name);
12906 crate::durable_file::create_directory(&dir)?;
12907 let pending = dir.join(format!("state.json.{txn_id}.tmp"));
12908 {
12909 let mut file = std::fs::OpenOptions::new()
12910 .create_new(true)
12911 .write(true)
12912 .open(&pending)?;
12913 file.write_all(state)?;
12914 file.sync_all()?;
12915 }
12916 Ok(pending)
12917}
12918
12919fn publish_external_state_file(root: &Path, name: &str, pending: &Path) -> Result<()> {
12920 let path = external_state_file(root, name);
12921 crate::durable_file::replace(pending, &path)?;
12922 Ok(())
12923}
12924
12925fn write_external_state_file(
12926 durable: &crate::durable_file::DurableRoot,
12927 name: &str,
12928 state: &[u8],
12929) -> Result<()> {
12930 let directory = Path::new(VTAB_DIR).join(name);
12931 durable.create_directory_all(&directory)?;
12932 durable.write_atomic(directory.join("state.json"), state)?;
12933 Ok(())
12934}
12935
12936fn validate_recovered_data_table(
12937 catalog: &Catalog,
12938 tables: &HashMap<u64, TableHandle>,
12939 table_id: u64,
12940 commit_epoch: u64,
12941 offset: u64,
12942) -> Result<bool> {
12943 let entry = catalog
12944 .tables
12945 .iter()
12946 .find(|entry| entry.table_id == table_id)
12947 .ok_or_else(|| MongrelError::CorruptWal {
12948 offset,
12949 reason: format!("committed record references unknown table {table_id}"),
12950 })?;
12951 if commit_epoch < entry.created_epoch {
12952 return Err(MongrelError::CorruptWal {
12953 offset,
12954 reason: format!(
12955 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
12956 entry.created_epoch
12957 ),
12958 });
12959 }
12960 match entry.state {
12961 TableState::Dropped { at_epoch } => {
12962 let abandoned_build_boundary =
12967 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
12968 if commit_epoch >= at_epoch && !abandoned_build_boundary {
12969 Err(MongrelError::CorruptWal {
12970 offset,
12971 reason: format!(
12972 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
12973 ),
12974 })
12975 } else {
12976 Ok(false)
12977 }
12978 }
12979 TableState::Live | TableState::Building { .. } => {
12980 if tables.contains_key(&table_id) {
12981 Ok(true)
12982 } else {
12983 Err(MongrelError::CorruptWal {
12984 offset,
12985 reason: format!("live table {table_id} has no mounted recovery handle"),
12986 })
12987 }
12988 }
12989 }
12990}
12991
12992type RecoveryTableStage = (
12993 Vec<crate::memtable::Row>,
12994 Vec<(crate::rowid::RowId, Epoch)>,
12995 Option<Epoch>,
12996 Epoch,
12997);
12998
12999#[derive(Clone)]
13000struct RecoveryValidationTable {
13001 schema: Schema,
13002 flushed_epoch: u64,
13003}
13004
13005fn validate_shared_wal_recovery_plan(
13006 durable_root: &crate::durable_file::DurableRoot,
13007 catalog: &Catalog,
13008 recovered_table_ids: &HashSet<u64>,
13009 reconciled_table_ids: &HashSet<u64>,
13010 meta_dek: Option<&[u8; META_DEK_LEN]>,
13011 kek: Option<Arc<crate::encryption::Kek>>,
13012 records: &[crate::wal::Record],
13013) -> Result<()> {
13014 use crate::wal::{DdlOp, Op};
13015
13016 let mut tables = HashMap::<u64, RecoveryValidationTable>::new();
13017 for entry in &catalog.tables {
13018 if !matches!(entry.state, TableState::Live) {
13019 continue;
13020 }
13021 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
13022 let manifest = match crate::manifest::read_durable(durable_root, &relative_dir, meta_dek) {
13023 Ok(manifest) => Some(manifest),
13024 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
13025 Err(error) => return Err(error),
13026 };
13027 let flushed_epoch = if let Some(manifest) = manifest {
13028 if manifest.table_id != entry.table_id {
13029 return Err(MongrelError::Conflict(format!(
13030 "catalog table {} storage identity mismatch",
13031 entry.table_id
13032 )));
13033 }
13034 if (manifest.schema_id != entry.schema.schema_id
13035 && !reconciled_table_ids.contains(&entry.table_id))
13036 || manifest.flushed_epoch > manifest.current_epoch
13037 || manifest.global_idx_epoch > manifest.current_epoch
13038 || manifest.next_row_id == u64::MAX
13039 || manifest.auto_inc_next < 0
13040 || manifest.auto_inc_next == i64::MAX
13041 || (entry.schema.auto_increment_column().is_none() && manifest.auto_inc_next != 0)
13042 {
13043 return Err(MongrelError::InvalidArgument(format!(
13044 "table {} manifest counters or schema identity are invalid",
13045 entry.table_id
13046 )));
13047 }
13048 #[cfg(feature = "encryption")]
13049 let idx_dek = kek.as_ref().map(|key| key.derive_idx_key());
13050 #[cfg(not(feature = "encryption"))]
13051 let idx_dek: Option<zeroize::Zeroizing<[u8; 32]>> = None;
13052 crate::global_idx::read_durable_for(
13053 durable_root,
13054 &relative_dir,
13055 entry.table_id,
13056 &entry.schema,
13057 idx_dek.as_deref(),
13058 )?;
13059 let mut run_ids = HashSet::new();
13060 let mut maximum_row_id = None::<u64>;
13061 for run in &manifest.runs {
13062 if run.run_id >= u64::MAX as u128
13063 || run.epoch_created > manifest.current_epoch
13064 || !run_ids.insert(run.run_id)
13065 {
13066 return Err(MongrelError::InvalidArgument(format!(
13067 "table {} manifest contains an invalid or duplicate run id",
13068 entry.table_id
13069 )));
13070 }
13071 let relative = relative_dir
13072 .join(crate::engine::RUNS_DIR)
13073 .join(format!("r-{}.sr", run.run_id as u64));
13074 let file = durable_root.open_regular(&relative)?;
13075 let mut reader = crate::sorted_run::RunReader::open_file(
13076 file,
13077 entry.schema.clone(),
13078 kek.clone(),
13079 )?;
13080 let header = reader.header();
13081 if header.run_id != run.run_id
13082 || header.level != run.level
13083 || header.row_count != run.row_count
13084 || !header.is_uniform_epoch() && header.epoch_created != run.epoch_created
13085 || header.is_uniform_epoch() && header.epoch_created != 0
13086 || header.schema_id > entry.schema.schema_id
13087 {
13088 return Err(MongrelError::InvalidArgument(format!(
13089 "table {} run {} differs from its manifest: header=(id {}, level {}, rows {}, epoch {}, schema {}), manifest=(id {}, level {}, rows {}, epoch {}, schema <= {})",
13090 entry.table_id,
13091 run.run_id,
13092 header.run_id,
13093 header.level,
13094 header.row_count,
13095 header.epoch_created,
13096 header.schema_id,
13097 run.run_id,
13098 run.level,
13099 run.row_count,
13100 run.epoch_created,
13101 entry.schema.schema_id,
13102 )));
13103 }
13104 if header.row_count != 0 {
13105 maximum_row_id = Some(
13106 maximum_row_id
13107 .map_or(header.max_row_id, |value| value.max(header.max_row_id)),
13108 );
13109 }
13110 reader.validate_all_pages()?;
13111 }
13112 if maximum_row_id.is_some_and(|maximum| manifest.next_row_id <= maximum) {
13113 return Err(MongrelError::InvalidArgument(format!(
13114 "table {} next_row_id does not advance beyond persisted rows",
13115 entry.table_id
13116 )));
13117 }
13118 for run in &manifest.retiring {
13119 if run.run_id >= u64::MAX as u128
13120 || run.retire_epoch > manifest.current_epoch
13121 || !run_ids.insert(run.run_id)
13122 {
13123 return Err(MongrelError::InvalidArgument(format!(
13124 "table {} manifest contains an invalid or aliased retired run",
13125 entry.table_id
13126 )));
13127 }
13128 }
13129 manifest.flushed_epoch
13130 } else {
13131 if !recovered_table_ids.contains(&entry.table_id) {
13132 return Err(MongrelError::NotFound(format!(
13133 "live table {} manifest is missing",
13134 entry.table_id
13135 )));
13136 }
13137 0
13138 };
13139 tables.insert(
13140 entry.table_id,
13141 RecoveryValidationTable {
13142 schema: entry.schema.clone(),
13143 flushed_epoch,
13144 },
13145 );
13146 }
13147
13148 let committed = records
13149 .iter()
13150 .filter_map(|record| match record.op {
13151 Op::TxnCommit { epoch, .. } => Some((record.txn_id, epoch)),
13152 _ => None,
13153 })
13154 .collect::<HashMap<_, _>>();
13155 let mut run_ids = HashSet::new();
13156 let mut recovered_row_ids = HashMap::<u64, HashSet<u64>>::new();
13157 for record in records {
13158 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
13159 continue;
13160 };
13161 match &record.op {
13162 Op::Put { table_id, rows } => {
13163 let table = validate_recovery_data_table_plan(
13164 catalog,
13165 &tables,
13166 *table_id,
13167 commit_epoch,
13168 record.seq.0,
13169 )?;
13170 let decoded: Vec<crate::memtable::Row> =
13171 bincode::deserialize(rows).map_err(|error| MongrelError::CorruptWal {
13172 offset: record.seq.0,
13173 reason: format!(
13174 "committed Put payload for transaction {} could not be decoded: {error}",
13175 record.txn_id
13176 ),
13177 })?;
13178 if let Some(table) = table {
13179 for row in &decoded {
13180 if !recovered_row_ids
13181 .entry(*table_id)
13182 .or_default()
13183 .insert(row.row_id.0)
13184 {
13185 return Err(MongrelError::CorruptWal {
13186 offset: record.seq.0,
13187 reason: format!(
13188 "committed WAL repeats recovered row id {} for table {table_id}",
13189 row.row_id.0
13190 ),
13191 });
13192 }
13193 validate_recovered_row(&table.schema, row)?;
13194 }
13195 }
13196 }
13197 Op::Delete { table_id, .. } | Op::TruncateTable { table_id } => {
13198 validate_recovery_data_table_plan(
13199 catalog,
13200 &tables,
13201 *table_id,
13202 commit_epoch,
13203 record.seq.0,
13204 )?;
13205 }
13206 Op::ExternalTableState { name, .. } => validate_recovered_external_name(name)?,
13207 Op::Ddl(DdlOp::ResetExternalTableState {
13208 name,
13209 generation_epoch,
13210 }) => {
13211 if *generation_epoch != commit_epoch {
13212 return Err(MongrelError::CorruptWal {
13213 offset: record.seq.0,
13214 reason: format!(
13215 "external state reset epoch {generation_epoch} does not match WAL commit epoch {commit_epoch}"
13216 ),
13217 });
13218 }
13219 validate_recovered_external_name(name)?;
13220 }
13221 Op::TxnCommit { added_runs, .. } => {
13222 for added in added_runs {
13223 let Some(table) = validate_recovery_data_table_plan(
13224 catalog,
13225 &tables,
13226 added.table_id,
13227 commit_epoch,
13228 record.seq.0,
13229 )?
13230 else {
13231 continue;
13232 };
13233 if added.run_id >= u64::MAX as u128
13234 || !run_ids.insert((added.table_id, added.run_id))
13235 {
13236 return Err(MongrelError::CorruptWal {
13237 offset: record.seq.0,
13238 reason: format!(
13239 "duplicate or invalid recovered run {} for table {}",
13240 added.run_id, added.table_id
13241 ),
13242 });
13243 }
13244 if commit_epoch <= table.flushed_epoch {
13245 continue;
13246 }
13247 validate_planned_spilled_run(
13248 durable_root,
13249 record.txn_id,
13250 commit_epoch,
13251 added,
13252 &table.schema,
13253 kek.clone(),
13254 )?;
13255 }
13256 }
13257 _ => {}
13258 }
13259 }
13260 Ok(())
13261}
13262
13263fn validate_recovery_data_table_plan<'a>(
13264 catalog: &Catalog,
13265 tables: &'a HashMap<u64, RecoveryValidationTable>,
13266 table_id: u64,
13267 commit_epoch: u64,
13268 offset: u64,
13269) -> Result<Option<&'a RecoveryValidationTable>> {
13270 let entry = catalog
13271 .tables
13272 .iter()
13273 .find(|entry| entry.table_id == table_id)
13274 .ok_or_else(|| MongrelError::CorruptWal {
13275 offset,
13276 reason: format!("committed record references unknown table {table_id}"),
13277 })?;
13278 if commit_epoch < entry.created_epoch {
13279 return Err(MongrelError::CorruptWal {
13280 offset,
13281 reason: format!(
13282 "table {table_id} record epoch {commit_epoch} precedes creation epoch {}",
13283 entry.created_epoch
13284 ),
13285 });
13286 }
13287 match entry.state {
13288 TableState::Dropped { at_epoch } => {
13289 let abandoned =
13290 entry.name.starts_with(CTAS_BUILD_TABLE_PREFIX) && commit_epoch == at_epoch;
13291 if commit_epoch >= at_epoch && !abandoned {
13292 return Err(MongrelError::CorruptWal {
13293 offset,
13294 reason: format!(
13295 "table {table_id} record epoch {commit_epoch} is not before drop epoch {at_epoch}"
13296 ),
13297 });
13298 }
13299 Ok(None)
13300 }
13301 TableState::Live => {
13302 tables
13303 .get(&table_id)
13304 .map(Some)
13305 .ok_or_else(|| MongrelError::CorruptWal {
13306 offset,
13307 reason: format!("live table {table_id} has no recovery plan"),
13308 })
13309 }
13310 TableState::Building { .. } => Err(MongrelError::CorruptWal {
13311 offset,
13312 reason: format!("building table {table_id} was not normalized before recovery"),
13313 }),
13314 }
13315}
13316
13317fn validate_planned_spilled_run(
13318 root: &crate::durable_file::DurableRoot,
13319 txn_id: u64,
13320 commit_epoch: u64,
13321 added: &crate::wal::AddedRun,
13322 schema: &Schema,
13323 kek: Option<Arc<crate::encryption::Kek>>,
13324) -> Result<()> {
13325 let table = Path::new(TABLES_DIR).join(added.table_id.to_string());
13326 let destination = table
13327 .join(crate::engine::RUNS_DIR)
13328 .join(format!("r-{}.sr", added.run_id as u64));
13329 let pending = table
13330 .join("_txn")
13331 .join(txn_id.to_string())
13332 .join(format!("r-{}.sr", added.run_id as u64));
13333 let file = match root.open_regular(&destination) {
13334 Ok(file) => file,
13335 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
13336 root.open_regular(&pending).map_err(|pending_error| {
13337 if pending_error.kind() == std::io::ErrorKind::NotFound {
13338 MongrelError::CorruptWal {
13339 offset: commit_epoch,
13340 reason: format!(
13341 "committed spilled run {} for transaction {txn_id} is missing",
13342 added.run_id
13343 ),
13344 }
13345 } else {
13346 pending_error.into()
13347 }
13348 })?
13349 }
13350 Err(error) => return Err(error.into()),
13351 };
13352 let mut reader = crate::sorted_run::RunReader::open_file(file, schema.clone(), kek)?;
13353 let header = reader.header();
13354 if header.run_id != added.run_id
13355 || header.content_hash != added.content_hash
13356 || header.row_count != added.row_count
13357 || header.level != added.level
13358 || header.min_row_id != added.min_row_id
13359 || header.max_row_id != added.max_row_id
13360 || header.schema_id != schema.schema_id
13361 || !header.is_uniform_epoch()
13362 || header.epoch_created != 0
13363 {
13364 return Err(MongrelError::CorruptWal {
13365 offset: commit_epoch,
13366 reason: format!(
13367 "committed spilled run {} metadata differs from WAL",
13368 added.run_id
13369 ),
13370 });
13371 }
13372 reader.validate_all_pages()?;
13373 Ok(())
13374}
13375
13376fn recover_shared_wal(
13385 durable_root: &crate::durable_file::DurableRoot,
13386 tables: &HashMap<u64, TableHandle>,
13387 catalog: &Catalog,
13388 epoch: &EpochAuthority,
13389 records: &[crate::wal::Record],
13390) -> Result<()> {
13391 use crate::memtable::Row;
13392 use crate::wal::{DdlOp, Op};
13393
13394 let mut committed: HashMap<u64, u64> = HashMap::new();
13396 let mut spilled_to_link: Vec<(
13397 u64, u64, Vec<crate::wal::AddedRun>,
13400 )> = Vec::new();
13401 for r in records {
13402 if let Op::TxnCommit {
13403 epoch: ce,
13404 ref added_runs,
13405 } = r.op
13406 {
13407 committed.insert(r.txn_id, ce);
13408 if !added_runs.is_empty() {
13409 spilled_to_link.push((r.txn_id, ce, added_runs.clone()));
13410 }
13411 }
13412 }
13413 for record in records {
13414 let Some(&commit_epoch) = committed.get(&record.txn_id) else {
13415 continue;
13416 };
13417 match &record.op {
13418 Op::Put { table_id, .. }
13419 | Op::Delete { table_id, .. }
13420 | Op::TruncateTable { table_id } => {
13421 validate_recovered_data_table(
13422 catalog,
13423 tables,
13424 *table_id,
13425 commit_epoch,
13426 record.seq.0,
13427 )?;
13428 }
13429 Op::TxnCommit { added_runs, .. } => {
13430 for run in added_runs {
13431 validate_recovered_data_table(
13432 catalog,
13433 tables,
13434 run.table_id,
13435 commit_epoch,
13436 record.seq.0,
13437 )?;
13438 }
13439 }
13440 _ => {}
13441 }
13442 }
13443 let truncated_transactions: HashSet<(u64, u64)> = records
13444 .iter()
13445 .filter_map(|record| {
13446 committed.get(&record.txn_id)?;
13447 match record.op {
13448 Op::TruncateTable { table_id } => Some((record.txn_id, table_id)),
13449 _ => None,
13450 }
13451 })
13452 .collect();
13453
13454 enum ExternalRecoveryAction {
13456 Write { name: String, state: Vec<u8> },
13457 Reset { name: String },
13458 }
13459 let mut stage: HashMap<u64, RecoveryTableStage> = HashMap::new();
13460 let mut external_actions = Vec::new();
13461 let mut max_epoch = epoch.visible().0;
13462 for r in records.iter().cloned() {
13463 let Some(&ce) = committed.get(&r.txn_id) else {
13464 continue; };
13466 let commit_epoch = Epoch(ce);
13467 max_epoch = max_epoch.max(ce);
13468 match r.op {
13469 Op::Put { table_id, rows } => {
13470 let skip = tables
13472 .get(&table_id)
13473 .map(|h| h.lock().flushed_epoch() >= ce)
13474 .unwrap_or(true);
13475 if skip {
13476 continue;
13477 }
13478 let rows: Vec<Row> = bincode::deserialize(&rows).map_err(|error| {
13479 MongrelError::CorruptWal {
13480 offset: r.seq.0,
13481 reason: format!(
13482 "committed Put payload for transaction {} could not be decoded: {error}",
13483 r.txn_id
13484 ),
13485 }
13486 })?;
13487 let rows: Vec<Row> = rows
13490 .into_iter()
13491 .map(|mut row| {
13492 row.committed_epoch = commit_epoch;
13493 row
13494 })
13495 .collect();
13496 let entry = stage
13497 .entry(table_id)
13498 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
13499 entry.0.extend(rows);
13500 entry.3 = commit_epoch;
13501 }
13502 Op::Delete { table_id, row_ids } => {
13503 let skip = tables
13504 .get(&table_id)
13505 .map(|h| h.lock().flushed_epoch() >= ce)
13506 .unwrap_or(true);
13507 if skip {
13508 continue;
13509 }
13510 let dels = row_ids.into_iter().map(|rid| (rid, commit_epoch));
13511 let entry = stage
13512 .entry(table_id)
13513 .or_insert_with(|| (Vec::new(), Vec::new(), None, commit_epoch));
13514 entry.1.extend(dels);
13515 entry.3 = commit_epoch;
13516 }
13517 Op::TruncateTable { table_id } => {
13518 let skip = tables
13519 .get(&table_id)
13520 .map(|h| h.lock().flushed_epoch() >= ce)
13521 .unwrap_or(true);
13522 if skip {
13523 continue;
13524 }
13525 stage.insert(
13526 table_id,
13527 (Vec::new(), Vec::new(), Some(commit_epoch), commit_epoch),
13528 );
13529 }
13530 Op::ExternalTableState { name, state } => {
13531 let current_generation = catalog
13532 .external_tables
13533 .iter()
13534 .find(|entry| entry.name == name)
13535 .map(|entry| entry.created_epoch);
13536 if current_generation.is_some_and(|created_epoch| ce >= created_epoch) {
13537 validate_recovered_external_name(&name)?;
13538 external_actions.push(ExternalRecoveryAction::Write { name, state });
13539 }
13540 }
13541 Op::Ddl(DdlOp::ResetExternalTableState {
13542 name,
13543 generation_epoch,
13544 }) => {
13545 if generation_epoch != ce {
13546 return Err(MongrelError::CorruptWal {
13547 offset: r.seq.0,
13548 reason: format!(
13549 "external state reset epoch {generation_epoch} does not match WAL commit epoch {ce}"
13550 ),
13551 });
13552 }
13553 validate_recovered_external_name(&name)?;
13554 external_actions.push(ExternalRecoveryAction::Reset { name });
13555 }
13556 Op::Flush { .. }
13557 | Op::TxnCommit { .. }
13558 | Op::TxnAbort
13559 | Op::Ddl(_)
13560 | Op::BeforeImage { .. }
13561 | Op::CommitTimestamp { .. }
13562 | Op::SpilledRows { .. } => {}
13563 }
13564 }
13565 for (_, commit_epoch, added_runs) in &mut spilled_to_link {
13566 added_runs.retain(|added| {
13567 tables
13568 .get(&added.table_id)
13569 .is_some_and(|table| table.lock().flushed_epoch() < *commit_epoch)
13570 });
13571 }
13572 spilled_to_link.retain(|(_, _, added_runs)| !added_runs.is_empty());
13573 validate_recovery_table_stages(tables, &stage)?;
13574 validate_recovery_spilled_runs(durable_root, tables, &spilled_to_link)?;
13575
13576 for action in external_actions {
13580 match action {
13581 ExternalRecoveryAction::Write { name, state } => {
13582 write_external_state_file(durable_root, &name, &state)?;
13583 }
13584 ExternalRecoveryAction::Reset { name } => {
13585 durable_root.create_directory_all(VTAB_DIR)?;
13586 durable_root.remove_directory_all(Path::new(VTAB_DIR).join(name))?;
13587 }
13588 }
13589 }
13590 for (table_id, (rows, deletes, truncate_epoch, table_epoch)) in stage {
13591 let Some(handle) = tables.get(&table_id) else {
13592 continue;
13593 };
13594 let mut t = handle.lock();
13595 if let Some(epoch) = truncate_epoch {
13596 t.apply_truncate(epoch);
13597 }
13598 t.recover_apply(rows, deletes)?;
13599 let rows = t.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
13603 t.live_count = rows.len() as u64;
13604 t.persist_manifest(table_epoch.max(epoch.visible()))?;
13608 }
13609
13610 for (txn_id, ce, added_runs) in &spilled_to_link {
13614 for ar in added_runs {
13615 let Some(handle) = tables.get(&ar.table_id) else {
13616 continue;
13617 };
13618 let mut t = handle.lock();
13619 let table_dir = Path::new(TABLES_DIR).join(ar.table_id.to_string());
13620 let destination = table_dir
13621 .join(crate::engine::RUNS_DIR)
13622 .join(format!("r-{}.sr", ar.run_id));
13623 match durable_root.open_regular(&destination) {
13624 Ok(_) => {}
13625 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
13626 let pending = table_dir
13627 .join("_txn")
13628 .join(txn_id.to_string())
13629 .join(format!("r-{}.sr", ar.run_id));
13630 durable_root.rename_file_new(&pending, &destination)?;
13631 }
13632 Err(error) => return Err(error.into()),
13633 }
13634 let linked = t.recover_spilled_run(crate::manifest::RunRef {
13640 run_id: ar.run_id,
13641 level: ar.level,
13642 epoch_created: *ce,
13643 row_count: ar.row_count,
13644 });
13645 let replaced = truncated_transactions.contains(&(*txn_id, ar.table_id));
13646 if replaced {
13647 t.set_flushed_epoch(Epoch(*ce));
13648 }
13649 if linked || replaced {
13650 t.persist_manifest(Epoch(*ce).max(epoch.visible()))?;
13651 }
13652 }
13653 }
13654
13655 epoch.advance_recovered(Epoch(max_epoch));
13656 Ok(())
13657}
13658
13659fn reconcile_recovered_table_metadata(
13660 tables: &HashMap<u64, TableHandle>,
13661 epoch: Epoch,
13662) -> Result<()> {
13663 let mut table_ids = tables.keys().copied().collect::<Vec<_>>();
13664 table_ids.sort_unstable();
13665 let mut plans = Vec::with_capacity(table_ids.len());
13666 for table_id in &table_ids {
13667 let handle = tables.get(table_id).ok_or_else(|| {
13668 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
13669 })?;
13670 plans.push((*table_id, handle.lock().plan_recovered_metadata()?));
13671 }
13672 for (table_id, plan) in plans {
13675 let handle = tables.get(&table_id).ok_or_else(|| {
13676 MongrelError::Other(format!("mounted table {table_id} vanished during recovery"))
13677 })?;
13678 handle.lock().apply_recovered_metadata(plan, epoch)?;
13679 }
13680 Ok(())
13681}
13682
13683fn validate_recovered_external_name(name: &str) -> Result<()> {
13684 if name.is_empty()
13685 || !name.chars().all(|character| {
13686 character.is_ascii_alphanumeric() || character == '_' || character == '-'
13687 })
13688 {
13689 return Err(MongrelError::CorruptWal {
13690 offset: 0,
13691 reason: format!("unsafe recovered external-table name {name:?}"),
13692 });
13693 }
13694 Ok(())
13695}
13696
13697fn validate_recovery_table_stages(
13698 tables: &HashMap<u64, TableHandle>,
13699 stages: &HashMap<u64, RecoveryTableStage>,
13700) -> Result<()> {
13701 for (table_id, (rows, _, _, _)) in stages {
13702 let handle = tables
13703 .get(table_id)
13704 .ok_or_else(|| MongrelError::CorruptWal {
13705 offset: *table_id,
13706 reason: format!("recovery stage references unmounted table {table_id}"),
13707 })?;
13708 let table = handle.lock();
13709 table.visible_rows(Snapshot::at(Epoch(u64::MAX)))?;
13712 for row in rows {
13713 validate_recovered_row(table.schema(), row)?;
13714 }
13715 }
13716 Ok(())
13717}
13718
13719fn validate_recovered_row(schema: &Schema, row: &crate::memtable::Row) -> Result<()> {
13720 if row.deleted || row.row_id.0 == u64::MAX {
13721 return Err(MongrelError::CorruptWal {
13722 offset: row.row_id.0,
13723 reason: "committed Put payload contains a tombstone or exhausted row id".into(),
13724 });
13725 }
13726 let cells = row
13727 .columns
13728 .iter()
13729 .map(|(column, value)| (*column, value.clone()))
13730 .collect::<Vec<_>>();
13731 schema
13732 .validate_persisted_values(&cells)
13733 .map_err(|error| MongrelError::CorruptWal {
13734 offset: row.row_id.0,
13735 reason: format!("recovered row violates table schema: {error}"),
13736 })?;
13737 if schema.auto_increment_column().is_some_and(|column| {
13738 matches!(row.columns.get(&column.id), Some(Value::Int64(value)) if *value == i64::MAX)
13739 }) {
13740 return Err(MongrelError::CorruptWal {
13741 offset: row.row_id.0,
13742 reason: "recovered AUTO_INCREMENT value exhausts i64".into(),
13743 });
13744 }
13745 Ok(())
13746}
13747
13748fn validate_recovery_spilled_runs(
13749 root: &crate::durable_file::DurableRoot,
13750 tables: &HashMap<u64, TableHandle>,
13751 spilled: &[(u64, u64, Vec<crate::wal::AddedRun>)],
13752) -> Result<()> {
13753 let mut identities = HashSet::new();
13754 for (txn_id, commit_epoch, added_runs) in spilled {
13755 for added in added_runs {
13756 if added.run_id >= u64::MAX as u128 {
13757 return Err(MongrelError::CorruptWal {
13758 offset: *commit_epoch,
13759 reason: format!(
13760 "recovered run id {} exceeds the on-disk namespace",
13761 added.run_id
13762 ),
13763 });
13764 }
13765 let Some(handle) = tables.get(&added.table_id) else {
13766 continue;
13767 };
13768 if !identities.insert((added.table_id, added.run_id)) {
13769 return Err(MongrelError::CorruptWal {
13770 offset: *commit_epoch,
13771 reason: format!(
13772 "duplicate recovered run {} for table {}",
13773 added.run_id, added.table_id
13774 ),
13775 });
13776 }
13777 let table = handle.lock();
13778 validate_planned_spilled_run(
13779 root,
13780 *txn_id,
13781 *commit_epoch,
13782 added,
13783 table.schema(),
13784 table.kek(),
13785 )?;
13786 }
13787 }
13788 Ok(())
13789}
13790
13791fn validate_condition_columns(condition: &ProcedureCondition, schema: &Schema) -> Result<()> {
13792 match condition {
13793 ProcedureCondition::Pk { .. } => {
13794 if schema.primary_key().is_none() {
13795 return Err(MongrelError::InvalidArgument(
13796 "procedure condition Pk references a table without a primary key".into(),
13797 ));
13798 }
13799 }
13800 ProcedureCondition::BitmapEq { column_id, .. }
13801 | ProcedureCondition::BitmapIn { column_id, .. }
13802 | ProcedureCondition::Range { column_id, .. }
13803 | ProcedureCondition::RangeF64 { column_id, .. }
13804 | ProcedureCondition::IsNull { column_id }
13805 | ProcedureCondition::IsNotNull { column_id }
13806 | ProcedureCondition::FmContains { column_id, .. } => {
13807 validate_column_id(*column_id, schema)?;
13808 }
13809 }
13810 Ok(())
13811}
13812
13813fn bind_procedure_args(
13814 procedure: &StoredProcedure,
13815 mut args: HashMap<String, crate::Value>,
13816) -> Result<HashMap<String, crate::Value>> {
13817 let mut out = HashMap::new();
13818 for param in &procedure.params {
13819 let value = match args.remove(¶m.name) {
13820 Some(value) => value,
13821 None => param.default.clone().ok_or_else(|| {
13822 MongrelError::InvalidArgument(format!(
13823 "missing required procedure parameter {:?}",
13824 param.name
13825 ))
13826 })?,
13827 };
13828 if !param.nullable && matches!(value, crate::Value::Null) {
13829 return Err(MongrelError::InvalidArgument(format!(
13830 "procedure parameter {:?} must not be NULL",
13831 param.name
13832 )));
13833 }
13834 if !matches!(value, crate::Value::Null) && !value_matches_type(&value, param.ty.clone()) {
13835 return Err(MongrelError::InvalidArgument(format!(
13836 "procedure parameter {:?} has wrong type",
13837 param.name
13838 )));
13839 }
13840 out.insert(param.name.clone(), value);
13841 }
13842 if let Some(extra) = args.keys().next() {
13843 return Err(MongrelError::InvalidArgument(format!(
13844 "unknown procedure parameter {extra:?}"
13845 )));
13846 }
13847 Ok(out)
13848}
13849
13850fn value_matches_type(value: &crate::Value, ty: crate::TypeId) -> bool {
13851 matches!(
13852 (value, ty),
13853 (crate::Value::Bool(_), crate::TypeId::Bool)
13854 | (crate::Value::Int64(_), crate::TypeId::Int8)
13855 | (crate::Value::Int64(_), crate::TypeId::Int16)
13856 | (crate::Value::Int64(_), crate::TypeId::Int32)
13857 | (crate::Value::Int64(_), crate::TypeId::Int64)
13858 | (crate::Value::Int64(_), crate::TypeId::UInt8)
13859 | (crate::Value::Int64(_), crate::TypeId::UInt16)
13860 | (crate::Value::Int64(_), crate::TypeId::UInt32)
13861 | (crate::Value::Int64(_), crate::TypeId::UInt64)
13862 | (crate::Value::Int64(_), crate::TypeId::TimestampNanos)
13863 | (crate::Value::Int64(_), crate::TypeId::Date32)
13864 | (crate::Value::Float64(_), crate::TypeId::Float32)
13865 | (crate::Value::Float64(_), crate::TypeId::Float64)
13866 | (crate::Value::Bytes(_), crate::TypeId::Bytes)
13867 | (crate::Value::Embedding(_), crate::TypeId::Embedding { .. })
13868 )
13869}
13870
13871fn eval_cells(
13872 cells: &[crate::procedure::ProcedureCell],
13873 args: &HashMap<String, crate::Value>,
13874 outputs: &HashMap<String, ProcedureCallOutput>,
13875) -> Result<Vec<(u16, crate::Value)>> {
13876 cells
13877 .iter()
13878 .map(|cell| Ok((cell.column_id, eval_value(&cell.value, args, outputs)?)))
13879 .collect()
13880}
13881
13882fn eval_condition(
13883 condition: &ProcedureCondition,
13884 args: &HashMap<String, crate::Value>,
13885 outputs: &HashMap<String, ProcedureCallOutput>,
13886) -> Result<crate::Condition> {
13887 Ok(match condition {
13888 ProcedureCondition::Pk { value } => {
13889 crate::Condition::Pk(eval_value(value, args, outputs)?.encode_key())
13890 }
13891 ProcedureCondition::BitmapEq { column_id, value } => crate::Condition::BitmapEq {
13892 column_id: *column_id,
13893 value: eval_value(value, args, outputs)?.encode_key(),
13894 },
13895 ProcedureCondition::BitmapIn { column_id, values } => crate::Condition::BitmapIn {
13896 column_id: *column_id,
13897 values: values
13898 .iter()
13899 .map(|value| Ok(eval_value(value, args, outputs)?.encode_key()))
13900 .collect::<Result<Vec<_>>>()?,
13901 },
13902 ProcedureCondition::Range { column_id, lo, hi } => crate::Condition::Range {
13903 column_id: *column_id,
13904 lo: expect_i64(eval_value(lo, args, outputs)?)?,
13905 hi: expect_i64(eval_value(hi, args, outputs)?)?,
13906 },
13907 ProcedureCondition::RangeF64 {
13908 column_id,
13909 lo,
13910 lo_inclusive,
13911 hi,
13912 hi_inclusive,
13913 } => crate::Condition::RangeF64 {
13914 column_id: *column_id,
13915 lo: expect_f64(eval_value(lo, args, outputs)?)?,
13916 lo_inclusive: *lo_inclusive,
13917 hi: expect_f64(eval_value(hi, args, outputs)?)?,
13918 hi_inclusive: *hi_inclusive,
13919 },
13920 ProcedureCondition::IsNull { column_id } => crate::Condition::IsNull {
13921 column_id: *column_id,
13922 },
13923 ProcedureCondition::IsNotNull { column_id } => crate::Condition::IsNotNull {
13924 column_id: *column_id,
13925 },
13926 ProcedureCondition::FmContains { column_id, pattern } => crate::Condition::FmContains {
13927 column_id: *column_id,
13928 pattern: expect_bytes(eval_value(pattern, args, outputs)?)?,
13929 },
13930 })
13931}
13932
13933fn eval_value(
13934 value: &ProcedureValue,
13935 args: &HashMap<String, crate::Value>,
13936 outputs: &HashMap<String, ProcedureCallOutput>,
13937) -> Result<crate::Value> {
13938 match value {
13939 ProcedureValue::Literal(value) => Ok(value.clone()),
13940 ProcedureValue::Param(name) => args.get(name).cloned().ok_or_else(|| {
13941 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
13942 }),
13943 ProcedureValue::StepScalar(id) => match outputs.get(id) {
13944 Some(ProcedureCallOutput::Scalar(value)) => Ok(value.clone()),
13945 _ => Err(MongrelError::InvalidArgument(format!(
13946 "procedure step {id:?} did not return a scalar"
13947 ))),
13948 },
13949 ProcedureValue::StepRows(_) | ProcedureValue::StepRow(_) => {
13950 Err(MongrelError::InvalidArgument(
13951 "row-valued procedure reference cannot be used as a scalar".into(),
13952 ))
13953 }
13954 ProcedureValue::Object(_) | ProcedureValue::Array(_) => Err(MongrelError::InvalidArgument(
13955 "structured procedure value cannot be used as a scalar cell".into(),
13956 )),
13957 }
13958}
13959
13960fn eval_return_output(
13961 value: &ProcedureValue,
13962 args: &HashMap<String, crate::Value>,
13963 outputs: &HashMap<String, ProcedureCallOutput>,
13964) -> Result<ProcedureCallOutput> {
13965 match value {
13966 ProcedureValue::Literal(value) => Ok(ProcedureCallOutput::Scalar(value.clone())),
13967 ProcedureValue::Param(name) => Ok(ProcedureCallOutput::Scalar(
13968 args.get(name).cloned().ok_or_else(|| {
13969 MongrelError::InvalidArgument(format!("unknown procedure parameter {name:?}"))
13970 })?,
13971 )),
13972 ProcedureValue::StepRows(id)
13973 | ProcedureValue::StepRow(id)
13974 | ProcedureValue::StepScalar(id) => outputs.get(id).cloned().ok_or_else(|| {
13975 MongrelError::InvalidArgument(format!("unknown procedure step output {id:?}"))
13976 }),
13977 ProcedureValue::Object(fields) => {
13978 let mut out = Vec::with_capacity(fields.len());
13979 for (name, value) in fields {
13980 out.push((name.clone(), eval_return_output(value, args, outputs)?));
13981 }
13982 Ok(ProcedureCallOutput::Object(out))
13983 }
13984 ProcedureValue::Array(values) => {
13985 let mut out = Vec::with_capacity(values.len());
13986 for value in values {
13987 out.push(eval_return_output(value, args, outputs)?);
13988 }
13989 Ok(ProcedureCallOutput::Array(out))
13990 }
13991 }
13992}
13993
13994fn expect_i64(value: crate::Value) -> Result<i64> {
13995 match value {
13996 crate::Value::Int64(value) => Ok(value),
13997 _ => Err(MongrelError::InvalidArgument(
13998 "procedure value must be Int64".into(),
13999 )),
14000 }
14001}
14002
14003fn expect_f64(value: crate::Value) -> Result<f64> {
14004 match value {
14005 crate::Value::Float64(value) => Ok(value),
14006 _ => Err(MongrelError::InvalidArgument(
14007 "procedure value must be Float64".into(),
14008 )),
14009 }
14010}
14011
14012fn expect_bytes(value: crate::Value) -> Result<Vec<u8>> {
14013 match value {
14014 crate::Value::Bytes(value) => Ok(value),
14015 _ => Err(MongrelError::InvalidArgument(
14016 "procedure value must be Bytes".into(),
14017 )),
14018 }
14019}
14020
14021fn validate_column_id(column_id: u16, schema: &Schema) -> Result<()> {
14022 if schema.columns.iter().any(|c| c.id == column_id) {
14023 Ok(())
14024 } else {
14025 Err(MongrelError::InvalidArgument(format!(
14026 "unknown column id {column_id}"
14027 )))
14028 }
14029}
14030
14031fn trigger_matches_event(
14032 trigger: &StoredTrigger,
14033 event: &WriteEvent,
14034 cat: &Catalog,
14035) -> Result<bool> {
14036 if trigger.event != event.kind {
14037 return Ok(false);
14038 }
14039 let TriggerTarget::Table(target) = &trigger.target else {
14040 return Ok(false);
14041 };
14042 if target != &event.table {
14043 return Ok(false);
14044 }
14045 if trigger.event == TriggerEvent::Update && !trigger.update_of.is_empty() {
14046 let schema = &cat
14047 .live(target)
14048 .ok_or_else(|| {
14049 MongrelError::InvalidArgument(format!(
14050 "trigger {:?} references unknown table {target:?}",
14051 trigger.name
14052 ))
14053 })?
14054 .schema;
14055 let mut watched = Vec::with_capacity(trigger.update_of.len());
14056 for name in &trigger.update_of {
14057 let col = schema.column(name).ok_or_else(|| {
14058 MongrelError::InvalidArgument(format!(
14059 "trigger {:?} references unknown UPDATE OF column {name:?}",
14060 trigger.name
14061 ))
14062 })?;
14063 watched.push(col.id);
14064 }
14065 if !event
14066 .changed_columns
14067 .iter()
14068 .any(|column_id| watched.contains(column_id))
14069 {
14070 return Ok(false);
14071 }
14072 }
14073 Ok(true)
14074}
14075
14076fn changed_columns(old: Option<&TriggerRowImage>, new: Option<&TriggerRowImage>) -> Vec<u16> {
14077 let mut ids = std::collections::BTreeSet::new();
14078 if let Some(old) = old {
14079 ids.extend(old.columns.keys().copied());
14080 }
14081 if let Some(new) = new {
14082 ids.extend(new.columns.keys().copied());
14083 }
14084 ids.into_iter()
14085 .filter(|id| {
14086 old.and_then(|row| row.columns.get(id)) != new.and_then(|row| row.columns.get(id))
14087 })
14088 .collect()
14089}
14090
14091fn eval_trigger_cells(
14092 cells: &[crate::trigger::TriggerCell],
14093 event: &WriteEvent,
14094 selected: Option<&TriggerRowImage>,
14095) -> Result<Vec<(u16, Value)>> {
14096 cells
14097 .iter()
14098 .map(|cell| {
14099 Ok((
14100 cell.column_id,
14101 eval_trigger_value(&cell.value, event, selected)?,
14102 ))
14103 })
14104 .collect()
14105}
14106
14107fn eval_trigger_expr(expr: &TriggerExpr, event: &WriteEvent) -> Result<bool> {
14108 match expr {
14109 TriggerExpr::Value(value) => match eval_trigger_value(value, event, None)? {
14110 Value::Bool(value) => Ok(value),
14111 Value::Null => Ok(false),
14112 other => Err(MongrelError::InvalidArgument(format!(
14113 "trigger WHEN value must be boolean, got {other:?}"
14114 ))),
14115 },
14116 TriggerExpr::Eq { left, right } => Ok(values_equal(
14117 &eval_trigger_value(left, event, None)?,
14118 &eval_trigger_value(right, event, None)?,
14119 )),
14120 TriggerExpr::NotEq { left, right } => Ok(!values_equal(
14121 &eval_trigger_value(left, event, None)?,
14122 &eval_trigger_value(right, event, None)?,
14123 )),
14124 TriggerExpr::Lt { left, right } => match value_order(
14125 &eval_trigger_value(left, event, None)?,
14126 &eval_trigger_value(right, event, None)?,
14127 ) {
14128 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
14129 None => Ok(false),
14130 },
14131 TriggerExpr::Lte { left, right } => match value_order(
14132 &eval_trigger_value(left, event, None)?,
14133 &eval_trigger_value(right, event, None)?,
14134 ) {
14135 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
14136 None => Ok(false),
14137 },
14138 TriggerExpr::Gt { left, right } => match value_order(
14139 &eval_trigger_value(left, event, None)?,
14140 &eval_trigger_value(right, event, None)?,
14141 ) {
14142 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
14143 None => Ok(false),
14144 },
14145 TriggerExpr::Gte { left, right } => match value_order(
14146 &eval_trigger_value(left, event, None)?,
14147 &eval_trigger_value(right, event, None)?,
14148 ) {
14149 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
14150 None => Ok(false),
14151 },
14152 TriggerExpr::IsNull(value) => Ok(matches!(
14153 eval_trigger_value(value, event, None)?,
14154 Value::Null
14155 )),
14156 TriggerExpr::IsNotNull(value) => Ok(!matches!(
14157 eval_trigger_value(value, event, None)?,
14158 Value::Null
14159 )),
14160 TriggerExpr::And { left, right } => {
14161 if !eval_trigger_expr(left, event)? {
14162 Ok(false)
14163 } else {
14164 Ok(eval_trigger_expr(right, event)?)
14165 }
14166 }
14167 TriggerExpr::Or { left, right } => {
14168 if eval_trigger_expr(left, event)? {
14169 Ok(true)
14170 } else {
14171 Ok(eval_trigger_expr(right, event)?)
14172 }
14173 }
14174 TriggerExpr::Not(expr) => Ok(!eval_trigger_expr(expr, event)?),
14175 }
14176}
14177
14178fn eval_trigger_condition(
14179 condition: &TriggerCondition,
14180 event: &WriteEvent,
14181 selected: &TriggerRowImage,
14182 schema: &Schema,
14183) -> Result<bool> {
14184 match condition {
14185 TriggerCondition::Pk { value } => {
14186 let pk = schema.primary_key().ok_or_else(|| {
14187 MongrelError::InvalidArgument(
14188 "trigger condition Pk references a table without a primary key".into(),
14189 )
14190 })?;
14191 let lhs = eval_trigger_value(value, event, Some(selected))?;
14192 Ok(values_equal(
14193 &lhs,
14194 selected.columns.get(&pk.id).unwrap_or(&Value::Null),
14195 ))
14196 }
14197 TriggerCondition::Eq { column_id, value } => Ok(values_equal(
14198 selected.columns.get(column_id).unwrap_or(&Value::Null),
14199 &eval_trigger_value(value, event, Some(selected))?,
14200 )),
14201 TriggerCondition::NotEq { column_id, value } => Ok(!values_equal(
14202 selected.columns.get(column_id).unwrap_or(&Value::Null),
14203 &eval_trigger_value(value, event, Some(selected))?,
14204 )),
14205 TriggerCondition::Lt { column_id, value } => match value_order(
14206 selected.columns.get(column_id).unwrap_or(&Value::Null),
14207 &eval_trigger_value(value, event, Some(selected))?,
14208 ) {
14209 Some(ordering) => Ok(ordering == std::cmp::Ordering::Less),
14210 None => Ok(false),
14211 },
14212 TriggerCondition::Lte { column_id, value } => match value_order(
14213 selected.columns.get(column_id).unwrap_or(&Value::Null),
14214 &eval_trigger_value(value, event, Some(selected))?,
14215 ) {
14216 Some(ordering) => Ok(ordering != std::cmp::Ordering::Greater),
14217 None => Ok(false),
14218 },
14219 TriggerCondition::Gt { column_id, value } => match value_order(
14220 selected.columns.get(column_id).unwrap_or(&Value::Null),
14221 &eval_trigger_value(value, event, Some(selected))?,
14222 ) {
14223 Some(ordering) => Ok(ordering == std::cmp::Ordering::Greater),
14224 None => Ok(false),
14225 },
14226 TriggerCondition::Gte { column_id, value } => match value_order(
14227 selected.columns.get(column_id).unwrap_or(&Value::Null),
14228 &eval_trigger_value(value, event, Some(selected))?,
14229 ) {
14230 Some(ordering) => Ok(ordering != std::cmp::Ordering::Less),
14231 None => Ok(false),
14232 },
14233 TriggerCondition::IsNull { column_id } => Ok(matches!(
14234 selected.columns.get(column_id),
14235 None | Some(Value::Null)
14236 )),
14237 TriggerCondition::IsNotNull { column_id } => Ok(!matches!(
14238 selected.columns.get(column_id),
14239 None | Some(Value::Null)
14240 )),
14241 TriggerCondition::And { left, right } => {
14242 if !eval_trigger_condition(left, event, selected, schema)? {
14243 Ok(false)
14244 } else {
14245 Ok(eval_trigger_condition(right, event, selected, schema)?)
14246 }
14247 }
14248 TriggerCondition::Or { left, right } => {
14249 if eval_trigger_condition(left, event, selected, schema)? {
14250 Ok(true)
14251 } else {
14252 Ok(eval_trigger_condition(right, event, selected, schema)?)
14253 }
14254 }
14255 TriggerCondition::Not(condition) => {
14256 Ok(!eval_trigger_condition(condition, event, selected, schema)?)
14257 }
14258 }
14259}
14260
14261fn eval_trigger_value(
14262 value: &TriggerValue,
14263 event: &WriteEvent,
14264 selected: Option<&TriggerRowImage>,
14265) -> Result<Value> {
14266 match value {
14267 TriggerValue::Literal(value) => Ok(value.clone()),
14268 TriggerValue::NewColumn(column_id) => event
14269 .new
14270 .as_ref()
14271 .and_then(|row| row.columns.get(column_id))
14272 .cloned()
14273 .ok_or_else(|| MongrelError::InvalidArgument("NEW column is not available".into())),
14274 TriggerValue::OldColumn(column_id) => event
14275 .old
14276 .as_ref()
14277 .and_then(|row| row.columns.get(column_id))
14278 .cloned()
14279 .ok_or_else(|| MongrelError::InvalidArgument("OLD column is not available".into())),
14280 TriggerValue::SelectedColumn(column_id) => selected
14281 .and_then(|row| row.columns.get(column_id))
14282 .cloned()
14283 .ok_or_else(|| {
14284 MongrelError::InvalidArgument("SELECTED column is not available".into())
14285 }),
14286 }
14287}
14288
14289fn values_equal(left: &Value, right: &Value) -> bool {
14290 match (left, right) {
14291 (Value::Null, Value::Null) => true,
14292 (Value::Bool(a), Value::Bool(b)) => a == b,
14293 (Value::Int64(a), Value::Int64(b)) => a == b,
14294 (Value::Float64(a), Value::Float64(b)) => a.to_bits() == b.to_bits(),
14295 (Value::Bytes(a), Value::Bytes(b)) => a == b,
14296 (Value::Embedding(a), Value::Embedding(b)) => {
14297 a.len() == b.len()
14298 && a.iter()
14299 .zip(b.iter())
14300 .all(|(a, b)| a.to_bits() == b.to_bits())
14301 }
14302 _ => false,
14303 }
14304}
14305
14306fn value_order(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
14307 match (left, right) {
14308 (Value::Null, _) | (_, Value::Null) => None,
14309 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
14310 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
14311 (Value::Int64(a), Value::Float64(b)) => {
14314 let af = *a as f64;
14315 Some(af.total_cmp(b))
14316 }
14317 (Value::Float64(a), Value::Int64(b)) => {
14320 let bf = *b as f64;
14321 Some(a.total_cmp(&bf))
14322 }
14323 (Value::Float64(a), Value::Float64(b)) => Some(a.total_cmp(b)),
14324 (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
14325 (Value::Embedding(_), Value::Embedding(_)) => None,
14326 _ => None,
14327 }
14328}
14329
14330fn trigger_message(value: Value) -> String {
14331 match value {
14332 Value::Null => "NULL".into(),
14333 Value::Bool(value) => value.to_string(),
14334 Value::Int64(value) => value.to_string(),
14335 Value::Float64(value) => value.to_string(),
14336 Value::Bytes(value) => String::from_utf8_lossy(&value).into_owned(),
14337 Value::Embedding(value) => format!("{value:?}"),
14338 Value::Decimal(value) => value.to_string(),
14339 Value::Interval {
14340 months,
14341 days,
14342 nanos,
14343 } => format!("{months}m {days}d {nanos}ns"),
14344 Value::Uuid(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
14345 Value::Json(b) => String::from_utf8_lossy(&b).into_owned(),
14346 }
14347}
14348
14349fn validate_trigger_step<'a>(
14350 step: &TriggerStep,
14351 cat: &'a Catalog,
14352 target_schema: &Schema,
14353 event: TriggerEvent,
14354 select_schemas: &mut HashMap<String, &'a Schema>,
14355) -> Result<()> {
14356 match step {
14357 TriggerStep::SetNew { cells } => {
14358 if event == TriggerEvent::Delete {
14359 return Err(MongrelError::InvalidArgument(
14360 "SetNew trigger step is not valid for DELETE triggers".into(),
14361 ));
14362 }
14363 for cell in cells {
14364 validate_column_id(cell.column_id, target_schema)?;
14365 validate_trigger_value(&cell.value, target_schema, event)?;
14366 }
14367 }
14368 TriggerStep::Insert { table, cells } => {
14369 let schema = trigger_write_schema(cat, table, "insert")?;
14370 for cell in cells {
14371 validate_column_id(cell.column_id, schema)?;
14372 validate_trigger_value(&cell.value, target_schema, event)?;
14373 }
14374 }
14375 TriggerStep::UpdateByPk { table, pk, cells } => {
14376 let schema = trigger_write_schema(cat, table, "update")?;
14377 if schema.primary_key().is_none() {
14378 return Err(MongrelError::InvalidArgument(format!(
14379 "trigger update_by_pk references table {table:?} without a primary key"
14380 )));
14381 }
14382 validate_trigger_value(pk, target_schema, event)?;
14383 for cell in cells {
14384 validate_column_id(cell.column_id, schema)?;
14385 validate_trigger_value(&cell.value, target_schema, event)?;
14386 }
14387 }
14388 TriggerStep::DeleteByPk { table, pk } => {
14389 let schema = trigger_write_schema(cat, table, "delete")?;
14390 if schema.primary_key().is_none() {
14391 return Err(MongrelError::InvalidArgument(format!(
14392 "trigger delete_by_pk references table {table:?} without a primary key"
14393 )));
14394 }
14395 validate_trigger_value(pk, target_schema, event)?;
14396 }
14397 TriggerStep::Select {
14398 id,
14399 table,
14400 conditions,
14401 } => {
14402 let schema = trigger_read_schema(cat, table)?;
14403 for condition in conditions {
14404 validate_trigger_condition(condition, schema, target_schema, event)?;
14405 }
14406 if select_schemas.contains_key(id) {
14407 return Err(MongrelError::InvalidArgument(format!(
14408 "duplicate select id {id:?} in trigger program"
14409 )));
14410 }
14411 select_schemas.insert(id.clone(), schema);
14412 }
14413 TriggerStep::Foreach { id, steps } => {
14414 if !select_schemas.contains_key(id) {
14415 return Err(MongrelError::InvalidArgument(format!(
14416 "foreach references unknown select id {id:?}"
14417 )));
14418 }
14419 let mut inner_select_schemas = select_schemas.clone();
14420 for step in steps {
14421 validate_trigger_step(step, cat, target_schema, event, &mut inner_select_schemas)?;
14422 }
14423 }
14424 TriggerStep::DeleteWhere { table, conditions } => {
14425 let schema = trigger_write_schema(cat, table, "delete")?;
14426 for condition in conditions {
14427 validate_trigger_condition(condition, schema, target_schema, event)?;
14428 }
14429 }
14430 TriggerStep::UpdateWhere {
14431 table,
14432 conditions,
14433 cells,
14434 } => {
14435 let schema = trigger_write_schema(cat, table, "update")?;
14436 for condition in conditions {
14437 validate_trigger_condition(condition, schema, target_schema, event)?;
14438 }
14439 for cell in cells {
14440 validate_column_id(cell.column_id, schema)?;
14441 validate_trigger_value(&cell.value, target_schema, event)?;
14442 }
14443 }
14444 TriggerStep::Raise { message, .. } => {
14445 validate_trigger_value(message, target_schema, event)?
14446 }
14447 }
14448 Ok(())
14449}
14450
14451fn trigger_validation_error(error: MongrelError) -> MongrelError {
14452 match error {
14453 MongrelError::TriggerValidation(_) => error,
14454 MongrelError::InvalidArgument(message)
14455 | MongrelError::Conflict(message)
14456 | MongrelError::NotFound(message) => MongrelError::TriggerValidation(message),
14457 error => error,
14458 }
14459}
14460
14461fn trigger_write_schema<'a>(cat: &'a Catalog, table: &str, op: &str) -> Result<&'a Schema> {
14462 if let Some(entry) = cat.live(table) {
14463 return Ok(&entry.schema);
14464 }
14465 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
14466 let allowed = match op {
14467 "insert" => entry.capabilities.writable || entry.capabilities.insert_only,
14468 "update" | "delete" => entry.capabilities.writable,
14469 _ => false,
14470 };
14471 if !allowed {
14472 return Err(MongrelError::InvalidArgument(format!(
14473 "trigger {op} references external table {table:?}, but module {:?} is not writable for that operation",
14474 entry.module
14475 )));
14476 }
14477 if !entry.capabilities.transaction_safe {
14478 return Err(MongrelError::InvalidArgument(format!(
14479 "trigger {op} references external table {table:?}, but module {:?} is not transaction-safe",
14480 entry.module
14481 )));
14482 }
14483 return Ok(&entry.declared_schema);
14484 }
14485 Err(MongrelError::InvalidArgument(format!(
14486 "trigger references unknown table {table:?}"
14487 )))
14488}
14489
14490fn trigger_read_schema<'a>(cat: &'a Catalog, table: &str) -> Result<&'a Schema> {
14491 if let Some(entry) = cat.live(table) {
14492 return Ok(&entry.schema);
14493 }
14494 if let Some(entry) = cat.external_tables.iter().find(|entry| entry.name == table) {
14495 if entry.capabilities.trigger_safe {
14496 return Ok(&entry.declared_schema);
14497 }
14498 return Err(MongrelError::InvalidArgument(format!(
14499 "trigger reads external table {table:?}, but module {:?} is not trigger-safe",
14500 entry.module
14501 )));
14502 }
14503 Err(MongrelError::InvalidArgument(format!(
14504 "trigger references unknown table {table:?}"
14505 )))
14506}
14507
14508fn validate_trigger_condition(
14509 condition: &TriggerCondition,
14510 schema: &Schema,
14511 target_schema: &Schema,
14512 event: TriggerEvent,
14513) -> Result<()> {
14514 match condition {
14515 TriggerCondition::Pk { value } => {
14516 if schema.primary_key().is_none() {
14517 return Err(MongrelError::InvalidArgument(
14518 "trigger condition Pk references a table without a primary key".into(),
14519 ));
14520 }
14521 validate_trigger_value(value, target_schema, event)
14522 }
14523 TriggerCondition::Eq { column_id, value }
14524 | TriggerCondition::NotEq { column_id, value }
14525 | TriggerCondition::Lt { column_id, value }
14526 | TriggerCondition::Lte { column_id, value }
14527 | TriggerCondition::Gt { column_id, value }
14528 | TriggerCondition::Gte { column_id, value } => {
14529 validate_column_id(*column_id, schema)?;
14530 validate_trigger_value(value, target_schema, event)
14531 }
14532 TriggerCondition::IsNull { column_id } | TriggerCondition::IsNotNull { column_id } => {
14533 validate_column_id(*column_id, schema)
14534 }
14535 TriggerCondition::And { left, right } | TriggerCondition::Or { left, right } => {
14536 validate_trigger_condition(left, schema, target_schema, event)?;
14537 validate_trigger_condition(right, schema, target_schema, event)
14538 }
14539 TriggerCondition::Not(condition) => {
14540 validate_trigger_condition(condition, schema, target_schema, event)
14541 }
14542 }
14543}
14544
14545fn validate_trigger_expr(expr: &TriggerExpr, schema: &Schema, event: TriggerEvent) -> Result<()> {
14546 match expr {
14547 TriggerExpr::Value(value) | TriggerExpr::IsNull(value) | TriggerExpr::IsNotNull(value) => {
14548 validate_trigger_value(value, schema, event)
14549 }
14550 TriggerExpr::Eq { left, right }
14551 | TriggerExpr::NotEq { left, right }
14552 | TriggerExpr::Lt { left, right }
14553 | TriggerExpr::Lte { left, right }
14554 | TriggerExpr::Gt { left, right }
14555 | TriggerExpr::Gte { left, right } => {
14556 validate_trigger_value(left, schema, event)?;
14557 validate_trigger_value(right, schema, event)
14558 }
14559 TriggerExpr::And { left, right } | TriggerExpr::Or { left, right } => {
14560 validate_trigger_expr(left, schema, event)?;
14561 validate_trigger_expr(right, schema, event)
14562 }
14563 TriggerExpr::Not(expr) => validate_trigger_expr(expr, schema, event),
14564 }
14565}
14566
14567fn validate_trigger_value(
14568 value: &TriggerValue,
14569 schema: &Schema,
14570 event: TriggerEvent,
14571) -> Result<()> {
14572 match value {
14573 TriggerValue::Literal(_) => Ok(()),
14574 TriggerValue::NewColumn(id) => {
14575 if event == TriggerEvent::Delete {
14576 return Err(MongrelError::InvalidArgument(
14577 "DELETE triggers cannot reference NEW".into(),
14578 ));
14579 }
14580 validate_column_id(*id, schema)
14581 }
14582 TriggerValue::OldColumn(id) => {
14583 if event == TriggerEvent::Insert {
14584 return Err(MongrelError::InvalidArgument(
14585 "INSERT triggers cannot reference OLD".into(),
14586 ));
14587 }
14588 validate_column_id(*id, schema)
14589 }
14590 TriggerValue::SelectedColumn(_) => Ok(()),
14594 }
14595}
14596
14597fn recover_ddl_from_wal(
14603 root: &Path,
14604 durable_root: Option<&crate::durable_file::DurableRoot>,
14605 target_catalog: &mut Catalog,
14606 meta_dek: Option<&[u8; META_DEK_LEN]>,
14607 wal_dek: Option<&zeroize::Zeroizing<[u8; 32]>>,
14608 apply: bool,
14609 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
14610) -> Result<()> {
14611 use crate::wal::SharedWal;
14612 let records = match durable_root {
14613 Some(root) => SharedWal::replay_durable_with_dek(root, wal_dek)?,
14614 None => SharedWal::replay_with_dek(root, wal_dek)?,
14615 };
14616 recover_ddl_from_records(
14617 root,
14618 durable_root,
14619 target_catalog,
14620 meta_dek,
14621 apply,
14622 table_roots,
14623 &records,
14624 )
14625}
14626
14627fn recover_ddl_from_records(
14628 root: &Path,
14629 durable_root: Option<&crate::durable_file::DurableRoot>,
14630 target_catalog: &mut Catalog,
14631 meta_dek: Option<&[u8; META_DEK_LEN]>,
14632 apply: bool,
14633 table_roots: Option<&HashMap<u64, Arc<crate::durable_file::DurableRoot>>>,
14634 records: &[crate::wal::Record],
14635) -> Result<()> {
14636 use crate::wal::{DdlOp, Op};
14637
14638 let original_catalog = target_catalog.clone();
14639 let mut recovered_catalog = original_catalog.clone();
14640 let cat = &mut recovered_catalog;
14641 let mut created_table_ids = HashSet::<u64>::new();
14642 let mut ttl_updates = HashMap::<u64, (Option<crate::manifest::TtlPolicy>, u64)>::new();
14643
14644 let mut committed: HashMap<u64, u64> = HashMap::new();
14645 for r in records {
14646 if let Op::TxnCommit { epoch: ce, .. } = r.op {
14647 committed.insert(r.txn_id, ce);
14648 }
14649 }
14650 let catalog_snapshot_txns = records
14651 .iter()
14652 .filter_map(|record| {
14653 (committed.contains_key(&record.txn_id)
14654 && matches!(&record.op, Op::Ddl(DdlOp::CatalogSnapshot { .. })))
14655 .then_some(record.txn_id)
14656 })
14657 .collect::<HashSet<_>>();
14658
14659 let mut changed = false;
14660 let mut applied_catalog_epoch = cat.db_epoch;
14661 let max_committed_epoch = committed.values().copied().max().unwrap_or(cat.db_epoch);
14662 for r in records.iter().cloned() {
14663 let Some(&ce) = committed.get(&r.txn_id) else {
14664 continue;
14665 };
14666 let txn_id = r.txn_id;
14667 match r.op {
14668 Op::Ddl(DdlOp::CreateTable {
14669 table_id,
14670 ref name,
14671 ref schema_json,
14672 }) => {
14673 if cat.tables.iter().any(|t| t.table_id == table_id) {
14674 continue;
14675 }
14676 let schema = DdlOp::decode_schema(schema_json)?;
14677 validate_recovered_schema(&schema)?;
14678 created_table_ids.insert(table_id);
14679 cat.tables.push(CatalogEntry {
14680 table_id,
14681 name: name.clone(),
14682 schema,
14683 state: TableState::Live,
14684 created_epoch: ce,
14685 });
14686 cat.next_table_id =
14687 cat.next_table_id
14688 .max(table_id.checked_add(1).ok_or_else(|| {
14689 MongrelError::Full("table id namespace exhausted".into())
14690 })?);
14691 changed = true;
14692 }
14693 Op::Ddl(DdlOp::CreateBuildingTable {
14694 table_id,
14695 ref build_name,
14696 ref intended_name,
14697 ref query_id,
14698 created_at_unix_nanos,
14699 ref schema_json,
14700 }) => {
14701 if cat.tables.iter().any(|table| table.table_id == table_id) {
14702 continue;
14703 }
14704 let schema = DdlOp::decode_schema(schema_json)?;
14705 validate_recovered_schema(&schema)?;
14706 created_table_ids.insert(table_id);
14707 cat.tables.push(CatalogEntry {
14708 table_id,
14709 name: build_name.clone(),
14710 schema,
14711 state: TableState::Building {
14712 intended_name: intended_name.clone(),
14713 query_id: query_id.clone(),
14714 created_at_unix_nanos,
14715 replaces_table_id: None,
14716 },
14717 created_epoch: ce,
14718 });
14719 cat.next_table_id =
14720 cat.next_table_id
14721 .max(table_id.checked_add(1).ok_or_else(|| {
14722 MongrelError::Full("table id namespace exhausted".into())
14723 })?);
14724 changed = true;
14725 }
14726 Op::Ddl(DdlOp::CreateRebuildingTable {
14727 table_id,
14728 ref build_name,
14729 ref intended_name,
14730 ref query_id,
14731 created_at_unix_nanos,
14732 replaces_table_id,
14733 ref schema_json,
14734 }) => {
14735 if cat.tables.iter().any(|table| table.table_id == table_id) {
14736 continue;
14737 }
14738 let schema = DdlOp::decode_schema(schema_json)?;
14739 validate_recovered_schema(&schema)?;
14740 created_table_ids.insert(table_id);
14741 cat.tables.push(CatalogEntry {
14742 table_id,
14743 name: build_name.clone(),
14744 schema,
14745 state: TableState::Building {
14746 intended_name: intended_name.clone(),
14747 query_id: query_id.clone(),
14748 created_at_unix_nanos,
14749 replaces_table_id: Some(replaces_table_id),
14750 },
14751 created_epoch: ce,
14752 });
14753 cat.next_table_id =
14754 cat.next_table_id
14755 .max(table_id.checked_add(1).ok_or_else(|| {
14756 MongrelError::Full("table id namespace exhausted".into())
14757 })?);
14758 changed = true;
14759 }
14760 Op::Ddl(DdlOp::DropTable { table_id }) => {
14761 let mut dropped_name = None;
14762 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
14763 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
14764 dropped_name = Some(entry.name.clone());
14765 entry.state = TableState::Dropped { at_epoch: ce };
14766 changed = true;
14767 }
14768 }
14769 if let Some(name) = dropped_name {
14770 let before = cat.materialized_views.len();
14771 cat.materialized_views
14772 .retain(|definition| definition.name != name);
14773 changed |= before != cat.materialized_views.len();
14774 cat.security.rls_tables.retain(|table| table != &name);
14775 cat.security.policies.retain(|policy| policy.table != name);
14776 cat.security.masks.retain(|mask| mask.table != name);
14777 for role in &mut cat.roles {
14778 role.permissions
14779 .retain(|permission| permission_table(permission) != Some(&name));
14780 }
14781 if !catalog_snapshot_txns.contains(&txn_id) {
14782 advance_security_version(cat)?;
14783 }
14784 }
14785 }
14786 Op::Ddl(DdlOp::PublishBuildingTable {
14787 table_id,
14788 ref new_name,
14789 }) => {
14790 if let Some(entry) = cat
14791 .tables
14792 .iter_mut()
14793 .find(|table| table.table_id == table_id)
14794 {
14795 if entry.name != *new_name || !matches!(entry.state, TableState::Live) {
14796 entry.name = new_name.clone();
14797 entry.state = TableState::Live;
14798 changed = true;
14799 }
14800 }
14801 }
14802 Op::Ddl(DdlOp::ReplaceBuildingTable {
14803 table_id,
14804 replaced_table_id,
14805 ref new_name,
14806 }) => {
14807 changed |=
14808 apply_rebuilding_publish(cat, table_id, replaced_table_id, new_name, ce)?;
14809 }
14810 Op::Ddl(DdlOp::RenameTable {
14811 table_id,
14812 ref new_name,
14813 }) => {
14814 let mut old_name = None;
14815 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
14816 if entry.name != *new_name {
14817 old_name = Some(entry.name.clone());
14818 entry.name = new_name.clone();
14819 changed = true;
14820 }
14821 }
14822 if let Some(old_name) = old_name {
14823 if let Some(definition) = cat
14824 .materialized_views
14825 .iter_mut()
14826 .find(|definition| definition.name == old_name)
14827 {
14828 definition.name = new_name.clone();
14829 }
14830 for table in &mut cat.security.rls_tables {
14831 if *table == old_name {
14832 *table = new_name.clone();
14833 }
14834 }
14835 for policy in &mut cat.security.policies {
14836 if policy.table == old_name {
14837 policy.table = new_name.clone();
14838 }
14839 }
14840 for mask in &mut cat.security.masks {
14841 if mask.table == old_name {
14842 mask.table = new_name.clone();
14843 }
14844 }
14845 for role in &mut cat.roles {
14846 for permission in &mut role.permissions {
14847 rename_permission_table(permission, &old_name, new_name);
14848 }
14849 }
14850 if !catalog_snapshot_txns.contains(&txn_id) {
14851 advance_security_version(cat)?;
14852 }
14853 }
14854 }
14858 Op::Ddl(DdlOp::AlterTable {
14859 table_id,
14860 ref column_json,
14861 }) => {
14862 let column = DdlOp::decode_column(column_json)?;
14863 let mut renamed = None;
14864 if let Some(entry) = cat.tables.iter_mut().find(|t| t.table_id == table_id) {
14865 renamed = entry
14866 .schema
14867 .columns
14868 .iter()
14869 .find(|existing| existing.id == column.id && existing.name != column.name)
14870 .map(|existing| {
14871 (
14872 entry.name.clone(),
14873 existing.name.clone(),
14874 column.name.clone(),
14875 )
14876 });
14877 if apply_recovered_column_def(&mut entry.schema, column)? {
14878 validate_recovered_schema(&entry.schema)?;
14879 changed = true;
14880 }
14881 }
14882 if let Some((table, old_name, new_name)) = renamed {
14883 for role in &mut cat.roles {
14884 for permission in &mut role.permissions {
14885 rename_permission_column(permission, &table, &old_name, &new_name);
14886 }
14887 }
14888 if !catalog_snapshot_txns.contains(&txn_id) {
14889 advance_security_version(cat)?;
14890 }
14891 }
14892 }
14893 Op::Ddl(DdlOp::SetTtl {
14894 table_id,
14895 ref policy_json,
14896 }) => {
14897 let policy = DdlOp::decode_ttl(policy_json)?;
14898 let entry = cat
14899 .tables
14900 .iter()
14901 .find(|entry| entry.table_id == table_id)
14902 .ok_or_else(|| {
14903 MongrelError::Schema(format!(
14904 "recovered TTL references unknown table id {table_id}"
14905 ))
14906 })?;
14907 if let Some(policy) = policy {
14908 let valid = entry
14909 .schema
14910 .columns
14911 .iter()
14912 .find(|column| column.id == policy.column_id)
14913 .is_some_and(|column| {
14914 column.ty == TypeId::TimestampNanos
14915 && policy.duration_nanos > 0
14916 && policy.duration_nanos <= i64::MAX as u64
14917 });
14918 if !valid {
14919 return Err(MongrelError::Schema(format!(
14920 "invalid recovered TTL policy for table id {table_id}"
14921 )));
14922 }
14923 }
14924 ttl_updates.insert(table_id, (policy, ce));
14925 }
14926 Op::Ddl(DdlOp::SetMaterializedView {
14927 ref name,
14928 ref definition_json,
14929 }) => {
14930 let definition = DdlOp::decode_materialized_view(definition_json)?;
14931 if definition.name != *name {
14932 return Err(MongrelError::Schema(format!(
14933 "materialized view WAL name mismatch: {name:?}"
14934 )));
14935 }
14936 if cat.live(name).is_some() {
14937 if let Some(existing) = cat
14938 .materialized_views
14939 .iter_mut()
14940 .find(|existing| existing.name == *name)
14941 {
14942 if *existing != definition {
14943 *existing = definition;
14944 changed = true;
14945 }
14946 } else {
14947 cat.materialized_views.push(definition);
14948 changed = true;
14949 }
14950 }
14951 }
14952 Op::Ddl(DdlOp::SetSecurityCatalog { ref security_json }) => {
14953 let security = DdlOp::decode_security(security_json)?;
14954 validate_security_catalog(cat, &security)?;
14955 if cat.security != security {
14956 cat.security = security;
14957 if !catalog_snapshot_txns.contains(&txn_id) {
14958 advance_security_version(cat)?;
14959 }
14960 changed = true;
14961 }
14962 }
14963 Op::Ddl(DdlOp::SetSqlPragma { ref key, value }) => {
14964 let target = match key.as_str() {
14965 "user_version" => &mut cat.user_version,
14966 "application_id" => &mut cat.application_id,
14967 _ => {
14968 return Err(MongrelError::InvalidArgument(format!(
14969 "unsupported recovered SQL pragma {key:?}"
14970 )))
14971 }
14972 };
14973 if *target != Some(value) {
14974 *target = Some(value);
14975 cat.db_epoch = cat.db_epoch.max(ce);
14976 changed = true;
14977 }
14978 }
14979 Op::Ddl(DdlOp::CatalogSnapshot { ref catalog_json }) => {
14980 if ce <= applied_catalog_epoch {
14981 continue;
14982 }
14983 let snapshot = DdlOp::decode_catalog(catalog_json)?;
14984 if snapshot.db_epoch != ce {
14985 return Err(MongrelError::Schema(format!(
14986 "catalog snapshot epoch {} does not match WAL commit epoch {ce}",
14987 snapshot.db_epoch
14988 )));
14989 }
14990 validate_recovered_catalog(&snapshot)?;
14991 validate_catalog_transition(cat, &snapshot)?;
14992 *cat = snapshot;
14993 applied_catalog_epoch = ce;
14994 changed = true;
14995 }
14996 _ => {}
14997 }
14998 }
14999
15000 if cat.db_epoch < max_committed_epoch {
15001 cat.db_epoch = max_committed_epoch;
15002 changed = true;
15003 }
15004 changed |= repair_catalog_allocator_counters(cat)?;
15005
15006 validate_recovered_catalog(cat)?;
15007 let storage_reconciliation = validate_recovered_storage_plan(
15008 root,
15009 durable_root,
15010 cat,
15011 &created_table_ids,
15012 &ttl_updates,
15013 meta_dek,
15014 )?;
15015
15016 let needs_storage_apply = !storage_reconciliation.is_empty() || !ttl_updates.is_empty();
15017 if apply && (changed || needs_storage_apply) {
15018 for table_id in storage_reconciliation {
15019 let entry = cat
15020 .tables
15021 .iter()
15022 .find(|entry| entry.table_id == table_id)
15023 .ok_or_else(|| MongrelError::CorruptWal {
15024 offset: table_id,
15025 reason: "recovery storage plan lost its catalog table".into(),
15026 })?;
15027 ensure_recovered_table_storage(
15028 table_roots
15029 .and_then(|roots| roots.get(&table_id))
15030 .map(Arc::as_ref),
15031 durable_root,
15032 &root.join(TABLES_DIR).join(table_id.to_string()),
15033 table_id,
15034 &entry.schema,
15035 meta_dek,
15036 )?;
15037 }
15038 for (table_id, (policy, ttl_epoch)) in ttl_updates {
15039 let Some(entry) = cat.tables.iter().find(|entry| {
15040 entry.table_id == table_id
15041 && matches!(entry.state, TableState::Live | TableState::Building { .. })
15042 }) else {
15043 continue;
15044 };
15045 let table_root = if let Some(root) = table_roots.and_then(|roots| roots.get(&table_id))
15046 {
15047 root.try_clone()?
15048 } else if let Some(root) = durable_root {
15049 root.open_directory(Path::new(TABLES_DIR).join(table_id.to_string()))?
15050 } else {
15051 crate::durable_file::DurableRoot::open(
15052 root.join(TABLES_DIR).join(table_id.to_string()),
15053 )?
15054 };
15055 let table_dir = table_root.io_path()?;
15056 let mut manifest = crate::manifest::read_durable(&table_root, "", meta_dek)?;
15057 if manifest.ttl != policy || manifest.current_epoch < ttl_epoch {
15058 manifest.ttl = policy;
15059 manifest.current_epoch = manifest.current_epoch.max(ttl_epoch);
15060 manifest.schema_id = entry.schema.schema_id;
15061 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
15062 }
15063 }
15064 if changed {
15065 match durable_root {
15066 Some(root) => catalog::write_durable(root, cat, meta_dek)?,
15067 None => catalog::write_atomic(root, cat, meta_dek)?,
15068 }
15069 }
15070 }
15071 *target_catalog = recovered_catalog;
15072 Ok(())
15073}
15074
15075fn ensure_recovered_table_storage(
15076 pinned_table: Option<&crate::durable_file::DurableRoot>,
15077 durable_root: Option<&crate::durable_file::DurableRoot>,
15078 fallback_table_dir: &Path,
15079 table_id: u64,
15080 schema: &Schema,
15081 meta_dek: Option<&[u8; META_DEK_LEN]>,
15082) -> Result<()> {
15083 let table_root = if let Some(root) = pinned_table {
15084 root.try_clone()?
15085 } else if let Some(root) = durable_root {
15086 let relative = Path::new(TABLES_DIR).join(table_id.to_string());
15087 match root.open_directory(&relative) {
15088 Ok(table) => table,
15089 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
15090 root.create_directory_all_pinned(relative)?
15091 }
15092 Err(error) => return Err(error.into()),
15093 }
15094 } else {
15095 crate::durable_file::create_directory_all(fallback_table_dir)?;
15096 crate::durable_file::DurableRoot::open(fallback_table_dir)?
15097 };
15098 let table_dir = table_root.io_path()?;
15099 let mut existing_manifest = match crate::manifest::read_durable(&table_root, "", meta_dek) {
15100 Ok(manifest) => {
15101 if manifest.table_id != table_id {
15102 return Err(MongrelError::Conflict(format!(
15103 "recovered table directory id mismatch: expected {table_id}, found {}",
15104 manifest.table_id
15105 )));
15106 }
15107 Some(manifest)
15108 }
15109 Err(MongrelError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => None,
15110 Err(error) => return Err(error),
15111 };
15112
15113 table_root.create_directory_all(crate::engine::WAL_DIR)?;
15114 table_root.create_directory_all(crate::engine::RUNS_DIR)?;
15115 crate::engine::write_schema(&table_dir, schema)?;
15116
15117 if let Some(mut manifest) = existing_manifest.take() {
15118 if manifest.schema_id != schema.schema_id {
15119 manifest.schema_id = schema.schema_id;
15120 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
15121 }
15122 } else {
15123 let mut manifest = crate::manifest::Manifest::new(table_id, schema.schema_id);
15125 crate::manifest::write_atomic(&table_dir, &mut manifest, meta_dek)?;
15126 }
15127 Ok(())
15128}
15129
15130fn validate_recovered_schema(schema: &Schema) -> Result<()> {
15131 schema.validate_auto_increment()?;
15132 schema.validate_defaults()?;
15133 schema.validate_ai()?;
15134 let mut column_ids = HashSet::new();
15135 let mut column_names = HashSet::new();
15136 for column in &schema.columns {
15137 if !column_ids.insert(column.id) || !column_names.insert(column.name.as_str()) {
15138 return Err(MongrelError::Schema(
15139 "recovered schema contains duplicate columns".into(),
15140 ));
15141 }
15142 match &column.ty {
15143 TypeId::Decimal128 { precision, scale }
15144 if *precision == 0 || *precision > 38 || scale.unsigned_abs() > *precision =>
15145 {
15146 return Err(MongrelError::Schema(format!(
15147 "column {:?} has invalid decimal precision or scale",
15148 column.name
15149 )));
15150 }
15151 TypeId::Enum { variants }
15152 if variants.is_empty()
15153 || variants.iter().any(String::is_empty)
15154 || variants.iter().collect::<HashSet<_>>().len() != variants.len() =>
15155 {
15156 return Err(MongrelError::Schema(format!(
15157 "column {:?} has invalid enum variants",
15158 column.name
15159 )));
15160 }
15161 _ => {}
15162 }
15163 }
15164 let mut index_names = HashSet::new();
15165 for index in &schema.indexes {
15166 index.validate_options()?;
15167 if index.name.is_empty()
15168 || !index_names.insert(index.name.as_str())
15169 || schema
15170 .columns
15171 .iter()
15172 .all(|column| column.id != index.column_id)
15173 {
15174 return Err(MongrelError::Schema(format!(
15175 "recovered index {:?} references missing column {}",
15176 index.name, index.column_id
15177 )));
15178 }
15179 }
15180 let mut colocated = HashSet::new();
15181 for group in &schema.colocation {
15182 if group.is_empty()
15183 || group.iter().any(|id| !column_ids.contains(id))
15184 || group.iter().any(|id| !colocated.insert(*id))
15185 {
15186 return Err(MongrelError::Schema(
15187 "recovered schema contains invalid column co-location groups".into(),
15188 ));
15189 }
15190 }
15191
15192 let mut constraint_ids = HashSet::new();
15193 let mut constraint_names = HashSet::<String>::new();
15194 let mut validate_constraint_identity = |id: u16, name: &str| -> Result<()> {
15195 if name.is_empty()
15196 || !constraint_ids.insert(id)
15197 || !constraint_names.insert(name.to_owned())
15198 {
15199 return Err(MongrelError::Schema(
15200 "recovered schema contains duplicate or empty constraint identities".into(),
15201 ));
15202 }
15203 Ok(())
15204 };
15205 for unique in &schema.constraints.uniques {
15206 validate_constraint_identity(unique.id, &unique.name)?;
15207 if unique.columns.is_empty()
15208 || unique.columns.iter().any(|id| !column_ids.contains(id))
15209 || unique.columns.iter().collect::<HashSet<_>>().len() != unique.columns.len()
15210 {
15211 return Err(MongrelError::Schema(format!(
15212 "unique constraint {:?} has invalid columns",
15213 unique.name
15214 )));
15215 }
15216 }
15217 for foreign_key in &schema.constraints.foreign_keys {
15218 validate_constraint_identity(foreign_key.id, &foreign_key.name)?;
15219 if foreign_key.ref_table.is_empty()
15220 || foreign_key.columns.is_empty()
15221 || foreign_key.columns.len() != foreign_key.ref_columns.len()
15222 || foreign_key
15223 .columns
15224 .iter()
15225 .any(|id| !column_ids.contains(id))
15226 || foreign_key.columns.iter().collect::<HashSet<_>>().len() != foreign_key.columns.len()
15227 || foreign_key.ref_columns.iter().collect::<HashSet<_>>().len()
15228 != foreign_key.ref_columns.len()
15229 {
15230 return Err(MongrelError::Schema(format!(
15231 "foreign key {:?} has invalid columns",
15232 foreign_key.name
15233 )));
15234 }
15235 if (matches!(foreign_key.on_delete, crate::constraint::FkAction::SetNull)
15236 || matches!(foreign_key.on_update, crate::constraint::FkAction::SetNull))
15237 && foreign_key.columns.iter().any(|id| {
15238 schema
15239 .columns
15240 .iter()
15241 .find(|column| column.id == *id)
15242 .is_none_or(|column| {
15243 !column.flags.contains(crate::schema::ColumnFlags::NULLABLE)
15244 })
15245 })
15246 {
15247 return Err(MongrelError::Schema(format!(
15248 "foreign key {:?} uses SET NULL on a non-nullable column",
15249 foreign_key.name
15250 )));
15251 }
15252 }
15253 for check in &schema.constraints.checks {
15254 validate_constraint_identity(check.id, &check.name)?;
15255 check.expr.validate()?;
15256 validate_check_columns(&check.expr, &column_ids)?;
15257 }
15258 Ok(())
15259}
15260
15261fn validate_check_columns(
15262 expression: &crate::constraint::CheckExpr,
15263 column_ids: &HashSet<u16>,
15264) -> Result<()> {
15265 use crate::constraint::CheckExpr;
15266 match expression {
15267 CheckExpr::Col(id) | CheckExpr::IsNull(id) | CheckExpr::IsNotNull(id) => {
15268 if column_ids.contains(id) {
15269 Ok(())
15270 } else {
15271 Err(MongrelError::Schema(format!(
15272 "check constraint references unknown column {id}"
15273 )))
15274 }
15275 }
15276 CheckExpr::Regex { col, .. } => {
15277 if column_ids.contains(col) {
15278 Ok(())
15279 } else {
15280 Err(MongrelError::Schema(format!(
15281 "check constraint references unknown column {col}"
15282 )))
15283 }
15284 }
15285 CheckExpr::Add(left, right)
15286 | CheckExpr::Sub(left, right)
15287 | CheckExpr::Mul(left, right)
15288 | CheckExpr::Div(left, right)
15289 | CheckExpr::Mod(left, right)
15290 | CheckExpr::Eq(left, right)
15291 | CheckExpr::Ne(left, right)
15292 | CheckExpr::Lt(left, right)
15293 | CheckExpr::Le(left, right)
15294 | CheckExpr::Gt(left, right)
15295 | CheckExpr::Ge(left, right)
15296 | CheckExpr::And(left, right)
15297 | CheckExpr::Or(left, right) => {
15298 validate_check_columns(left, column_ids)?;
15299 validate_check_columns(right, column_ids)
15300 }
15301 CheckExpr::Not(inner) => validate_check_columns(inner, column_ids),
15302 CheckExpr::True | CheckExpr::Lit(_) => Ok(()),
15303 }
15304}
15305
15306fn validate_catalog_transition(current: &Catalog, next: &Catalog) -> Result<()> {
15307 for (name, prior, candidate) in [
15308 ("db_epoch", current.db_epoch, next.db_epoch),
15309 ("next_table_id", current.next_table_id, next.next_table_id),
15310 (
15311 "next_segment_no",
15312 current.next_segment_no,
15313 next.next_segment_no,
15314 ),
15315 ("next_user_id", current.next_user_id, next.next_user_id),
15316 (
15317 "security_version",
15318 current.security_version,
15319 next.security_version,
15320 ),
15321 ] {
15322 if candidate < prior {
15323 return Err(MongrelError::Schema(format!(
15324 "catalog snapshot rolls back {name} from {prior} to {candidate}"
15325 )));
15326 }
15327 }
15328 for prior in ¤t.tables {
15329 let Some(candidate) = next
15330 .tables
15331 .iter()
15332 .find(|entry| entry.table_id == prior.table_id)
15333 else {
15334 return Err(MongrelError::Schema(format!(
15335 "catalog snapshot removes table identity {}",
15336 prior.table_id
15337 )));
15338 };
15339 if candidate.created_epoch != prior.created_epoch
15340 || candidate.schema.schema_id < prior.schema.schema_id
15341 || matches!(prior.state, TableState::Dropped { .. })
15342 && !matches!(candidate.state, TableState::Dropped { .. })
15343 {
15344 return Err(MongrelError::Schema(format!(
15345 "catalog snapshot rolls back table identity {}",
15346 prior.table_id
15347 )));
15348 }
15349 }
15350 for prior in ¤t.users {
15351 if let Some(candidate) = next.users.iter().find(|user| user.id == prior.id) {
15352 if candidate.username != prior.username
15353 || candidate.created_epoch != prior.created_epoch
15354 {
15355 return Err(MongrelError::Schema(format!(
15356 "catalog snapshot reuses user identity {}",
15357 prior.id
15358 )));
15359 }
15360 }
15361 }
15362 Ok(())
15363}
15364
15365fn validate_recovered_catalog(catalog: &Catalog) -> Result<()> {
15366 let mut table_ids = HashSet::new();
15367 let mut active_names = HashSet::new();
15368 let mut max_table_id = None::<u64>;
15369 for entry in &catalog.tables {
15370 if !table_ids.insert(entry.table_id) {
15371 return Err(MongrelError::Schema(format!(
15372 "catalog contains duplicate table id {}",
15373 entry.table_id
15374 )));
15375 }
15376 max_table_id = Some(max_table_id.map_or(entry.table_id, |value| value.max(entry.table_id)));
15377 if entry.name.is_empty() || entry.created_epoch > catalog.db_epoch {
15378 return Err(MongrelError::Schema(format!(
15379 "catalog table {} has invalid name or creation epoch",
15380 entry.table_id
15381 )));
15382 }
15383 validate_recovered_schema(&entry.schema)?;
15384 match &entry.state {
15385 TableState::Live => {
15386 if !active_names.insert(entry.name.as_str()) {
15387 return Err(MongrelError::Schema(format!(
15388 "catalog contains duplicate active table name {:?}",
15389 entry.name
15390 )));
15391 }
15392 }
15393 TableState::Dropped { at_epoch } => {
15394 if *at_epoch < entry.created_epoch || *at_epoch > catalog.db_epoch {
15395 return Err(MongrelError::Schema(format!(
15396 "catalog table {} has invalid drop epoch {at_epoch}",
15397 entry.table_id
15398 )));
15399 }
15400 }
15401 TableState::Building {
15402 intended_name,
15403 query_id,
15404 replaces_table_id,
15405 ..
15406 } => {
15407 if intended_name.is_empty() || query_id.is_empty() {
15408 return Err(MongrelError::Schema(format!(
15409 "building table {} has empty identity fields",
15410 entry.table_id
15411 )));
15412 }
15413 if !active_names.insert(entry.name.as_str()) {
15414 return Err(MongrelError::Schema(format!(
15415 "catalog contains duplicate active/building table name {:?}",
15416 entry.name
15417 )));
15418 }
15419 if replaces_table_id.is_some_and(|id| id == entry.table_id) {
15420 return Err(MongrelError::Schema(
15421 "building table cannot replace itself".into(),
15422 ));
15423 }
15424 }
15425 }
15426 }
15427 if let Some(maximum) = max_table_id {
15428 let required = maximum
15429 .checked_add(1)
15430 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
15431 if catalog.next_table_id < required {
15432 return Err(MongrelError::Schema(format!(
15433 "catalog next_table_id {} precedes required {required}",
15434 catalog.next_table_id
15435 )));
15436 }
15437 }
15438 for entry in &catalog.tables {
15439 if let TableState::Building {
15440 replaces_table_id: Some(replaced),
15441 ..
15442 } = entry.state
15443 {
15444 if !table_ids.contains(&replaced) {
15445 return Err(MongrelError::Schema(format!(
15446 "building table {} replaces unknown table {replaced}",
15447 entry.table_id
15448 )));
15449 }
15450 }
15451 }
15452 for entry in &catalog.tables {
15453 if matches!(entry.state, TableState::Live | TableState::Building { .. }) {
15454 validate_foreign_key_targets(catalog, &entry.schema)?;
15455 }
15456 }
15457
15458 let mut external_names = HashSet::new();
15459 for entry in &catalog.external_tables {
15460 entry.validate()?;
15461 validate_recovered_schema(&entry.declared_schema)?;
15462 if !entry.declared_schema.constraints.is_empty() {
15463 return Err(MongrelError::Schema(format!(
15464 "external table {:?} cannot carry engine-enforced constraints",
15465 entry.name
15466 )));
15467 }
15468 if entry.created_epoch > catalog.db_epoch
15469 || !external_names.insert(entry.name.as_str())
15470 || active_names.contains(entry.name.as_str())
15471 {
15472 return Err(MongrelError::Schema(format!(
15473 "invalid or duplicate external table {:?}",
15474 entry.name
15475 )));
15476 }
15477 }
15478
15479 let mut procedure_names = HashSet::new();
15480 for entry in &catalog.procedures {
15481 entry.procedure.validate()?;
15482 if entry.procedure.created_epoch > entry.procedure.updated_epoch
15483 || entry.procedure.updated_epoch > catalog.db_epoch
15484 || !procedure_names.insert(entry.procedure.name.as_str())
15485 {
15486 return Err(MongrelError::Schema(format!(
15487 "invalid or duplicate procedure {:?}",
15488 entry.procedure.name
15489 )));
15490 }
15491 validate_recovered_procedure_references(catalog, &entry.procedure)?;
15492 }
15493
15494 let mut trigger_names = HashSet::new();
15495 for entry in &catalog.triggers {
15496 entry.trigger.validate()?;
15497 if entry.trigger.created_epoch > entry.trigger.updated_epoch
15498 || entry.trigger.updated_epoch > catalog.db_epoch
15499 || !trigger_names.insert(entry.trigger.name.as_str())
15500 {
15501 return Err(MongrelError::Schema(format!(
15502 "invalid or duplicate trigger {:?}",
15503 entry.trigger.name
15504 )));
15505 }
15506 validate_recovered_trigger_references(catalog, &entry.trigger)?;
15507 }
15508
15509 let mut views = HashSet::new();
15510 for view in &catalog.materialized_views {
15511 let target = catalog.live(&view.name).ok_or_else(|| {
15512 MongrelError::Schema(format!(
15513 "materialized view {:?} has no live table",
15514 view.name
15515 ))
15516 })?;
15517 if view.name.is_empty()
15518 || view.query.trim().is_empty()
15519 || view.last_refresh_epoch > catalog.db_epoch
15520 || !views.insert(view.name.as_str())
15521 {
15522 return Err(MongrelError::Schema(format!(
15523 "materialized view {:?} has no unique live table",
15524 view.name
15525 )));
15526 }
15527 if let Some(incremental) = &view.incremental {
15528 let source = catalog.live(&incremental.source_table).ok_or_else(|| {
15529 MongrelError::Schema(format!(
15530 "materialized view {:?} references missing source {:?}",
15531 view.name, incremental.source_table
15532 ))
15533 })?;
15534 if source.table_id != incremental.source_table_id
15535 || source
15536 .schema
15537 .columns
15538 .iter()
15539 .all(|column| column.id != incremental.group_column)
15540 {
15541 return Err(MongrelError::Schema(format!(
15542 "materialized view {:?} has invalid incremental source",
15543 view.name
15544 )));
15545 }
15546 let target_ids = target
15547 .schema
15548 .columns
15549 .iter()
15550 .map(|column| column.id)
15551 .collect::<HashSet<_>>();
15552 let mut output_ids = HashSet::new();
15553 let count_outputs = incremental
15554 .outputs
15555 .iter()
15556 .filter(|output| {
15557 matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
15558 })
15559 .count();
15560 if incremental.checkpoint_event_id.is_empty()
15561 || !target_ids.contains(&incremental.group_output_column)
15562 || !target_ids.contains(&incremental.count_output_column)
15563 || incremental.outputs.is_empty()
15564 || count_outputs != 1
15565 || incremental.outputs.iter().any(|output| {
15566 !target_ids.contains(&output.output_column)
15567 || output.output_column == incremental.group_output_column
15568 || !output_ids.insert(output.output_column)
15569 || matches!(output.kind, crate::catalog::IncrementalAggregateKind::Count)
15570 && output.output_column != incremental.count_output_column
15571 || match output.kind {
15572 crate::catalog::IncrementalAggregateKind::Sum { source_column } => {
15573 source
15574 .schema
15575 .columns
15576 .iter()
15577 .all(|column| column.id != source_column)
15578 }
15579 crate::catalog::IncrementalAggregateKind::Count => false,
15580 }
15581 })
15582 {
15583 return Err(MongrelError::Schema(format!(
15584 "materialized view {:?} has invalid incremental outputs",
15585 view.name
15586 )));
15587 }
15588 }
15589 }
15590
15591 validate_security_catalog(catalog, &catalog.security)?;
15592 validate_recovered_auth_catalog(catalog)?;
15593 Ok(())
15594}
15595
15596fn repair_catalog_allocator_counters(catalog: &mut Catalog) -> Result<bool> {
15597 let mut changed = false;
15598 if let Some(maximum) = catalog.tables.iter().map(|entry| entry.table_id).max() {
15599 let required = maximum
15600 .checked_add(1)
15601 .ok_or_else(|| MongrelError::Full("table id namespace exhausted".into()))?;
15602 if catalog.next_table_id < required {
15603 catalog.next_table_id = required;
15604 changed = true;
15605 }
15606 }
15607 if let Some(maximum) = catalog.users.iter().map(|user| user.id).max() {
15608 let required = maximum
15609 .checked_add(1)
15610 .ok_or_else(|| MongrelError::Full("user id namespace exhausted".into()))?;
15611 if catalog.next_user_id < required {
15612 catalog.next_user_id = required;
15613 changed = true;
15614 }
15615 }
15616 Ok(changed)
15617}
15618
15619fn validate_foreign_key_targets(catalog: &Catalog, schema: &Schema) -> Result<()> {
15620 for foreign_key in &schema.constraints.foreign_keys {
15621 let parent = catalog.live(&foreign_key.ref_table).ok_or_else(|| {
15622 MongrelError::Schema(format!(
15623 "foreign key {:?} references unknown live table {:?}",
15624 foreign_key.name, foreign_key.ref_table
15625 ))
15626 })?;
15627 let referenced_unique = parent
15628 .schema
15629 .constraints
15630 .uniques
15631 .iter()
15632 .any(|unique| unique.columns == foreign_key.ref_columns)
15633 || foreign_key.ref_columns.len() == 1
15634 && parent
15635 .schema
15636 .primary_key()
15637 .is_some_and(|column| column.id == foreign_key.ref_columns[0]);
15638 if !referenced_unique {
15639 return Err(MongrelError::Schema(format!(
15640 "foreign key {:?} does not reference a unique key",
15641 foreign_key.name
15642 )));
15643 }
15644 for (local_id, parent_id) in foreign_key.columns.iter().zip(&foreign_key.ref_columns) {
15645 let local = schema.columns.iter().find(|column| column.id == *local_id);
15646 let referenced = parent
15647 .schema
15648 .columns
15649 .iter()
15650 .find(|column| column.id == *parent_id);
15651 if local
15652 .zip(referenced)
15653 .is_none_or(|(local, referenced)| local.ty != referenced.ty)
15654 {
15655 return Err(MongrelError::Schema(format!(
15656 "foreign key {:?} has missing or incompatible columns",
15657 foreign_key.name
15658 )));
15659 }
15660 }
15661 }
15662 Ok(())
15663}
15664
15665fn validate_recovered_procedure_references(
15666 catalog: &Catalog,
15667 procedure: &StoredProcedure,
15668) -> Result<()> {
15669 for step in &procedure.body.steps {
15670 let Some(table_name) = step.table() else {
15671 continue;
15672 };
15673 let schema = &catalog
15674 .live(table_name)
15675 .ok_or_else(|| {
15676 MongrelError::Schema(format!(
15677 "procedure {:?} references unknown table {table_name:?}",
15678 procedure.name
15679 ))
15680 })?
15681 .schema;
15682 match step {
15683 ProcedureStep::NativeQuery {
15684 conditions,
15685 projection,
15686 ..
15687 } => {
15688 for condition in conditions {
15689 validate_condition_columns(condition, schema)?;
15690 }
15691 for id in projection.iter().flatten() {
15692 validate_column_id(*id, schema)?;
15693 }
15694 }
15695 ProcedureStep::Put { cells, .. } => {
15696 for cell in cells {
15697 validate_column_id(cell.column_id, schema)?;
15698 }
15699 }
15700 ProcedureStep::Upsert {
15701 cells,
15702 update_cells,
15703 ..
15704 } => {
15705 for cell in cells.iter().chain(update_cells.iter().flatten()) {
15706 validate_column_id(cell.column_id, schema)?;
15707 }
15708 }
15709 ProcedureStep::DeleteByPk { .. } if schema.primary_key().is_none() => {
15710 return Err(MongrelError::Schema(format!(
15711 "procedure {:?} deletes by primary key on table without one",
15712 procedure.name
15713 )));
15714 }
15715 ProcedureStep::DeleteByPk { .. }
15716 | ProcedureStep::DeleteRows { .. }
15717 | ProcedureStep::SqlQuery { .. } => {}
15718 }
15719 }
15720 Ok(())
15721}
15722
15723fn validate_recovered_trigger_references(catalog: &Catalog, trigger: &StoredTrigger) -> Result<()> {
15724 let target_schema = match &trigger.target {
15725 TriggerTarget::Table(name) => catalog
15726 .live(name)
15727 .ok_or_else(|| {
15728 MongrelError::Schema(format!(
15729 "trigger {:?} references unknown table {name:?}",
15730 trigger.name
15731 ))
15732 })?
15733 .schema
15734 .clone(),
15735 TriggerTarget::View(_) => Schema {
15736 columns: trigger.target_columns.clone(),
15737 ..Schema::default()
15738 },
15739 };
15740 for column in &trigger.update_of {
15741 if target_schema.column(column).is_none() {
15742 return Err(MongrelError::Schema(format!(
15743 "trigger {:?} references unknown UPDATE OF column {column:?}",
15744 trigger.name
15745 )));
15746 }
15747 }
15748 if let Some(expr) = &trigger.when {
15749 validate_trigger_expr(expr, &target_schema, trigger.event)?;
15750 }
15751 let mut selects = HashMap::new();
15752 for step in &trigger.program.steps {
15753 if matches!(step, TriggerStep::SetNew { .. }) && trigger.timing != TriggerTiming::Before {
15754 return Err(MongrelError::Schema(
15755 "SetNew is only valid in BEFORE triggers".into(),
15756 ));
15757 }
15758 validate_trigger_step(step, catalog, &target_schema, trigger.event, &mut selects)?;
15759 }
15760 Ok(())
15761}
15762
15763fn validate_recovered_auth_catalog(catalog: &Catalog) -> Result<()> {
15764 let mut role_names = HashSet::new();
15765 for role in &catalog.roles {
15766 if role.name.is_empty()
15767 || role.created_epoch > catalog.db_epoch
15768 || !role_names.insert(role.name.as_str())
15769 {
15770 return Err(MongrelError::Schema(format!(
15771 "invalid or duplicate role {:?}",
15772 role.name
15773 )));
15774 }
15775 for permission in &role.permissions {
15776 if let Some(table) = permission_table(permission) {
15777 let schema = catalog
15778 .live(table)
15779 .map(|entry| &entry.schema)
15780 .or_else(|| {
15781 catalog
15782 .external_tables
15783 .iter()
15784 .find(|entry| entry.name == table)
15785 .map(|entry| &entry.declared_schema)
15786 })
15787 .ok_or_else(|| {
15788 MongrelError::Schema(format!(
15789 "role {:?} references unknown table {table:?}",
15790 role.name
15791 ))
15792 })?;
15793 let columns = match permission {
15794 crate::auth::Permission::SelectColumns { columns, .. }
15795 | crate::auth::Permission::InsertColumns { columns, .. }
15796 | crate::auth::Permission::UpdateColumns { columns, .. } => Some(columns),
15797 _ => None,
15798 };
15799 if columns.is_some_and(|columns| {
15800 columns.is_empty()
15801 || columns.iter().any(|column| schema.column(column).is_none())
15802 }) {
15803 return Err(MongrelError::Schema(format!(
15804 "role {:?} contains invalid column permissions",
15805 role.name
15806 )));
15807 }
15808 }
15809 }
15810 }
15811 let mut user_ids = HashSet::new();
15812 let mut usernames = HashSet::new();
15813 let mut maximum_user_id = 0;
15814 for user in &catalog.users {
15815 maximum_user_id = maximum_user_id.max(user.id);
15816 if user.id == 0
15817 || user.username.is_empty()
15818 || user.password_hash.is_empty()
15819 || user.created_epoch > catalog.db_epoch
15820 || !user_ids.insert(user.id)
15821 || !usernames.insert(user.username.as_str())
15822 || user
15823 .roles
15824 .iter()
15825 .any(|role| !role_names.contains(role.as_str()))
15826 {
15827 return Err(MongrelError::Schema(format!(
15828 "invalid or duplicate user {:?}",
15829 user.username
15830 )));
15831 }
15832 }
15833 if !catalog.users.is_empty() && catalog.next_user_id <= maximum_user_id {
15834 return Err(MongrelError::Schema(
15835 "catalog next_user_id does not advance beyond existing user ids".into(),
15836 ));
15837 }
15838 if catalog.require_auth && !catalog.users.iter().any(|user| user.is_admin) {
15839 return Err(MongrelError::Schema(
15840 "authenticated catalog has no administrator".into(),
15841 ));
15842 }
15843 Ok(())
15844}
15845
15846fn validate_recovered_storage_plan(
15847 root: &Path,
15848 durable_root: Option<&crate::durable_file::DurableRoot>,
15849 catalog: &Catalog,
15850 created_table_ids: &HashSet<u64>,
15851 ttl_updates: &HashMap<u64, (Option<crate::manifest::TtlPolicy>, u64)>,
15852 meta_dek: Option<&[u8; META_DEK_LEN]>,
15853) -> Result<Vec<u64>> {
15854 const MAX_SCHEMA_BYTES: u64 = 16 * 1024 * 1024;
15855 let mut reconcile = Vec::new();
15856 for entry in &catalog.tables {
15857 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
15858 continue;
15859 }
15860 let relative_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
15861 let table_dir = root.join(TABLES_DIR).join(entry.table_id.to_string());
15862 let table_exists = match durable_root {
15863 Some(root) => match root.open_directory(&relative_dir) {
15864 Ok(_) => true,
15865 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
15866 Err(error) => return Err(error.into()),
15867 },
15868 None => table_dir.is_dir(),
15869 };
15870 if !table_exists {
15871 if created_table_ids.contains(&entry.table_id) {
15872 reconcile.push(entry.table_id);
15873 continue;
15874 }
15875 return Err(MongrelError::NotFound(format!(
15876 "catalog table {} storage is missing",
15877 entry.table_id
15878 )));
15879 }
15880 let manifest_result = match durable_root {
15881 Some(root) => crate::manifest::read_durable(root, &relative_dir, meta_dek),
15882 None => crate::manifest::read(&table_dir, meta_dek),
15883 };
15884 let manifest = match manifest_result {
15885 Ok(manifest) => manifest,
15886 Err(MongrelError::Io(error))
15887 if created_table_ids.contains(&entry.table_id)
15888 && error.kind() == std::io::ErrorKind::NotFound =>
15889 {
15890 reconcile.push(entry.table_id);
15891 continue;
15892 }
15893 Err(error) => return Err(error),
15894 };
15895 if manifest.table_id != entry.table_id {
15896 return Err(MongrelError::Conflict(format!(
15897 "catalog table {} storage identity mismatch",
15898 entry.table_id
15899 )));
15900 }
15901 let schema_result = match durable_root {
15902 Some(root) => root
15903 .open_regular(relative_dir.join(crate::engine::SCHEMA_FILENAME))
15904 .map_err(MongrelError::from),
15905 None => crate::durable_file::open_regular_nofollow(
15906 &table_dir.join(crate::engine::SCHEMA_FILENAME),
15907 ),
15908 };
15909 let file = match schema_result {
15910 Ok(file) => file,
15911 Err(MongrelError::Io(error))
15912 if created_table_ids.contains(&entry.table_id)
15913 && error.kind() == std::io::ErrorKind::NotFound =>
15914 {
15915 reconcile.push(entry.table_id);
15916 continue;
15917 }
15918 Err(error) => return Err(error),
15919 };
15920 let length = file.metadata()?.len();
15921 if length > MAX_SCHEMA_BYTES {
15922 return Err(MongrelError::ResourceLimitExceeded {
15923 resource: "recovered schema bytes",
15924 requested: usize::try_from(length).unwrap_or(usize::MAX),
15925 limit: MAX_SCHEMA_BYTES as usize,
15926 });
15927 }
15928 let disk_schema: Schema = serde_json::from_reader(file.take(MAX_SCHEMA_BYTES + 1))
15929 .map_err(|error| MongrelError::Schema(format!("decode recovered schema: {error}")))?;
15930 if manifest.schema_id != entry.schema.schema_id
15931 || crate::wal::DdlOp::encode_schema(&disk_schema)?
15932 != crate::wal::DdlOp::encode_schema(&entry.schema)?
15933 {
15934 reconcile.push(entry.table_id);
15935 }
15936 }
15937 for table_id in ttl_updates.keys() {
15938 if !catalog.tables.iter().any(|entry| {
15939 entry.table_id == *table_id
15940 && matches!(entry.state, TableState::Live | TableState::Building { .. })
15941 }) {
15942 continue;
15943 }
15944 let relative_dir = Path::new(TABLES_DIR).join(table_id.to_string());
15945 let table_exists = match durable_root {
15946 Some(root) => match root.open_directory(&relative_dir) {
15947 Ok(_) => true,
15948 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
15949 Err(error) => return Err(error.into()),
15950 },
15951 None => root.join(&relative_dir).is_dir(),
15952 };
15953 if !table_exists && !created_table_ids.contains(table_id) {
15954 return Err(MongrelError::NotFound(format!(
15955 "TTL recovery table {table_id} storage is missing"
15956 )));
15957 }
15958 }
15959 reconcile.sort_unstable();
15960 reconcile.dedup();
15961 Ok(reconcile)
15962}
15963
15964fn validate_catalog_table_storage(
15965 root: &crate::durable_file::DurableRoot,
15966 catalog: &Catalog,
15967 meta_dek: Option<&[u8; META_DEK_LEN]>,
15968) -> Result<()> {
15969 for entry in &catalog.tables {
15970 if !matches!(entry.state, TableState::Live | TableState::Building { .. }) {
15971 continue;
15972 }
15973 let table_dir = Path::new(TABLES_DIR).join(entry.table_id.to_string());
15974 let manifest = crate::manifest::read_durable(root, &table_dir, meta_dek)?;
15975 if manifest.table_id != entry.table_id || manifest.schema_id != entry.schema.schema_id {
15976 return Err(MongrelError::Conflict(format!(
15977 "catalog table {} storage identity mismatch",
15978 entry.table_id
15979 )));
15980 }
15981 root.open_regular(table_dir.join(crate::engine::SCHEMA_FILENAME))?;
15982 }
15983 Ok(())
15984}
15985
15986fn apply_recovered_column_def(schema: &mut Schema, column: ColumnDef) -> Result<bool> {
15987 match schema.columns.iter_mut().find(|c| c.id == column.id) {
15988 Some(existing) if *existing == column => Ok(false),
15989 Some(existing) => {
15990 *existing = column;
15991 schema.schema_id = schema
15992 .schema_id
15993 .checked_add(1)
15994 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
15995 Ok(true)
15996 }
15997 None => {
15998 schema.columns.push(column);
15999 schema.schema_id = schema
16000 .schema_id
16001 .checked_add(1)
16002 .ok_or_else(|| MongrelError::Schema("schema id space exhausted".into()))?;
16003 Ok(true)
16004 }
16005 }
16006}
16007
16008fn permission_table(permission: &crate::auth::Permission) -> Option<&str> {
16009 use crate::auth::Permission;
16010 match permission {
16011 Permission::Select { table }
16012 | Permission::Insert { table }
16013 | Permission::Update { table }
16014 | Permission::Delete { table }
16015 | Permission::SelectColumns { table, .. }
16016 | Permission::InsertColumns { table, .. }
16017 | Permission::UpdateColumns { table, .. } => Some(table),
16018 Permission::All | Permission::Ddl | Permission::Admin => None,
16019 }
16020}
16021
16022fn apply_rebuilding_publish(
16023 catalog: &mut Catalog,
16024 table_id: u64,
16025 replaced_table_id: u64,
16026 new_name: &str,
16027 epoch: u64,
16028) -> Result<bool> {
16029 let already_published = catalog.tables.iter().any(|entry| {
16030 entry.table_id == table_id
16031 && entry.name == new_name
16032 && matches!(entry.state, TableState::Live)
16033 }) && catalog.tables.iter().any(|entry| {
16034 entry.table_id == replaced_table_id && matches!(entry.state, TableState::Dropped { .. })
16035 });
16036 if already_published {
16037 return Ok(false);
16038 }
16039 let schema = catalog
16040 .tables
16041 .iter()
16042 .find(|entry| entry.table_id == table_id)
16043 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?
16044 .schema
16045 .clone();
16046 let replaced = catalog
16047 .tables
16048 .iter_mut()
16049 .find(|entry| entry.table_id == replaced_table_id)
16050 .ok_or_else(|| MongrelError::NotFound(format!("table id {replaced_table_id} not found")))?;
16051 replaced.state = TableState::Dropped { at_epoch: epoch };
16052 let replacement = catalog
16053 .tables
16054 .iter_mut()
16055 .find(|entry| entry.table_id == table_id)
16056 .ok_or_else(|| MongrelError::NotFound(format!("table id {table_id} not found")))?;
16057 replacement.name = new_name.to_string();
16058 replacement.state = TableState::Live;
16059
16060 for role in &mut catalog.roles {
16061 role.permissions.retain_mut(|permission| {
16062 retain_rebuilt_permission_columns(permission, new_name, &schema)
16063 });
16064 }
16065 for definition in &mut catalog.materialized_views {
16066 if let Some(incremental) = definition.incremental.as_mut() {
16067 if incremental.source_table == new_name
16068 && incremental.source_table_id == replaced_table_id
16069 {
16070 incremental.source_table_id = table_id;
16071 }
16072 }
16073 }
16074 advance_security_version(catalog)?;
16075 Ok(true)
16076}
16077
16078fn retain_rebuilt_permission_columns(
16079 permission: &mut crate::auth::Permission,
16080 target_table: &str,
16081 schema: &Schema,
16082) -> bool {
16083 use crate::auth::Permission;
16084 let columns = match permission {
16085 Permission::SelectColumns { table, columns }
16086 | Permission::InsertColumns { table, columns }
16087 | Permission::UpdateColumns { table, columns }
16088 if table == target_table =>
16089 {
16090 Some(columns)
16091 }
16092 _ => None,
16093 };
16094 if let Some(columns) = columns {
16095 columns.retain(|column| schema.column(column).is_some());
16096 !columns.is_empty()
16097 } else {
16098 true
16099 }
16100}
16101
16102fn rename_permission_table(permission: &mut crate::auth::Permission, old: &str, new: &str) {
16103 use crate::auth::Permission;
16104 let table = match permission {
16105 Permission::Select { table }
16106 | Permission::Insert { table }
16107 | Permission::Update { table }
16108 | Permission::Delete { table }
16109 | Permission::SelectColumns { table, .. }
16110 | Permission::InsertColumns { table, .. }
16111 | Permission::UpdateColumns { table, .. } => Some(table),
16112 Permission::All | Permission::Ddl | Permission::Admin => None,
16113 };
16114 if let Some(table) = table.filter(|table| table.as_str() == old) {
16115 *table = new.to_string();
16116 }
16117}
16118
16119fn rename_permission_column(
16120 permission: &mut crate::auth::Permission,
16121 target_table: &str,
16122 old: &str,
16123 new: &str,
16124) {
16125 use crate::auth::Permission;
16126 let columns = match permission {
16127 Permission::SelectColumns { table, columns }
16128 | Permission::InsertColumns { table, columns }
16129 | Permission::UpdateColumns { table, columns }
16130 if table == target_table =>
16131 {
16132 Some(columns)
16133 }
16134 _ => None,
16135 };
16136 if let Some(column) = columns
16137 .into_iter()
16138 .flatten()
16139 .find(|column| column.as_str() == old)
16140 {
16141 *column = new.to_string();
16142 }
16143}
16144
16145fn merge_permission(
16146 permissions: &mut Vec<crate::auth::Permission>,
16147 permission: crate::auth::Permission,
16148) {
16149 use crate::auth::Permission;
16150 let (kind, table, mut columns) = match permission {
16151 Permission::SelectColumns { table, columns } => (0, table, columns),
16152 Permission::InsertColumns { table, columns } => (1, table, columns),
16153 Permission::UpdateColumns { table, columns } => (2, table, columns),
16154 permission if !permissions.contains(&permission) => {
16155 permissions.push(permission);
16156 return;
16157 }
16158 _ => return,
16159 };
16160 for permission in permissions.iter_mut() {
16161 let existing = match permission {
16162 Permission::SelectColumns {
16163 table: existing_table,
16164 columns,
16165 } if kind == 0 && existing_table == &table => Some(columns),
16166 Permission::InsertColumns {
16167 table: existing_table,
16168 columns,
16169 } if kind == 1 && existing_table == &table => Some(columns),
16170 Permission::UpdateColumns {
16171 table: existing_table,
16172 columns,
16173 } if kind == 2 && existing_table == &table => Some(columns),
16174 _ => None,
16175 };
16176 if let Some(existing) = existing {
16177 existing.append(&mut columns);
16178 existing.sort();
16179 existing.dedup();
16180 return;
16181 }
16182 }
16183 columns.sort();
16184 columns.dedup();
16185 let permission = if kind == 0 {
16186 Permission::SelectColumns { table, columns }
16187 } else if kind == 1 {
16188 Permission::InsertColumns { table, columns }
16189 } else {
16190 Permission::UpdateColumns { table, columns }
16191 };
16192 permissions.push(permission);
16193}
16194
16195fn revoke_permission_from(
16196 permissions: &mut Vec<crate::auth::Permission>,
16197 revoked: &crate::auth::Permission,
16198) {
16199 use crate::auth::Permission;
16200 let revoked_columns = match revoked {
16201 Permission::SelectColumns { table, columns } => Some((0, table, columns)),
16202 Permission::InsertColumns { table, columns } => Some((1, table, columns)),
16203 Permission::UpdateColumns { table, columns } => Some((2, table, columns)),
16204 _ => None,
16205 };
16206 let Some((kind, table, columns)) = revoked_columns else {
16207 permissions.retain(|permission| permission != revoked);
16208 return;
16209 };
16210 for permission in permissions.iter_mut() {
16211 let current = match permission {
16212 Permission::SelectColumns {
16213 table: current_table,
16214 columns,
16215 } if kind == 0 && current_table == table => Some(columns),
16216 Permission::InsertColumns {
16217 table: current_table,
16218 columns,
16219 } if kind == 1 && current_table == table => Some(columns),
16220 Permission::UpdateColumns {
16221 table: current_table,
16222 columns,
16223 } if kind == 2 && current_table == table => Some(columns),
16224 _ => None,
16225 };
16226 if let Some(current) = current {
16227 current.retain(|column| !columns.contains(column));
16228 }
16229 }
16230 permissions.retain(|permission| match permission {
16231 Permission::SelectColumns { columns, .. }
16232 | Permission::InsertColumns { columns, .. }
16233 | Permission::UpdateColumns { columns, .. } => !columns.is_empty(),
16234 _ => true,
16235 });
16236}
16237
16238fn validate_security_catalog(
16239 catalog: &Catalog,
16240 security: &crate::security::SecurityCatalog,
16241) -> Result<()> {
16242 let mut policy_names = HashSet::new();
16243 for table in &security.rls_tables {
16244 if catalog.live(table).is_none() {
16245 return Err(MongrelError::NotFound(format!(
16246 "RLS table {table:?} not found"
16247 )));
16248 }
16249 }
16250 for policy in &security.policies {
16251 if !policy_names.insert((policy.table.clone(), policy.name.clone())) {
16252 return Err(MongrelError::InvalidArgument(format!(
16253 "duplicate policy {:?} on {:?}",
16254 policy.name, policy.table
16255 )));
16256 }
16257 let schema = &catalog
16258 .live(&policy.table)
16259 .ok_or_else(|| {
16260 MongrelError::NotFound(format!("policy table {:?} not found", policy.table))
16261 })?
16262 .schema;
16263 if let Some(expression) = &policy.using {
16264 validate_security_expression(expression, schema)?;
16265 }
16266 if let Some(expression) = &policy.with_check {
16267 validate_security_expression(expression, schema)?;
16268 }
16269 }
16270 let mut mask_names = HashSet::new();
16271 for mask in &security.masks {
16272 if !mask_names.insert((mask.table.clone(), mask.name.clone())) {
16273 return Err(MongrelError::InvalidArgument(format!(
16274 "duplicate mask {:?} on {:?}",
16275 mask.name, mask.table
16276 )));
16277 }
16278 let column = catalog
16279 .live(&mask.table)
16280 .and_then(|entry| {
16281 entry
16282 .schema
16283 .columns
16284 .iter()
16285 .find(|column| column.id == mask.column)
16286 })
16287 .ok_or_else(|| {
16288 MongrelError::NotFound(format!(
16289 "mask column {} on {:?} not found",
16290 mask.column, mask.table
16291 ))
16292 })?;
16293 if matches!(
16294 mask.strategy,
16295 crate::security::MaskStrategy::Redact { .. } | crate::security::MaskStrategy::Sha256
16296 ) && !matches!(column.ty, TypeId::Bytes | TypeId::Enum { .. })
16297 {
16298 return Err(MongrelError::InvalidArgument(format!(
16299 "mask {:?} requires a string/bytes column",
16300 mask.name
16301 )));
16302 }
16303 }
16304 Ok(())
16305}
16306
16307fn validate_security_expression(
16308 expression: &crate::security::SecurityExpr,
16309 schema: &Schema,
16310) -> Result<()> {
16311 use crate::security::SecurityExpr;
16312 match expression {
16313 SecurityExpr::True => Ok(()),
16314 SecurityExpr::ColumnEqCurrentUser { column }
16315 | SecurityExpr::ColumnEqValue { column, .. } => {
16316 if schema
16317 .columns
16318 .iter()
16319 .any(|candidate| candidate.id == *column)
16320 {
16321 Ok(())
16322 } else {
16323 Err(MongrelError::InvalidArgument(format!(
16324 "security expression references unknown column id {column}"
16325 )))
16326 }
16327 }
16328 SecurityExpr::And { left, right } | SecurityExpr::Or { left, right } => {
16329 validate_security_expression(left, schema)?;
16330 validate_security_expression(right, schema)
16331 }
16332 SecurityExpr::Not { expression } => validate_security_expression(expression, schema),
16333 }
16334}
16335
16336fn sweep_unreferenced_table_dirs(root: &Path, cat: &Catalog) -> Result<()> {
16338 let referenced = cat
16339 .tables
16340 .iter()
16341 .filter(|entry| matches!(entry.state, TableState::Live | TableState::Building { .. }))
16342 .map(|entry| entry.table_id)
16343 .collect::<HashSet<_>>();
16344 let tables_dir = root.join(TABLES_DIR);
16345 let entries = match std::fs::read_dir(&tables_dir) {
16346 Ok(entries) => entries,
16347 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
16348 Err(error) => return Err(error.into()),
16349 };
16350 for entry in entries {
16351 let entry = entry?;
16352 if !entry.file_type()?.is_dir() {
16353 continue;
16354 }
16355 let file_name = entry.file_name();
16356 let Some(name) = file_name.to_str() else {
16357 continue;
16358 };
16359 let Ok(table_id) = name.parse::<u64>() else {
16360 continue;
16361 };
16362 if name != table_id.to_string() {
16363 continue;
16364 }
16365 if !referenced.contains(&table_id) {
16366 crate::durable_file::remove_directory_all(&entry.path())?;
16367 }
16368 }
16369 Ok(())
16370}
16371
16372fn sweep_pending_txn_dirs(root: &Path, cat: &Catalog) {
16377 for entry in &cat.tables {
16378 let txn_dir = root
16379 .join(TABLES_DIR)
16380 .join(entry.table_id.to_string())
16381 .join("_txn");
16382 if txn_dir.exists() {
16383 let _ = std::fs::remove_dir_all(&txn_dir);
16384 }
16385 }
16386}
16387
16388#[cfg(test)]
16389mod write_permission_tests {
16390 use super::*;
16391 use crate::txn::Staged;
16392
16393 struct NoopExternalBridge;
16394
16395 impl ExternalTriggerBridge for NoopExternalBridge {
16396 fn apply_trigger_external_write(
16397 &self,
16398 _entry: &ExternalTableEntry,
16399 base_state: Vec<u8>,
16400 _op: ExternalTriggerWrite,
16401 ) -> Result<ExternalTriggerWriteResult> {
16402 Ok(ExternalTriggerWriteResult::new(base_state))
16403 }
16404 }
16405
16406 fn assert_txn_namespace_full<T>(result: Result<T>) {
16407 assert!(matches!(result, Err(MongrelError::Full(_))));
16408 }
16409
16410 #[test]
16411 fn every_begin_api_preserves_transaction_id_exhaustion_without_wal_mutation() {
16412 let directory = tempfile::tempdir().unwrap();
16413 let database = Database::create(directory.path()).unwrap();
16414 let generation = (*database.next_txn_id.lock() >> 32).saturating_add(1);
16415 *database.next_txn_id.lock() = generation << 32;
16416 let before = crate::wal::SharedWal::replay(directory.path())
16417 .unwrap()
16418 .len();
16419 let bridge = NoopExternalBridge;
16420
16421 assert_txn_namespace_full(database.begin().commit());
16422 assert_txn_namespace_full(database.begin_as(None).commit_with_row_ids());
16423 assert_txn_namespace_full(
16424 database
16425 .begin_with_isolation(crate::txn::IsolationLevel::Serializable)
16426 .commit(),
16427 );
16428 assert_txn_namespace_full(
16429 database
16430 .begin_with_external_trigger_bridge(&bridge)
16431 .commit(),
16432 );
16433 assert_txn_namespace_full(
16434 database
16435 .begin_with_external_trigger_bridge_as(&bridge, None)
16436 .commit_controlled(&crate::ExecutionControl::new(None), || Ok(())),
16437 );
16438
16439 assert_eq!(
16440 crate::wal::SharedWal::replay(directory.path())
16441 .unwrap()
16442 .len(),
16443 before
16444 );
16445 drop(database);
16446 Database::open(directory.path()).unwrap();
16447 }
16448
16449 #[test]
16450 fn recovered_storage_identity_mismatch_does_not_mutate_directory() {
16451 let directory = tempfile::tempdir().unwrap();
16452 let table_dir = directory.path().join("7");
16453 crate::durable_file::create_directory_all(&table_dir).unwrap();
16454 let original_schema = test_schema();
16455 crate::engine::write_schema(&table_dir, &original_schema).unwrap();
16456 let mut manifest = crate::manifest::Manifest::new(8, original_schema.schema_id);
16457 crate::manifest::write_atomic(&table_dir, &mut manifest, None).unwrap();
16458 let schema_path = table_dir.join(crate::engine::SCHEMA_FILENAME);
16459 let original_bytes = std::fs::read(&schema_path).unwrap();
16460
16461 let mut replacement_schema = original_schema;
16462 replacement_schema.schema_id += 1;
16463 assert!(matches!(
16464 ensure_recovered_table_storage(None, None, &table_dir, 7, &replacement_schema, None,),
16465 Err(MongrelError::Conflict(_))
16466 ));
16467
16468 assert_eq!(std::fs::read(schema_path).unwrap(), original_bytes);
16469 assert!(!table_dir.join(crate::engine::WAL_DIR).exists());
16470 assert!(!table_dir.join(crate::engine::RUNS_DIR).exists());
16471 assert_eq!(crate::manifest::read(&table_dir, None).unwrap().table_id, 8);
16472 }
16473
16474 #[test]
16475 fn catalog_table_missing_storage_fails_without_recreating_it() {
16476 let directory = tempfile::tempdir().unwrap();
16477 let table_dir = {
16478 let database = Database::create(directory.path()).unwrap();
16479 database.create_table("docs", test_schema()).unwrap();
16480 directory
16481 .path()
16482 .join(TABLES_DIR)
16483 .join(database.table_id("docs").unwrap().to_string())
16484 };
16485 std::fs::remove_dir_all(&table_dir).unwrap();
16486
16487 assert!(matches!(
16488 Database::open(directory.path()),
16489 Err(MongrelError::NotFound(_))
16490 ));
16491 assert!(!table_dir.exists());
16492 }
16493
16494 #[test]
16495 fn authentication_and_principal_resolution_share_one_catalog_snapshot() {
16496 let directory = tempfile::tempdir().unwrap();
16497 let database = std::sync::Arc::new(
16498 Database::create_with_credentials(directory.path(), "admin", "admin-password").unwrap(),
16499 );
16500 database.create_user("alice", "old-password").unwrap();
16501 let old_identity = database.user_identity("alice").unwrap();
16502 let (verified_tx, verified_rx) = std::sync::mpsc::channel();
16503 let (resume_tx, resume_rx) = std::sync::mpsc::channel();
16504 let (mutation_started_tx, mutation_started_rx) = std::sync::mpsc::channel();
16505 let (mutation_done_tx, mutation_done_rx) = std::sync::mpsc::channel();
16506
16507 std::thread::scope(|scope| {
16508 let authenticate = {
16509 let database = std::sync::Arc::clone(&database);
16510 scope.spawn(move || {
16511 database.authenticate_principal_inner("alice", "old-password", || {
16512 verified_tx.send(()).unwrap();
16513 resume_rx.recv().unwrap();
16514 })
16515 })
16516 };
16517 verified_rx.recv().unwrap();
16518 let mutate = {
16519 let database = std::sync::Arc::clone(&database);
16520 scope.spawn(move || {
16521 mutation_started_tx.send(()).unwrap();
16522 database.drop_user("alice").unwrap();
16523 database.create_user("alice", "new-password").unwrap();
16524 mutation_done_tx.send(()).unwrap();
16525 })
16526 };
16527 mutation_started_rx.recv().unwrap();
16528 assert!(mutation_done_rx
16529 .recv_timeout(std::time::Duration::from_millis(50))
16530 .is_err());
16531 resume_tx.send(()).unwrap();
16532 let principal = authenticate.join().unwrap().unwrap().unwrap();
16533 assert_eq!((principal.user_id, principal.created_epoch), old_identity);
16534 mutate.join().unwrap();
16535 });
16536
16537 assert_ne!(database.user_identity("alice").unwrap(), old_identity);
16538 assert!(database
16539 .authenticate_principal("alice", "old-password")
16540 .unwrap()
16541 .is_none());
16542 assert!(database
16543 .authenticate_principal("alice", "new-password")
16544 .unwrap()
16545 .is_some());
16546 }
16547
16548 #[test]
16549 fn homogeneous_batch_summarizes_to_one_permission_decision() {
16550 let staging = (0..10_050)
16551 .map(|_| {
16552 (
16553 7,
16554 Staged::Put(vec![(2, Value::Int64(2)), (1, Value::Int64(1))]),
16555 )
16556 })
16557 .collect::<Vec<_>>();
16558
16559 let needs = summarize_write_permissions(&staging);
16560 let table = needs.get(&7).unwrap();
16561 assert_eq!(needs.len(), 1);
16562 assert!(table.insert);
16563 assert_eq!(table.insert_columns, [1, 2]);
16564 assert!(!table.update);
16565 assert!(!table.delete);
16566 assert!(!table.truncate);
16567 }
16568
16569 #[test]
16570 fn mixed_writes_union_columns_and_preserve_empty_operations() {
16571 let staging = vec![
16572 (7, Staged::Put(vec![(2, Value::Int64(2))])),
16573 (7, Staged::Put(vec![(1, Value::Int64(1))])),
16574 (
16575 7,
16576 Staged::Update {
16577 row_id: RowId(1),
16578 new_row: vec![(1, Value::Int64(1)), (2, Value::Int64(2))],
16579 changed_columns: vec![2],
16580 },
16581 ),
16582 (7, Staged::Delete(RowId(2))),
16583 (8, Staged::Truncate),
16584 ];
16585
16586 let needs = summarize_write_permissions(&staging);
16587 let table = needs.get(&7).unwrap();
16588 assert_eq!(table.insert_columns, [1, 2]);
16589 assert!(table.update);
16590 assert_eq!(table.update_columns, [2]);
16591 assert!(table.delete);
16592 assert!(needs.get(&8).unwrap().truncate);
16593 }
16594
16595 #[test]
16596 fn final_permission_decisions_do_not_scale_with_rows() {
16597 let credentialless_dir = tempfile::tempdir().unwrap();
16598 let credentialless = Database::create(credentialless_dir.path()).unwrap();
16599 credentialless.create_table("docs", test_schema()).unwrap();
16600 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16601 credentialless
16602 .validate_write_permissions(&puts(credentialless.table_id("docs").unwrap()), None, None)
16603 .unwrap();
16604 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 0));
16605
16606 let authenticated_dir = tempfile::tempdir().unwrap();
16607 let authenticated =
16608 Database::create_with_credentials(authenticated_dir.path(), "admin", "admin-password")
16609 .unwrap();
16610 authenticated.create_table("docs", test_schema()).unwrap();
16611 let admin = authenticated.resolve_principal("admin").unwrap();
16612 WRITE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16613 authenticated
16614 .validate_write_permissions(
16615 &puts(authenticated.table_id("docs").unwrap()),
16616 Some(&admin),
16617 None,
16618 )
16619 .unwrap();
16620 WRITE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
16621 }
16622
16623 #[test]
16624 fn delete_batch_checks_permission_once_when_staged_and_once_when_committed() {
16625 let dir = tempfile::tempdir().unwrap();
16626 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
16627 db.create_table("docs", test_schema()).unwrap();
16628 let admin = db.resolve_principal("admin").unwrap();
16629 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16630
16631 let mut transaction = db.begin_as(Some(admin));
16632 transaction
16633 .delete_batch("docs", (0..100).map(RowId).collect())
16634 .unwrap();
16635 transaction.commit().unwrap();
16636
16637 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 2));
16638 }
16639
16640 #[test]
16641 fn truncate_validation_checks_admin_once_for_all_tables() {
16642 let dir = tempfile::tempdir().unwrap();
16643 let db = Database::create_with_credentials(dir.path(), "admin", "admin-password").unwrap();
16644 db.create_table("first", test_schema()).unwrap();
16645 db.create_table("second", test_schema()).unwrap();
16646 let admin = db.resolve_principal("admin").unwrap();
16647 let staging = vec![
16648 (db.table_id("first").unwrap(), Staged::Truncate),
16649 (db.table_id("second").unwrap(), Staged::Truncate),
16650 ];
16651
16652 TABLE_PERMISSION_DECISIONS.with(|decisions| decisions.set(0));
16653 db.validate_write_permissions(&staging, Some(&admin), None)
16654 .unwrap();
16655 TABLE_PERMISSION_DECISIONS.with(|decisions| assert_eq!(decisions.get(), 1));
16656 }
16657
16658 #[test]
16659 fn one_table_commit_batches_structural_work() {
16660 let dir = tempfile::tempdir().unwrap();
16661 let db = Database::create(dir.path()).unwrap();
16662 db.create_table("docs", test_schema()).unwrap();
16663 let table_id = db.table_id("docs").unwrap();
16664
16665 AUTO_INCREMENT_TABLE_LOCKS.with(|count| count.set(0));
16666 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
16667 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
16668 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
16669 db.transaction(|transaction| {
16670 for id in 0..100 {
16671 transaction.put("docs", vec![(1, Value::Int64(id))])?;
16672 }
16673 Ok(())
16674 })
16675 .unwrap();
16676
16677 AUTO_INCREMENT_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 2));
16678 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16679 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16680 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
16681
16682 let puts = crate::wal::SharedWal::replay(dir.path())
16683 .unwrap()
16684 .into_iter()
16685 .filter_map(|record| match record.op {
16686 crate::wal::Op::Put { table_id: id, rows } if id == table_id => Some(
16687 bincode::deserialize::<Vec<crate::memtable::Row>>(&rows)
16688 .unwrap()
16689 .len(),
16690 ),
16691 _ => None,
16692 })
16693 .collect::<Vec<_>>();
16694 assert_eq!(puts, [100]);
16695
16696 let row_ids = db
16697 .table("docs")
16698 .unwrap()
16699 .lock()
16700 .visible_rows(db.snapshot().0)
16701 .unwrap()
16702 .into_iter()
16703 .take(2)
16704 .map(|row| row.row_id)
16705 .collect::<Vec<_>>();
16706 PREBUILD_TABLE_LOCKS.with(|count| count.set(0));
16707 PUBLISH_TABLE_LOCKS.with(|count| count.set(0));
16708 COMMIT_MANIFEST_WRITES.with(|count| count.set(0));
16709 db.transaction(|transaction| {
16710 for row_id in row_ids {
16711 transaction.delete("docs", row_id)?;
16712 }
16713 Ok(())
16714 })
16715 .unwrap();
16716 PREBUILD_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16717 PUBLISH_TABLE_LOCKS.with(|count| assert_eq!(count.get(), 1));
16718 COMMIT_MANIFEST_WRITES.with(|count| assert_eq!(count.get(), 1));
16719
16720 let deletes = crate::wal::SharedWal::replay(dir.path())
16721 .unwrap()
16722 .into_iter()
16723 .filter_map(|record| match record.op {
16724 crate::wal::Op::Delete {
16725 table_id: id,
16726 row_ids,
16727 } if id == table_id => Some(row_ids.len()),
16728 _ => None,
16729 })
16730 .collect::<Vec<_>>();
16731 assert_eq!(deletes, [2]);
16732 }
16733
16734 fn puts(table_id: u64) -> Vec<(u64, Staged)> {
16735 (0..10_050)
16736 .map(|id| (table_id, Staged::Put(vec![(1, Value::Int64(id))])))
16737 .collect()
16738 }
16739
16740 fn test_schema() -> Schema {
16741 Schema {
16742 columns: vec![ColumnDef {
16743 id: 1,
16744 name: "id".into(),
16745 ty: TypeId::Int64,
16746 flags: crate::schema::ColumnFlags::empty()
16747 .with(crate::schema::ColumnFlags::PRIMARY_KEY),
16748 default_value: None,
16749 }],
16750 ..Schema::default()
16751 }
16752 }
16753}
16754
16755#[cfg(test)]
16756mod cdc_bounds_tests {
16757 use super::*;
16758
16759 #[test]
16760 fn retained_byte_limit_rejects_without_allocating_payload() {
16761 let mut retained = 0;
16762 let error = charge_cdc_bytes(
16763 &mut retained,
16764 CDC_MAX_RETAINED_BYTES.saturating_add(1),
16765 "CDC retained bytes",
16766 )
16767 .unwrap_err();
16768 assert!(matches!(
16769 error,
16770 MongrelError::ResourceLimitExceeded {
16771 resource: "CDC retained bytes",
16772 ..
16773 }
16774 ));
16775 }
16776
16777 #[test]
16778 fn row_json_estimate_accounts_for_byte_array_expansion() {
16779 let row = crate::memtable::Row::new(RowId(1), Epoch(1))
16780 .with_column(1, Value::Bytes(vec![0; 1024]));
16781 assert!(cdc_row_json_bytes(&row) >= 1024 * std::mem::size_of::<serde_json::Value>());
16782 }
16783}
16784
16785#[cfg(test)]
16786mod generation_metrics_tests {
16787 use super::*;
16788 use crate::schema::{ColumnDef, ColumnFlags, Schema, TypeId};
16789
16790 #[test]
16791 fn legacy_cow_fallback_is_measured() {
16792 let dir = tempfile::tempdir().unwrap();
16793 let table = Table::create(
16794 dir.path(),
16795 Schema {
16796 columns: vec![ColumnDef {
16797 id: 1,
16798 name: "id".into(),
16799 ty: TypeId::Int64,
16800 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
16801 default_value: None,
16802 }],
16803 ..Schema::default()
16804 },
16805 1,
16806 )
16807 .unwrap();
16808 let handle = TableHandle::from_table(table);
16809 let held = match &handle.inner {
16810 TableHandleInner::CopyOnWrite(slot) => Arc::clone(&slot.read()),
16811 TableHandleInner::Direct(_) => unreachable!(),
16812 };
16813
16814 handle.lock().set_sync_byte_threshold(1);
16815
16816 let stats = handle.generation_stats();
16817 assert_eq!(stats.cow_clone_count, 1);
16818 assert!(stats.estimated_cow_clone_bytes > 0);
16819 drop(held);
16820 }
16821}
16822
16823#[cfg(test)]
16824mod trigger_engine_tests {
16825 use super::*;
16826
16827 fn event_with(new_cells: &[(u16, Value)], old_cells: &[(u16, Value)]) -> WriteEvent {
16828 WriteEvent {
16829 table: "test".into(),
16830 kind: TriggerEvent::Insert,
16831 new: Some(TriggerRowImage {
16832 columns: new_cells.iter().cloned().collect(),
16833 }),
16834 old: Some(TriggerRowImage {
16835 columns: old_cells.iter().cloned().collect(),
16836 }),
16837 changed_columns: Vec::new(),
16838 op_indices: Vec::new(),
16839 put_idx: None,
16840 trigger_stack: Vec::new(),
16841 }
16842 }
16843
16844 fn event_insert(new_cells: &[(u16, Value)]) -> WriteEvent {
16845 WriteEvent {
16846 table: "test".into(),
16847 kind: TriggerEvent::Insert,
16848 new: Some(TriggerRowImage {
16849 columns: new_cells.iter().cloned().collect(),
16850 }),
16851 old: None,
16852 changed_columns: Vec::new(),
16853 op_indices: Vec::new(),
16854 put_idx: None,
16855 trigger_stack: Vec::new(),
16856 }
16857 }
16858
16859 #[test]
16860 fn value_order_int64_vs_float64() {
16861 assert_eq!(
16862 value_order(&Value::Int64(5), &Value::Float64(5.0)),
16863 Some(std::cmp::Ordering::Equal)
16864 );
16865 assert_eq!(
16866 value_order(&Value::Int64(5), &Value::Float64(3.0)),
16867 Some(std::cmp::Ordering::Greater)
16868 );
16869 assert_eq!(
16870 value_order(&Value::Int64(2), &Value::Float64(3.0)),
16871 Some(std::cmp::Ordering::Less)
16872 );
16873 }
16874
16875 #[test]
16876 fn value_order_null_returns_none() {
16877 assert_eq!(value_order(&Value::Int64(5), &Value::Null), None);
16878 assert_eq!(value_order(&Value::Null, &Value::Int64(5)), None);
16879 assert_eq!(value_order(&Value::Null, &Value::Null), None);
16880 }
16881
16882 #[test]
16883 fn value_order_cross_group_returns_none() {
16884 assert_eq!(
16885 value_order(&Value::Int64(5), &Value::Bytes(b"x".to_vec())),
16886 None
16887 );
16888 assert_eq!(value_order(&Value::Bool(true), &Value::Int64(1)), None);
16889 assert_eq!(
16890 value_order(
16891 &Value::Embedding(vec![1.0, 2.0]),
16892 &Value::Embedding(vec![1.0, 2.0])
16893 ),
16894 None
16895 );
16896 }
16897
16898 #[test]
16899 fn eval_trigger_expr_ranges_and_booleans() {
16900 let expr = TriggerExpr::And {
16901 left: Box::new(TriggerExpr::Gt {
16902 left: TriggerValue::NewColumn(1),
16903 right: TriggerValue::Literal(Value::Int64(0)),
16904 }),
16905 right: Box::new(TriggerExpr::Lte {
16906 left: TriggerValue::NewColumn(1),
16907 right: TriggerValue::Literal(Value::Int64(100)),
16908 }),
16909 };
16910 assert!(eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(50))])).unwrap());
16911 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Int64(200))])).unwrap());
16912 assert!(!eval_trigger_expr(&expr, &event_insert(&[(1, Value::Null)])).unwrap());
16913
16914 let or_expr = TriggerExpr::Or {
16915 left: Box::new(TriggerExpr::Lt {
16916 left: TriggerValue::NewColumn(1),
16917 right: TriggerValue::Literal(Value::Int64(0)),
16918 }),
16919 right: Box::new(TriggerExpr::Not(Box::new(TriggerExpr::IsNull(
16920 TriggerValue::OldColumn(2),
16921 )))),
16922 };
16923 assert!(eval_trigger_expr(
16924 &or_expr,
16925 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Int64(99))])
16926 )
16927 .unwrap());
16928 assert!(!eval_trigger_expr(
16929 &or_expr,
16930 &event_with(&[(1, Value::Int64(5))], &[(2, Value::Null)])
16931 )
16932 .unwrap());
16933
16934 assert!(eval_trigger_expr(
16935 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(true))),
16936 &event_insert(&[])
16937 )
16938 .unwrap());
16939 assert!(!eval_trigger_expr(
16940 &TriggerExpr::Value(TriggerValue::Literal(Value::Bool(false))),
16941 &event_insert(&[])
16942 )
16943 .unwrap());
16944 assert!(!eval_trigger_expr(
16945 &TriggerExpr::Value(TriggerValue::Literal(Value::Null)),
16946 &event_insert(&[])
16947 )
16948 .unwrap());
16949 }
16950}